Localization, accessibility and system integration
Ship text in many languages with String Catalogs, format numbers and dates per locale, support right-to-left layouts, make VoiceOver usable, and expose your app to the system with widgets and App Intents.
String Catalogs and formatting
// Localizable.xcstrings is a String Catalog; add languages in Xcode
Text("welcome_title") // looked up automatically
Text(verbatim: "v2.4.1") // never localise
// Plural variation is declared in the catalog, not in code
Text("\(count) items") // catalog entry with a plural rule
// Locale-aware formatting: never build these strings by hand
let price = Decimal(1299) / 100
Text(price, format: .currency(code: "GBP"))
let now = Date.now
Text(now, format: .dateTime.day().month(.wide).year())
Text(now, format: .relative(presentation: .named))- Enable the missing-translation check in build settings so new keys fail the build instead of shipping English.
- Never concatenate translated fragments — word order differs across languages. Use a single parameterised key.
- Use
verbatim:for identifiers, URLs and version numbers so they are never extracted for translation. - Test with a pseudo-locale or a long language such as German; English-length labels hide truncation.
Right-to-left and VoiceOver
struct Row: View {
let task: Task
var body: some View {
HStack {
Image(systemName: task.done ? "checkmark.circle.fill" : "circle")
.accessibilityHidden(true) // decorative, the label carries meaning
VStack(alignment: .leading) {
Text(task.title).font(.body)
Text(task.due, format: .dateTime.day().month())
.font(.caption).foregroundStyle(.secondary)
}
Spacer()
}
.accessibilityElement(children: .combine)
.accessibilityLabel("\(task.title), due \(task.due.formatted(date: .abbreviated, time: .omitted))")
.accessibilityValue(task.done ? "Completed" : "Not completed")
.accessibilityHint("Double tap to toggle")
}
}| Need | API | Note |
|---|---|---|
| Reading order in RTL | Leading/trailing edges | Avoid hard-coded .left and .right |
| Contrast in Dark Mode | Semantic colors | Use .primary, .secondary, .bar |
| Scale with text size | ScaledMetric | Fixed point sizes do not follow Dynamic Type |
| Reduce motion | @Environment(\.accessibilityReduceMotion) | Replace large transitions with a fade |
Extending into the system
import AppIntents
struct MarkDoneIntent: AppIntent {
static var title: LocalizedStringResource = "Mark task done"
static var openAppWhenRun = false
@Parameter(title: "Task ID") var taskID: String
func perform() async throws -> some IntentResult {
try await TaskStore.shared.markDone(taskID)
return .result()
}
}💡
Widget timelines are built from snapshots, not live views. Keep the timeline provider cheap, refresh on meaningful events rather than every minute, and share data with the app through an App Group container.
FAQ
How do I find untranslated strings?
Use the String Catalog's filter for missing or stale entries and turn on the build setting that fails or warns on them. Unused keys are equally worth pruning so translators are not paying for dead text.
Does accessibility work cost much effort?
Far less than retrofitting it. Adding labels and combining elements as you build a row takes minutes; auditing a finished app for clipped text, tiny targets and unlabeled icons takes days.
Related
Layout, stacks and adaptive UI Security, privacy and data protection
Last refreshed 2026-09-18.