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")
    }
}
NeedAPINote
Reading order in RTLLeading/trailing edgesAvoid hard-coded .left and .right
Contrast in Dark ModeSemantic colorsUse .primary, .secondary, .bar
Scale with text sizeScaledMetricFixed 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.

Layout, stacks and adaptive UI Security, privacy and data protection

Last refreshed 2026-09-18.