compose-multiplatform-localization
Skill15 starsChanged 22 days agoApache-2.0View on GitHub
Use when localising a Compose Multiplatform app — composeResources strings.xml, values-fr / values-b+zh+Hans folders, plurals, string arrays, stringResource, pluralStringResource, getString, generated Res class, format arguments, iOS language not switching (CFBundleLocalizations), Android per-app language, in-app language picker, RTL layout, locale-aware dates and numbers, pseudo-locales, or "%s shows literally", missing translations, wrong plural.
In this repository34 other files it gives its agents.
- AGENTS.mdAGENTS.md
- android-to-kmp-migrationSkill
- compose-design-system-componentSkill
- compose-multiplatform-navigationSkill
- compose-multiplatform-screenSkill
- figma-to-mobile-uiSkill
- kmp-api-endpointSkill
- kmp-build-checkSkill
- kmp-build-failure-triageSkill
- kmp-ci-github-actionsSkill
- kmp-code-reviewSkill
- kmp-crash-triageSkill
- kmp-data-layerSkill
- kmp-dev-machine-cleanupSkill
- kmp-expect-actualSkill
- kmp-feature-moduleSkill
- kmp-flaky-test-triageSkill
- kmp-gradle-version-catalogSkill
- kmp-koin-diSkill
- kmp-module-healthSkill
- kmp-room-databaseSkill
- kmp-secure-storageSkill
- kmp-swiftui-interopSkill
- kmp-test-coverage-backfillSkill
- kmp-testing-patternsSkill
- kmp-xcframework-distributionSkill
- mobile-accessibility-test-idsSkill
- mobile-bug-fixSkill
- mobile-feature-implementSkill
- mobile-feature-planSkill
- mobile-performance-fixSkill
- mobile-platform-paritySkill
- mobile-releaseSkill
- mobile-security-reviewSkill
The file
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 2 are distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 13
All user-visible text lives in commonMain/composeResources/values*/strings.xml and is resolved in the UI layer.
- Very specific: line 37
Mechanism: Search for composeResources, Res.string., stringResource(, moko (MR.strings), or Android R.string in shared code.
- Very specific: line 117
Keep indices stable so translators can reorder: "%2$s a %1$d notes" is valid.
- Distinctive: line 131
Never build sentences by concatenating fragments.
- Very specific: line 155
Android: add res/xml/locale_config.xml listing the languages and reference it with android:localeConfig on <application>.
- Very specific: line 186
Keep inherently LTR content LTR (phone numbers, media controls, code) with CompositionLocalProvider(LocalLayoutDirection provides …
- Distinctive: line 188
If Arabic text appears on iOS but the layout does not mirror, check the declared localizations (step 7) before debugging layout code.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Compose Multiplatform localization
All user-visible text lives in commonMain/composeResources/values*/strings.xml and is resolved in
the UI layer. ViewModels carry what to say (an enum, a resource reference), not the localised
string. Each platform must also be told which languages the app supports, and dates/numbers are
formatted with the platform's locale-aware formatters.
When to use
- Adding the first translation, a new language, or new strings to a CMP app.
- Plurals, format arguments, string arrays, resources used outside composables.
- iOS shows the wrong language or system UI in a different language from the app; the Android per-app language setting is missing; building an in-app language picker.
- Right-to-left support; locale-aware date, time, number and currency formatting.
- Symptoms:
%sor%.2fappears literally, a crash or blank text for one locale, "1 notes", English month names inside French text.
When not to use
- Projects that localise with another library (moko-resources, Lyricist, native
Localizable.xcstrings+ Androidres/): follow that library; the platform-declaration, RTL and formatting sections still apply. - Strings inside a native SwiftUI UI → the iOS string catalog; see SwiftUI skills in Related.
Step 0 — Read the project first
- Mechanism. Search for
composeResources,Res.string.,stringResource(, moko (MR.strings), or AndroidR.stringin shared code. Keep the existing one. - Resource config. In the module's
build.gradle.kts, thecompose.resources { }block:publicResClass,packageOfResClass,generateResClass. Note which module owns whichRes. - Languages. List the
values-*folders and compare with iOSCFBundleLocalizations(iosApp/iosApp/Info.plist) and Androidandroid:localeConfig/res/xml/locale_config.xml. - How text leaves ViewModels. Strings, resource ids, enums, a
UiTexttype? - Formatting. Existing date/number helpers (
expect/actual, kotlinx-datetime formats); AndroidminSdk(java.time needs core-library desugaring below API 26). - Key naming convention in existing
strings.xml.
Workflow
1. Dependency and configuration
# gradle/libs.versions.toml — use the Compose Multiplatform version
[libraries]
compose-components-resources = { module = "org.jetbrains.compose.components:components-resources", version.ref = "compose-multiplatform" }kotlin {
sourceSets {
commonMain.dependencies { implementation(libs.compose.components.resources) }
}
}
compose.resources {
packageOfResClass = "com.example.app.resources" // stable import path, independent of module renames
// publicResClass = true // only if other modules must read this module's Res
}The compose.components.resources accessor of the Compose Gradle plugin is deprecated from Compose
Multiplatform 1.10; use the catalog entry. With the Android KMP library plugin, also set
androidResources.enable = true in kotlin { androidLibrary { } } (AGP 8.8+).
Done when a build generates Res and Res.string.<key> resolves in the IDE.
2. Write the default strings
commonMain/composeResources/values/strings.xml holds every key, in the fallback language:
<resources>
<string name="notes_list_title">Notes</string>
<string name="notes_greeting">Hello, %1$s!</string>
<string name="notes_synced_at">Last synced %1$s</string>
<plurals name="notes_selected_count">
<item quantity="one">%1$d note selected</item>
<item quantity="other">%1$d notes selected</item>
</plurals>
<string-array name="notes_sort_options">
<item>Newest first</item>
<item>Oldest first</item>
</string-array>
</resources>- Name keys
feature_screen_element(or the project's convention); never by content (hello_text). - A key that exists only in a translation folder fails at runtime for every other locale.
3. Add translations
| Language | Folder |
| French | values-fr/ |
| Portuguese (Brazil) | values-pt-rBR/ (lowercase r before the region) |
| Chinese, Simplified / Traditional | values-b+zh+Hans/, values-b+zh+Hant/ plus a script-less values-zh/ |
| Arabic (RTL) | values-ar/ |
Qualifiers are case-sensitive. Without the script-less folder, a device reporting zh without a
script finds no resource and falls back to the default.
4. Format arguments: positional %N$s / %N$d only
Compose Multiplatform substitutes only %1$s, %2$d, … ($s and $d behave the same). Anything
else — %s, %d, %.2f, %1$.2f, %@ — is shown literally.
- Pre-format numbers, dates and currency in code (step 9) and pass the result as
%1$s. - Keep indices stable so translators can reorder:
"%2$s a %1$d notes"is valid. - Translations must use the same set of indices as the default (the script checks it). Passing fewer arguments than the highest index throws at runtime.
5. Plurals and arrays
Text(pluralStringResource(Res.plurals.notes_selected_count, count, count)) // quantity, then format arg
val options: List<String> = stringArrayResource(Res.array.notes_sort_options)- Quantity is an
Int. The plural rule (zero/one/two/few/many/other) is chosen from the current language;otheris required in every language. - Pass the number twice: once as the quantity, once as the format argument.
- Never build sentences by concatenating fragments; word order differs between languages.
- Translations of a string array must keep the same number of items.
6. Resolve text in the UI; carry intent from ViewModels
Text(stringResource(Res.string.notes_greeting, userName))stringResourcefollows the composition's locale and recomposes when it changes.- ViewModels expose an enum / sealed type (
NoteEditorError.SaveFailed) that the composable maps to a resource — the ViewModel stays testable without resources, and Compose-free logic modules (which cannot depend on the resources library) work the same way. - Outside composition (notification text, share sheets) use
suspend fun getString(Res.string.x). It reads the process locale — AndroidLocaleList.getAdjustedDefault(), iOSNSLocale.preferredLanguages— not a Compose-level override.
7. Declare supported languages on each platform
- iOS: list every language in
Info.plist→CFBundleLocalizations(and keepCFBundleDevelopmentRegionas the default). Compose resources do not live in.lprojfolders, so without this key iOS treats the app as single-language: system-provided UI (alerts, pickers, permission prompts) can stay in the development language, the per-app Language option in Settings is typically not offered, and the App Store may list only one language. - Android: add
res/xml/locale_config.xmllisting the languages and reference it withandroid:localeConfigon<application>. AGP'sgenerateLocaleConfigderives the list from Androidres/values-*folders, so it does not seecomposeResourcesstrings — write the file by hand unless the module also has matching Android resources.
Code and details: references/platform-language-setup.md.
8. In-app language picker
- Android:
AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags("fr"))(activity must extendAppCompatActivity; add theautoStoreLocalesservice entry for API < 33).stringResourcefollows it through the activity configuration. - iOS: there is no supported runtime API; the reliable path is opening the app's Settings page
(
UIApplication.openSettingsURLString), where the per-app language appears once step 7 is done. - JetBrains documents a cross-platform workaround (a
LocalAppLocaleCompositionLocal that rewrites the platform locale); it changes process-wide state and JetBrains calls it temporary. See the reference before adopting it.
9. Dates, times, numbers, currency
kotlinx-datetime does not do locale-aware formatting (its README puts internationalisation out of
scope); its format DSL builds fixed patterns. Put a small formatter interface in commonMain and
implement it per platform with java.time/java.text (Android) and NSDateFormatter/
NSNumberFormatter (iOS). Code: references/formatting-dates-numbers.md.
10. Right-to-left
- Compose mirrors layouts automatically for RTL locales. Use
start/end, neverleft/right. - Directional icons (back arrows, chevrons) must mirror:
Icons.AutoMirrored.*orImageVector.Builder(autoMirror = true). For vector drawables incomposeResources, check on an RTL device thatandroid:autoMirrored="true"is honoured by your Compose Multiplatform version. - Keep inherently LTR content LTR (phone numbers, media controls, code) with
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { … }. - If Arabic text appears on iOS but the layout does not mirror, check the declared localizations (step 7) before debugging layout code.
11. Check, pseudo-localise, test
python3 scripts/l10n_check.py check shared/src/commonMain/composeResources # JSON report
python3 scripts/l10n_check.py check shared/src/commonMain/composeResources --format table
python3 scripts/l10n_check.py pseudo shared/src/commonMain/composeResources --dry-runcheck reports keys missing from values/ (error), placeholder mismatches and unsupported
placeholders (error), plurals without other (error), missing translations and array-size
mismatches (warning). pseudo writes values-en-rXA/strings.xml with accented, padded, bracketed
text so truncation and hard-coded strings stand out. Locale-switching commands and test patterns:
references/testing-localization.md.
Quick reference
| Need | API / location |
| String in a composable | stringResource(Res.string.key) / stringResource(Res.string.key, arg1, arg2) |
| Plural | pluralStringResource(Res.plurals.key, quantity, quantity) |
| Array | stringArrayResource(Res.array.key) |
| Outside composition | getString(...), getPluralString(...), getStringArray(...) (suspend) |
| Placeholders | %1$s, %2$d only |
| Escapes | \n, \t, \uXXXX; @ and ? need no escaping |
| Language + region folder | values-pt-rBR |
| Script | values-b+zh+Hans + values-zh fallback |
| Force a direction | CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) |
| iOS supported languages | Info.plist → CFBundleLocalizations |
| Android per-app languages | res/xml/locale_config.xml + android:localeConfig |
Verify
Report each check as PASS, FAIL or NOT RUN (reason).
python3 scripts/l10n_check.py check <module>/src/commonMain/composeResources # exit 0 = no errors
python3 -m unittest discover -s scripts/tests # helper's own tests (from the skill dir)
./gradlew :shared:compileKotlinMetadata # Res accessors generate and compile
./gradlew :composeApp:assembleDebug
/usr/libexec/PlistBuddy -c "Print :CFBundleLocalizations" iosApp/iosApp/Info.plist # macOSManual (report NOT RUN without a device/simulator): switch to each supported language on Android and iOS; check one RTL language; run once with the pseudo-locale; set an unsupported language and confirm the default strings appear.
Common mistakes
| Mistake | Why it bites | Fix |
%s, %d, %.2f in strings | Shown literally; CMP only substitutes %N$s/%N$d | Positional placeholders; pre-format numbers |
Key only in values-xx/ | Lookup fails for every other locale | Every key in values/ |
values-zh-Hans folder name | Scripts are only recognised in the b+ form | values-b+zh+Hans plus values-zh |
No CFBundleLocalizations on iOS | System UI and formatters disagree with app text; no per-app language in Settings | Declare all languages |
Relying on generateLocaleConfig | It reads Android res/, not composeResources | Hand-written locale_config.xml |
Resolving strings in the ViewModel with getString | Ignores Compose-level locale changes; harder to test | Emit an enum/resource; resolve in UI |
"$count " + stringResource(...) concatenation | Breaks word order and plural rules | One string with placeholders / plurals |
Plural without other, or quantity passed but not the format arg | Runtime exception: a missing category falls back to other, and a missing other throws; a placeholder without a matching argument throws an index error | other always; pass the number twice |
Modifier.padding(left = …) / non-mirrored back arrow | Wrong side in RTL | start/end; auto-mirrored icons |
LocalDate.toString() shown to users | ISO format, not localised | Platform formatter via expect/actual |
| Exact-string assertions on formatted dates | CLDR data differs between JDK/Android/iOS versions (e.g. narrow no-break spaces) | Assert on parts or normalise whitespace |
| Expecting Android pseudo-locales to cover CMP strings | They are built from Android res/ | Generate values-en-rXA with the script |
Related skills
compose-multiplatform-screen— where text is rendered and how errors travel from the ViewModel.compose-design-system-component— components that own strings (e.g. "Loading").figma-to-mobile-ui— moving design copy into resources.- AvdLee/SwiftUI-Agent-Skill and twostraws/SwiftUI-Agent-Skill — for a native SwiftUI UI with its own string catalog.
- Meet-Miyani/compose-skill — broader Compose/CMP skill set.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

