Skip to main content

Localize iOS Live Activities Across the Lock Screen and Dynamic Island

2026-09-16

Localize iOS Live Activities Across the Lock Screen and Dynamic Island

Live Activity localization can fail while the main app remains perfectly translated. A delivery starts in French, but its Lock Screen card keeps an English status. The compact Dynamic Island view uses an untranslated abbreviation. VoiceOver reads a hardcoded label. A server update then inserts another English sentence into otherwise French interface copy.

The fix is not to translate the ActivityKit payload. Separate interface messages from live data. Put localizable labels, plurals, accessibility copy, and status messages in resources owned by the rendering extension. Send typed values and stable state codes from the app or server. Then define how a running activity responds when the selected app language changes, and verify every presentation rather than one screenshot.

Why Live Activity localization breaks

A Live Activity crosses boundaries that an ordinary screen does not. Apple's Live Activity implementation guide places the interface in a widget extension and requires support for Lock Screen, compact, minimal, and expanded presentations. ActivityKit controls the lifecycle, while the app or ActivityKit push notifications supply changing state.

Failures usually land in four places:

  1. The extension cannot access the localization resources that contain a required key.
  2. Dynamic state contains finished source-language prose instead of values the extension can format.
  3. One presentation or accessibility path uses a literal that never entered the string catalog.
  4. A running activity was created under an older language decision and has no defined update policy.

Calling all four a translation bug hides the cause. Check resource ownership, state design, presentation coverage, and lifecycle separately.

The app and Live Activity can disagree even when the screen is translated. A working app screen proves only that the application target found its resources. It does not test the extension target, content received from APNs, or text in the minimal Dynamic Island presentation.

Inventory every message surface first

Before touching code, inventory each visible or spoken message. Record where it originates, which target renders it, and whether it is static or derived from changing data.

Surface Typical copy Owner Update source
Lock Screen Status, ETA label, action text Widget extension App or push state
Compact presentation Short status or unit Widget extension Activity state
Minimal presentation Symbol label and accessibility text Widget extension Activity state
Expanded presentation Detailed status and actions Widget extension Activity state
Update alert Title and body App or notification service APNs payload
Stale or ended state Outdated and completion messages Widget extension Activity lifecycle

Do not use a single screenshot as the inventory. Compact and minimal views often contain different copy because space is limited. Accessibility labels may not be visible at all. Apple's guide explicitly calls for accessibility labels on the SwiftUI views used by each Live Activity presentation, so they belong in the localization checklist rather than a later accessibility pass.

Give Live Activity keys a clear namespace such as live_activity.delivery.status.preparing. Keep translator comments specific to the surface and constraint. A comment like "Compact Dynamic Island status, maximum two short words" is more useful than "Status label."

Apple's String Catalog documentation explains that Xcode can extract localizable SwiftUI strings, manage plurals, add device variants, and attach context. Extraction still depends on using localizable APIs and on the catalog being included in the target that renders the text. Add a release check that confirms the extension owns every required catalog and locale.

Send state values instead of translated sentences

The server should report what happened, not decide how every ActivityKit view phrases it. A brittle content state might contain statusText: "Driver is 3 minutes away". That text combines a status, a quantity, a unit, and English grammar. It cannot be reordered safely, and a language change cannot reformat it without another server update.

Prefer a coded state with typed values:

struct DeliveryActivityAttributes: ActivityAttributes {
    struct ContentState: Codable, Hashable {
        enum Phase: String, Codable {
            case preparing
            case collected
            case nearby
            case delivered
        }

        let phase: Phase
        let estimatedArrival: Date?
        let stopCount: Int?
        let localeTag: String
        let localeRevision: Int
    }

    let orderID: String
}

The phase selects a reviewed message key. The date remains a date until the extension formats it. The count remains numeric so the catalog can apply the target language's plural rules. localeTag and localeRevision make the language decision observable for a running activity. They are a product-level implementation pattern, not fields required by ActivityKit.

Keep identifiers stable and language neutral. Do not send an English enum value and display it directly. nearby is a state code, while "Arriving soon" is localizable interface copy.

The ActivityKit push-notification guide requires the payload's content-state to match the custom ContentState type declared by the app. That makes the state boundary enforceable. Decode known states, reject malformed payloads during preflight, and define a safe unknown-state presentation for forward compatibility.

A server update can then stay compact and language neutral:

{
  "aps": {
    "timestamp": 1786002000,
    "event": "update",
    "content-state": {
      "phase": "nearby",
      "estimatedArrival": 1786002300,
      "stopCount": 1,
      "localeTag": "fr-CA",
      "localeRevision": 8
    }
  }
}

The extension turns those values into the final message. This design also avoids making one broadcast update carry a sentence that is correct for only one language.

Render with one effective locale

Write one locale contract for the app and its Live Activities. Use a fixed precedence order:

  1. A supported language selected explicitly inside the app.
  2. A supported per-app language supplied by the system.
  3. The best supported device language.
  4. The product's required default locale.

Resolve that decision before starting the activity and include its canonical language tag and revision in the initial content state. Apply the resolved locale to the ActivityConfiguration view, then perform message lookup and date, time, number, and unit formatting inside that environment.

The following pattern is illustrative rather than drop-in production code:

ActivityConfiguration(for: DeliveryActivityAttributes.self) { context in
    DeliveryLockScreenView(state: context.state)
        .environment(
            \.locale,
            Locale(identifier: context.state.localeTag)
        )
} dynamicIsland: { context in
    DeliveryDynamicIsland(state: context.state)
        .environment(
            \.locale,
            Locale(identifier: context.state.localeTag)
        )
}

Centralize phase-to-key mapping so the Lock Screen and Dynamic Island cannot invent different terminology. Each presentation can choose a shorter approved variant, but both variants should come from the same catalog and status model.

Do not preformat estimatedArrival in the app process and pass the resulting sentence to the extension. Store the date and format it at render time. The same rule applies to distances, currency, percentages, counts, and units.

Decide what a language change does to a running activity

A language switch is a state transition, not only a screen redraw. Home-screen widgets hit the same stale-after-switch problem, and the widget refresh workflow for language changes uses the same ownership and verification approach. Do not assume an active Live Activity will automatically rebuild every presentation using the new app preference. Choose and test one explicit policy.

For an app-driven activity, persist the new locale first, increment localeRevision, and update each active activity with the same business state plus the new locale fields. Confirm that the newly rendered content uses the updated revision.

For a push-driven activity, synchronize the current locale and revision to the notification service. The next update should preserve the business state while carrying the new language decision. If the product requires an immediate visual change, have the app issue a local ActivityKit update rather than waiting for the next remote event.

If changing locale in place does not produce a consistent result on every supported operating system, end and restart the activity only when that behavior is acceptable to users and your lifecycle rules permit it. Restarting is not a universal default. It can change presentation timing and activity identity, so document the tradeoff and test it as a separate path.

Define fallback without changing the saved user preference. When one key is missing, use the extension's known default localization, emit a non-sensitive diagnostic, and fail the catalog completeness check in CI. A missing translation should not silently rewrite fr-CA to English for future updates.

Keep update alerts separate from activity state

ActivityKit push updates may include alert text. Apple specifically tells implementers to consider localizing both alert strings in its push-notification guide. Treat that alert as a second message channel, not as a field that the Live Activity view will automatically localize.

Choose a strategy:

  • Use bundled localization keys when the alert vocabulary ships with the app and can wait for normal releases.
  • Resolve alert copy on the server when it is dynamic, provided the server has the user's current language and the translation has passed review.
  • Omit the alert for routine state changes that do not need an interruption.

The activity's content-state should remain typed under all three options. Do not reuse a server-localized alert sentence as the status text in the extension. Alert wording and glanceable interface wording have different space, timing, and accessibility requirements.

Also plan for missed updates. Apple notes that devices may not receive a push and that notifications arriving after an activity ends are ignored. Use stale state to render an approved localized message such as "Update delayed" instead of leaving old data looking current. Ended activities need localized final content because they can remain visible after the event finishes.

Localize accessibility as real product copy

Visible labels do not cover VoiceOver. Every actionable control needs an appropriate accessible name, and changing data may need a phrase that makes sense when spoken without the surrounding layout.

A Mozilla engineering issue documents the practical work to localize accessibility strings for a Firefox iOS Live Activity. The issue calls out both Lock Screen and Dynamic Island labels and hints. This is a practitioner implementation report, not an Apple guarantee, but it shows why accessibility copy requires its own inventory.

For each presentation, test:

  • The reading order after the state changes.
  • Labels for icons that have no visible text.
  • Hints for buttons and toggles.
  • Plural and unit pronunciation.
  • Compact copy that expands into a clearer spoken label.
  • Stale, error, and ended states.

Do not build an accessibility label by concatenating translated fragments. Give translators one complete message with placeholders so they can control grammar and order.

Verify every presentation and lifecycle path

Use an installed release-like build. Previews are useful for layout work, but they do not prove target membership, push decoding, active-state updates, or language-change behavior.

For each launch locale, run at least these cases:

  • Start locally in the default language.
  • Start locally in a non-default app language.
  • Start or update through the supported ActivityKit push path.
  • Inspect Lock Screen, compact, minimal, and expanded presentations.
  • Enable VoiceOver and inspect labels, values, hints, and reading order.
  • Change the app language while the activity is active.
  • Update after the language change with the app in foreground and background states.
  • Mark the activity stale and verify the localized stale message.
  • End the activity and inspect the final retained presentation.
  • Remove one test translation and confirm the documented fallback plus diagnostic.
  • Test dates, counts, and units at plural and formatting boundaries.

Record activityID, phase, effectiveLocale, localeRevision, update source, and render time in development diagnostics. Exclude user content and translated sentences. Those fields let an engineer distinguish an old payload from a missing catalog entry without collecting sensitive copy.

When a test fails, keep the current activity running. Confirm that it received the expected locale revision, then check the extension resource and phase-to-key mapping. Inspect the formatter next. Last, determine whether the system is showing an older rendered state. An immediate reinstall discards evidence that identifies the failing boundary.

Avoid the common shortcuts

Do not put display-ready sentences in ContentState. That moves grammar and translation ownership into every producer.

Do not assume the main app catalog is visible to the extension. Verify target membership in the built product.

Do not test only the expanded Dynamic Island view. Compact and minimal presentations have independent content and accessibility needs.

Do not update the locale before persisting the user's choice, or an extension may read the previous value and cache another inconsistent state.

Do not translate alert text and call the activity complete. The alert, visual status, action labels, accessibility copy, stale state, and final state are separate messages.

Make the next activity update testable

Choose one existing Live Activity and replace its display-ready status sentence with a stable phase code. Move the corresponding status messages and accessibility labels into the extension-owned string catalog. Add localeTag and localeRevision to the content state, then run one installed-build test that changes the app language while the activity remains active.

The test passes only when the Lock Screen, compact, minimal, expanded, alert, and VoiceOver paths agree on the same locale revision. Once that lifecycle works for one activity, apply the ownership matrix and verification suite to every ActivityKit experience in the app.

References