Android shortcut localization can fail while every in-app screen looks correct. A long-press menu may show translated static labels, a dynamic shortcut may retain yesterday's language, and a pinned copy may open the right route with the wrong locale. The launcher owns part of this surface, so recomposing the app cannot repair it. A reliable implementation gives each shortcut a stable identity, separates label generation from route data, republishes runtime records after locale changes, and tests what the launcher actually displays.
This guide builds that contract for static, dynamic, and pinned shortcuts. It also covers cold starts, updates, framework boundaries, failure recovery, and a release matrix that catches stale labels before users do.
Understand the three shortcut lifecycles
Android's App shortcuts overview defines three types with different ownership. Static shortcuts are declared in an XML resource packaged with the app. Dynamic shortcuts are published and changed by application code. Pinned shortcuts are placed on a supported launcher with user approval, and users may also pin copies of static or dynamic shortcuts.
That distinction determines how localization updates reach the screen. A static shortcut can reference locale-qualified string resources, so Android can resolve its label from the installed package. A dynamic shortcut receives a concrete label when the app publishes it. The launcher does not know that the text came from a translation key. A pinned shortcut may outlive the dynamic record from which it was copied, depending on launcher behavior and the operations the app performs later.
Treat these as three release surfaces:
- The packaged static definition must contain resource references and valid default resources.
- The runtime publisher must build labels using the app's effective locale, not an incidental process or device locale.
- The pinned surface needs an explicit update and fallback policy because another application renders it.
A single screenshot from the main activity verifies none of these contracts.
Give every shortcut a stable semantic ID
Use an ID that describes the action, not its current label, language, rank, or destination text. open_saved_items can survive translation changes. saved_items_english cannot. The ID is how later calls find and update an existing record.
The Android ShortcutManager reference exposes manifest, dynamic, pinned, and cached shortcut sets. It also documents updateShortcuts, which updates existing shortcuts with matching IDs. Stable IDs let the app replace user-visible text without creating duplicate launcher actions.
Define the identity and route as language-neutral data:
data class ShortcutSpec(
val id: String,
val destination: String,
val shortLabelKey: Int,
val longLabelKey: Int,
)
val savedItemsShortcut = ShortcutSpec(
id = "open_saved_items",
destination = "saved-items",
shortLabelKey = R.string.shortcut_saved_items_short,
longLabelKey = R.string.shortcut_saved_items_long,
)
Keep the route stable as well. The intent should carry a destination identifier or a versioned application URI, not a localized path segment. Locale is presentation context, not shortcut identity. If the app requires an explicit locale during cold start, resolve it from the user's stored app preference when handling the intent. Do not freeze the language into a shortcut created months earlier.
Changing what an ID means is a migration, not an ordinary label update. Disable the old shortcut with a clear localized message when an action disappears, introduce a new ID for the replacement behavior, and document whether old pinned copies can still reach a safe destination.
Localize static shortcuts through Android resources
Static shortcut XML should reference string resources for every visible label. Android's localization guide explains locale-qualified resource directories and requires complete defaults so the app can run under any unsupported locale.
A static definition can look like this:
<shortcut
android:shortcutId="open_saved_items"
android:enabled="true"
android:icon="@drawable/ic_shortcut_saved">
<intent
android:action="android.intent.action.VIEW"
android:targetPackage="com.example.app"
android:targetClass="com.example.app.ShortcutRouterActivity"
android:data="example://shortcut/saved-items" />
<categories android:name="android.shortcut.conversation" />
<shortcut-short-label android:string="@string/shortcut_saved_items_short" />
<shortcut-long-label android:string="@string/shortcut_saved_items_long" />
</shortcut>
The exact elements depend on the application and supported Android versions, but the ownership rule is constant: visible text belongs in resources, while IDs and route data remain stable.
Add the shortcut strings to the default values/strings.xml file before adding translations. Then add corresponding entries under locale-qualified directories. Keep labels concise because launcher space is limited and varies by device. Give translators context that says where each label appears, whether it names an action or destination, and how the short and long variants relate.
Resource parity is necessary but insufficient. Build the release app bundle, install it under each target locale, and inspect the launcher's long-press menu. This catches incorrect resource ownership, missing files from a build variant, and launcher behavior that unit tests cannot render.
Publish dynamic labels from the effective app locale
Dynamic shortcuts need a dedicated publisher. Do not scatter ShortcutInfo.Builder calls across screens. One component should receive the effective app locale, create a locale-aware resource context, build all current labels, and publish or update the records.
fun publishLocalizedShortcuts(
appContext: Context,
locale: Locale,
specs: List<ShortcutSpec>,
) {
val config = Configuration(appContext.resources.configuration)
config.setLocale(locale)
val localized = appContext.createConfigurationContext(config)
val manager = appContext.getSystemService(ShortcutManager::class.java)
val shortcuts = specs.map { spec ->
val intent = Intent(
Intent.ACTION_VIEW,
"example://shortcut/${spec.destination}".toUri(),
appContext,
ShortcutRouterActivity::class.java,
)
ShortcutInfo.Builder(appContext, spec.id)
.setShortLabel(localized.getString(spec.shortLabelKey))
.setLongLabel(localized.getString(spec.longLabelKey))
.setIntent(intent)
.build()
}
manager.setDynamicShortcuts(shortcuts)
}
This is an implementation pattern, not a universal requirement to replace the entire set every time. Use updateShortcuts when preserving records not owned by the current operation matters. Respect platform rate limits, avoid republishing on every activity resume, and centralize retry decisions.
Trigger publication after the app commits a language preference, after login restores a server-side preference, and after an app update changes shortcut copy or definitions. Also publish when relevant user data changes the available dynamic actions. Store the last successfully published locale tag and shortcut schema version. If both match the desired state, skip unnecessary work.
Never use translated labels as lookup keys. When the language changes from English to Japanese, the publisher should update open_saved_items, not search for a record named “Saved.”
Decide how pinned shortcuts should behave
Pinned shortcuts need conservative promises. The launcher renders and stores them, and implementations differ. Query the current shortcut sets through ShortcutManager, update records by stable ID where supported, and verify the result on launchers your audience actually uses. Do not claim that changing a dynamic label will automatically repair every previously pinned copy.
Choose one product policy:
- Pinned shortcuts follow the current app language. Republish labels after a language change and test whether supported launchers update their copies.
- Pinned shortcuts preserve the language used when the user created them. This is rarely desirable, but it can be valid if the label represents user-authored text.
- The app cannot guarantee pinned-label refresh. Keep the action safe, update what the platform exposes, and document a remove-and-pin-again recovery path in support material.
For publisher-authored action labels, following the effective app language is usually the least surprising policy. User-authored names, such as a saved conversation title, should retain the user's text while surrounding action labels remain localized.
Do not delete and recreate shortcuts merely to force a visual refresh. Deletion can break user placement and history. First update matching IDs, inspect the returned sets, and reserve disable or removal operations for actions that no longer exist.
Route shortcut intents without losing locale context
A shortcut can display the correct language and still launch a mixed-language screen. Handle every shortcut through one routing boundary that initializes locale state before rendering the destination.
The router should:
- Validate the shortcut ID or destination against an allowlist.
- Load the stored effective app locale.
- Initialize locale-dependent resources before creating the destination UI.
- Resolve authentication and authorization without replacing the locale.
- Open the destination or a localized safe fallback.
- Record the shortcut ID, app locale, and outcome without logging user content.
This boundary matters for cold starts. A warm process may already hold the selected locale, while a process started by the launcher must rebuild it from durable state. Test both paths.
Framework wrappers add another boundary. A public Flutter report describes static Android shortcuts not being forwarded to the application. That report concerns one integration, but it demonstrates why an XML declaration is not enough. Verify that native intent delivery reaches the cross-platform router, including when the engine is not running.
Handle failures without multiplying stale records
Publication can fail because of rate limits, malformed definitions, unavailable destinations, or framework initialization. Keep the previous valid shortcuts when an update fails. Record the desired locale and schema version, then retry at a bounded lifecycle event such as the next foreground transition. Avoid an unbounded retry loop.
If a translation is missing, use the default resource through Android's normal fallback. Do not publish an empty label or a key name. Treat fallback use as telemetry and a release defect, not a crash condition.
If a shortcut destination has been removed, disable the old ID with a localized explanation when possible. Route unavoidable old pinned intents to a safe screen that explains what changed. Never repurpose the ID for an unrelated action.
When the effective locale changes while publication is in progress, compare the completed locale with current state. A slow English update must not overwrite a newer French request. Serialize updates or attach a monotonically increasing generation number and discard stale work before publication.
Verify the launcher surface before release
Build a matrix around lifecycle transitions rather than checking one translated label. Include at least the default language, one non-Latin script, one right-to-left language if supported, and one regional locale with shared base language.
For every locale, verify:
- Long-press static labels after a clean install.
- Dynamic labels after first launch and user sign-in.
- Pinned labels before and after an in-app language change.
- Warm and cold shortcut launches.
- App update behavior when copy or shortcut schema changes.
- Missing translation fallback.
- Removed destination recovery.
- Process death between pinning and invocation.
- A framework build where native intent delivery must cross into shared code.
Capture the visible launcher menu and destination screen in the same test record. Compare the expected locale tag, shortcut ID, displayed labels, route, and rendered destination language. Automated resource tests can catch missing keys, but a launcher screenshot or device assertion closes the gap that current shortcut documentation leaves open.
Start with one high-value shortcut. Assign it a stable ID, move all visible text into owned resources, centralize dynamic publication, and run the clean-install plus language-change matrix. Add the remaining shortcuts only after that path stays correct across the launchers you support.
For the iOS side of the same problem, see how to localize iOS app shortcuts and Quick Actions.
References
- Android App shortcuts overview supports the distinction between static, dynamic, and pinned shortcut lifecycles.
- Android ShortcutManager reference supports querying, publishing, updating, disabling, and removing shortcut records.
- Android localization guide supports locale-qualified resources and complete default resources.
- Flutter static Android shortcut issue provides a practitioner report about shortcut intent delivery across a framework boundary.
