Skip to main content

Keep Share Extensions and Share Targets in the App's Selected Language

2026-10-26

Keep Share Extensions and Share Targets in the App's Selected Language

Share extension localization can fail even when every screen in the main app is correct. A user selects French, opens another app, taps Share, and sees your extension in English. The failure happens because the extension may run in another process with its own bundle and no access to the main app's in-memory locale. Android share targets add a similar system entry point with a separate activity lifecycle. Fix the boundary by persisting the resolved app locale in shared platform storage, loading target-owned resources before rendering, defining a first-run fallback, and testing sharing after every language change.

Why the share flow loses the app locale

A share flow spans more than one owner. The source app creates the payload. The operating system displays the chooser or share sheet. Your receiving extension or activity renders its own labels, validation, preview, actions, and errors. The containing app may not be running at all.

Apple's archived Share extension guide describes a separate extension target with its own principal view controller, property list, interface, and extension context. That context provides shared text and attachments, but it does not provide the containing app's current language choice. A locale stored only in a singleton, browser local storage, dependency injection scope, or process-local preferences disappears at this boundary.

A public Actual Chat issue documented the exact symptom: its iOS share extension rendered every string in English because the extension ran separately and could not reach the selected language stored in the app. The linked merged fix passed the resolved interface language through an App Group and loaded the extension catalog against it. This is one application's implementation report, but the process boundary is the part worth copying into a design review.

Android uses a different component model. Its guide to receiving shared data shows that an activity becomes a share target by declaring intent filters and handling external ACTION_SEND or ACTION_SEND_MULTIPLE payloads. The activity can be created directly from the system chooser. If it builds UI before applying the app locale, resource lookup may use a device or stale configuration instead.

Give every sharing surface an explicit owner

Inventory the text before writing locale plumbing. A share extension can contain a display name, navigation title, placeholder, post button, attachment status, unsupported-content warning, authentication request, cancellation message, and upload failure. An Android target may expose an activity label in the chooser and then render an import screen. Sharing out from your app can also include app-authored subject lines or message templates.

Assign each string to one of three owners:

Surface Owner Locale source Verification
System chooser controls Operating system System policy Confirm, do not override
Extension or target UI Extension or target bundle Resolved app locale Open from another app
Shared subject or message Sending app Locale of the intended recipient or current user Inspect each destination
Imported content Source app or user Preserve original language Never translate silently
Upload and validation errors Receiving component Resolved app locale Force each failure path

Do not assume the main target's catalog automatically belongs to every extension. Apple's Xcode localization documentation treats localized resources as build resources, so target membership and bundle lookup must be checked. Android's localization guide likewise depends on default and locale-specific resources packaged with the receiving application.

A shared localization library can reduce duplicate keys, but only if the extension can include it without pulling in unavailable app dependencies. Keep extension-facing strings in a resource-bearing module that the extension can load. Give extension-specific copy its own keys. Copying a catalog during the build without declaring ownership creates two files that can drift unnoticed.

Persist the resolved locale across the process boundary

Store the locale that the app actually renders, not merely the user's nullable preference. The resolved value already accounts for supported locales, script variants, regional fallback, and product default. Persist it whenever the containing app resolves language at launch and whenever the user changes language.

On iOS, use an App Group container shared by the containing app and its extension. On Android, use an application-scoped locale source that the share activity can read before it creates localized UI. The platform mechanics differ, but the record can follow the same contract.

SharedLocaleState {
  requested_locale: "pt-BR"
  resolved_locale: "pt-BR"
  catalog_version: 184
  written_at: "2026-09-07T09:30:00Z"
  source: "user-selection"
}

resolveForShareTarget(state, packagedLocales):
  if state.resolved_locale is packaged:
    return state.resolved_locale
  if language fallback is packaged:
    return language fallback
  return product default

Write the state atomically. The extension should never read a half-written locale tag or catalog version. Keep the record small, non-sensitive, and independent from account tokens. A language setting does not justify opening broad shared storage to every extension.

Record both requested and resolved values when that distinction helps debugging. For example, a user may request zh-Hant-HK while the product packages zh-Hant. The extension should render the same fallback as the main app instead of resolving the request independently and landing on another script.

Do not put translated strings in the shared locale record. Persist stable state, then let the receiving component load its own reviewed resources. Storing rendered copy ties future extension launches to an old catalog and makes a translation update harder to activate safely.

Handle first run, stale state, and missing resources

A share extension can be invoked before the containing app has ever launched. The shared locale record will not exist. Choose a deterministic fallback: resolve from the extension's packaged localizations and the operating system's ordered language preferences, then use the product default. Show usable default text rather than a key or empty control.

An existing installation can also contain stale state after an upgrade. Include a small schema or catalog version so the reader can distinguish a valid older selection from malformed data. If the locale tag is invalid or unsupported, ignore it and resolve again. Do not crash the share flow because a preference written by an earlier release has an unexpected value.

Missing target membership deserves its own test. The saved locale may be correct while the extension bundle lacks that language. Build-time checks should compare the supported locale registry against every share target's packaged resources. Runtime fallback remains necessary, but it should not hide a release packaging error from CI.

Keep original shared content intact. A link title, note, image caption, or selected passage may be written in a different language from the interface. Translate extension controls, not user-authored or source-authored content. If the product offers translation, make it a separate visible action that preserves the original.

Apply locale before rendering any target UI

Read locale state at the earliest safe component entry point. On iOS, initialize localization before the extension's view creates strings. On Android, apply the application locale or localized context before inflating the receiving activity. Replacing labels after the first frame can flash the wrong language and leave cached formatters or accessibility descriptions unchanged.

Use one adapter rather than scattered locale reads:

struct ShareLocaleResolution {
    let requested: String?
    let resolved: String
    let fallback: String
}

func resolveShareLocale(
    sharedValue: String?,
    packagedLocales: Set<String>,
    productDefault: String
) -> ShareLocaleResolution {
    guard let sharedValue else {
        return .init(requested: nil, resolved: productDefault, fallback: "first-run")
    }
    if packagedLocales.contains(sharedValue) {
        return .init(requested: sharedValue, resolved: sharedValue, fallback: "exact")
    }
    let language = sharedValue.split(separator: "-").first.map(String.init)
    if let language, packagedLocales.contains(language) {
        return .init(requested: sharedValue, resolved: language, fallback: "language")
    }
    return .init(requested: sharedValue, resolved: productDefault, fallback: "default")
}

Treat this as pseudocode, not a complete iOS implementation. Production code should canonicalize language tags through the platform locale API and resolve script-sensitive fallbacks through the same registry used by the main app. The useful design property is that every fallback is named and testable.

If the extension has a long operation, capture the effective locale for that presentation. A language change in the containing app should affect the next invocation. Mutating a visible compose controller halfway through an upload can be more confusing than finishing that short transaction consistently.

Keep errors and authentication in the same language

Share targets often do more than accept a payload. They validate content types, load previews, ask the user to sign in, upload data, or hand off to the containing app. These branches expose locale gaps faster than a successful text share.

Map stable error conditions to extension-owned resources. Do not branch on English exception messages. Preserve diagnostics for logs, but show a reviewed message and action such as retry, open app, or cancel. If authentication must continue in the containing app, pass a stable route and validated locale hint rather than translated route text.

When the extension cannot access an account session, say so in the selected language and provide a supported recovery path. Do not copy credentials into shared preferences to make the extension convenient. Shared locale state and authentication state have different risk profiles.

Cancellation must also finish cleanly. The Apple extension context described in the Share extension guide owns completion and cancellation for the host request. Localized UI should not interfere with calling the correct completion path or preserving the source payload.

Test from the operating system entry point

Launching the extension target directly from an IDE proves little about the installed share flow. Test it from at least two source apps and through the system chooser on a release build.

For each supported launch locale, cover these cases:

  1. Fresh install where the containing app has never opened.
  2. First app launch followed by a share from another app.
  3. Explicit language change, app termination, then share invocation.
  4. Regional locale that falls back to a language-only catalog.
  5. Unsupported locale that reaches the product default.
  6. Text, URL, image, multiple attachment, and unsupported payload paths.
  7. Authentication missing, upload failure, retry, cancellation, and handoff.
  8. Right-to-left layout, text expansion, VoiceOver or TalkBack labels, and large text.
  9. Upgrade from a build that never wrote shared locale state.
  10. Extension resource removed accidentally from one localized release build.

Check the chooser label, first rendered frame, compose controls, attachment preview, progress state, validation, errors, and destination after handoff. On Android, verify that every advertised MIME type is handled safely because the receiving guide warns that senders can supply unexpected data. On iOS, verify activation rules so the extension appears only for payloads it can process.

Log locale resolution without user content. Useful fields include component name, app version, extension version, requested locale, resolved locale, fallback class, and catalog version. Do not log shared text, URLs, filenames, account details, or attachment metadata merely to diagnose language selection.

Add sharing to the localization release gate

Before the next mobile release, list every share extension and share target beside its owning catalog. Add a CI check for locale parity and target membership. Persist the resolved app locale through the supported cross-process store, implement explicit first-run and missing-resource fallbacks, and open the installed share flow after changing language.

Then force one unsupported payload, one authentication failure, one retry, and one cancellation from another app. Ship only when the chooser label, extension UI, errors, and return path use the intended language without changing or translating the shared content.

Extensions only see strings their own target bundles; see how to share iOS localization across app targets and Swift packages.

References