A multilingual keyboard product can ship a polished container app and still show English return keys, English space-bar copy, or one English layout under system Settings. The usual cause is not a missing host translation. The keyboard extension and the IME subtype layer own those strings. The host String Catalog never paints them unless the extension target ships localized resources and correct language metadata.
Put the keyboard extension on the same release bar as the host. Inventory extension chrome separately from the container, set PrimaryLanguage and Android subtype labels on purpose, and verify the IME after real Language & Region changes.
Why keyboard chrome drifts from the container language
A custom keyboard has two owners. The container app owns onboarding, paid-plan screens, theme pickers, and full-access explanations. The operating system hosts the keyboard extension beside every text field and lists subtypes in Settings. Apple's Custom Keyboard App Extension guide describes the extension as a separate process with its own Info.plist attributes. UIInputViewController is the primary view controller for that extension. Android's IME stack presents subtype locale and label metadata through InputMethodSubtype when users open language and keyboard settings.
The localization gap shows up in five places:
- Host UI strings live in the app target's catalog. Key labels, accessory-bar titles, and empty-state tips often live only in the keyboard extension target.
PrimaryLanguageand runtimeprimaryLanguagedescribe which language the keyboard claims to type. That is not the same as which UI language the extension bundle resolves.- Android subtype
localeandlabelattributes appear in system Settings even when the in-IME layout XML is already translated. - Xcode scheme Application Language can make the host look correct while the simulator device language still leaves the extension on English.
- Hardcoded English literals such as "Done", "ABC", or "123" never reach translators because they never lived in a catalog the extension loads.
Practitioner reports line up with that split. A Stack Overflow author describes a keyboard that always opened in English despite scheme language settings and a mul PrimaryLanguage value. The fix they landed on was base-language ordering plus simulator Language and Region changes so the extension could resolve the intended localization. Another author asks how to expose multiple keyboard layouts under Settings the way system keyboards do. KeyboardKit's public tracker discusses migrating keyboard localization types toward LocalizedStringResource, which is library-specific evidence that keyboard chrome is treated as its own localization surface. Those reports describe specific projects, not a universal OS defect. They still name the integration questions your workflow must answer.
General iOS localization guides, including Phrase's iOS localization overview and the live i18nAgent iOS guide, cover catalogs and UI strings. They do not connect keyboard-extension bundles to language metadata and Settings subtype labels. The Android localization guide likewise stops short of IME subtype labeling. Host-field keyboard hints covered elsewhere are a different problem: aligning an editable field's IME language is not the same as localizing the keyboard product you ship.
Build a keyboard-extension string inventory
Localize the keyboard extension target as its own bundle: host-app String Catalog membership does not paint UIInputViewController key labels or Android IME chrome.
For each keyboard-facing string, record:
| Field | Why it matters |
|---|---|
| Stable key | Shared meaning across iOS extension and Android IME when the role matches |
| Apple surface | Key caption, return/space/globe labels, accessory bar, full-access prompt inside the extension |
| Android surface | Key labels, candidates bar, subtype label, notification or setup copy owned by the IME |
| Bundle owner | Keyboard extension target, IME package, shared framework with explicit extension membership |
| Language metadata | PrimaryLanguage, runtime primaryLanguage, Android subtype locale |
| Fallback | Base localization that must remain English-complete without masking missing translations in QA |
| Verification | Device Language & Region, Settings → Keyboards / Languages & input, cold start after language change |
Put return, space, shift, mode-switch, and candidate-bar strings in the extension catalog with translator notes that the string appears on the system keyboard surface and must stay short. Do not assume a shared host catalog is enough. Xcode target membership and Android res/values-*/ packages for the IME module are part of the localization contract.
Keep a separate inventory for Settings-visible names. On Android that includes every InputMethodSubtype label. On iOS it includes how your product name and layout names appear under Keyboards settings when users enable the extension.
Configure PrimaryLanguage separately from UI translations
Treat PrimaryLanguage and UIInputViewController.primaryLanguage as release metadata separate from translation files, and re-verify after device language changes because scheme language alone does not prove the extension resolves the right .lproj.
Apple's App Extension Keys reference defines PrimaryLanguage as a BCP 47 language tag for custom keyboards. The Custom Keyboard guide explains that you can also switch the primary language dynamically through UIInputViewController.primaryLanguage, and the property documentation states that a set value supersedes the Info.plist key.
Use that metadata for these jobs:
- Declare the language the keyboard is designed to type when you ship a single-layout product.
- Update
primaryLanguagewhen the user switches layouts inside a multi-script keyboard. - Keep
PrefersRightToLeftaligned with RTL layouts instead of inferring direction only from the host app locale. - Never treat
PrimaryLanguage = mulas a substitute for shipping localized UI resources in the extension.
QA must change Language and Region on the device or simulator, not only the Xcode scheme Application Language. The Stack Overflow localization report above is a concrete reminder that extension resolution follows the environment the IME actually runs in.
On Android, follow the creating an input method guidance and define subtypes whose android:imeSubtypeLocale and label resources match the layouts you ship. Inventory Android InputMethodSubtype locale and label resources as Settings-visible strings, not optional extras, so subtype menus do not stay English while the in-IME layout looks translated.
<!-- Illustrative subtype declaration; keep labels in localized resources -->
<subtype
android:label="@string/subtype_french_azerty"
android:imeSubtypeLocale="fr_FR"
android:imeSubtypeMode="keyboard" />
Resolve @string/subtype_french_azerty from values-fr as well as the default values package. A French layout with an English Settings label still fails the multilingual release bar.
Own host-versus-extension catalog boundaries
Shared code is useful. Shared assumptions are not. If a Swift package or Android module supplies key captions, confirm the keyboard extension or IME process can load those resources at runtime. A string that resolves in the container's unit tests can still be missing when the extension runs in a tighter sandbox.
Practical rules:
- Give the keyboard extension its own String Catalog or
.lprojtree, even when keys mirror the host. - If you share a framework, enable the localization resources for the extension target and verify loading with the extension scheme.
- Keep English base strings complete so missing translations fail loudly in pseudo-localization rather than silently falling back in production.
- Document which strings are typing-language metadata versus UI chrome so translators do not "translate" a BCP 47 tag.
Multi-layout products need an explicit Settings story. System keyboards expose nested layout choices; custom keyboards do not automatically get the same UI. If you support AZERTY, QWERTZ, and phonetic layouts, decide whether those are in-IME switches, separate subtypes, or both, and localize every user-visible name on that path.
Verify the IME the way users enable it
Release checks should exercise the keyboard as an extension, not only as a preview controller inside the host.
Minimum verification matrix:
| Check | Pass condition |
|---|---|
| Fresh install, device language German | Extension key labels and return/space strings are German before the user opens the container |
| Host language override English, device Japanese | Documented policy: either follow device language for IME chrome or follow product policy without mixed English keys |
| Subtype / layout switch | Settings labels and primaryLanguage / subtype locale update together |
| RTL layout | PrefersRightToLeft and mirrored key geometry match the active layout |
| Missing translation | Pseudo-locale or missing-key test fails CI instead of shipping English leftovers unnoticed |
| Cold start after OS language change | No stale English captions from a cached extension process without a documented relaunch policy |
Automate what you can inside the extension test target, then keep a short manual pass on a physical device because Settings paths and keyboard selection UI differ across OS versions.
Release workflow checklist
- Inventory extension key labels, accessory copy, and Settings subtype strings separately from container marketing UI.
- Confirm extension target membership for every catalog or
values-*package the IME loads. - Set
PrimaryLanguage/ runtimeprimaryLanguageand Android subtype locales to match shipped layouts. - Localize subtype labels as first-class Settings strings.
- Run pseudo-localization on the extension scheme.
- Verify Language & Region changes on simulator and one physical device per major OS.
- Block release if the container is translated but the IME still shows English chrome.
How i18nAgent helps
Once extension and subtype strings live in normal resource catalogs, they can move through the same translation workflow as the rest of the app. i18nAgent helps teams keep iOS and Android resource files in sync, review AI drafts with human auditability, and catch formatting issues before a keyboard release. Pair that pipeline with the extension-specific checks above so polish in the host app is not mistaken for a finished IME.
Key takeaways
- Custom keyboard key labels and Settings subtype names are extension-owned, not host-catalog side effects.
- Configure
PrimaryLanguage/primaryLanguageand Android subtype locales as release metadata, then verify with device Language & Region changes. - Localize
InputMethodSubtypelabels as Settings-visible strings alongside in-IME captions. - Gate release on extension-scheme and physical-device checks, not only on a translated container app.
