Skip to content

[Resources] Support prioritized locale fallback chain for resources (CMP-6840) - #5754

Closed
Abhishek Keshri (2KAbhishek) wants to merge 6 commits into
JetBrains:masterfrom
2KAbhishek:fix/resource-locale-fallback-chain
Closed

Abhishek Keshri (2KAbhishek) wants to merge 6 commits into
JetBrains:masterfrom
2KAbhishek:fix/resource-locale-fallback-chain

Conversation

@2KAbhishek

@2KAbhishek Abhishek Keshri (2KAbhishek) commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Description

Resolves CMP-6840 by supporting prioritized multi-locale resource matching across all platforms.

Previously, resource matching evaluated only a single platform locale. If an application provided resources in Spanish (values-es/) but not French, a bilingual user preferring French then Spanish ([fr, es]) received the default values/ folder instead of their second-preference Spanish. Furthermore, platforms reported only a single preferred locale to Compose.

Key Changes & Design Considerations

  1. Prioritized Multi-Locale Matching:

    • Evaluates the ordered list of user locales against available resources following the multi-locale resolution sequence (as in Table 3 of Android's Multilingual Support guide).
    • Supports same-language region fallback (e.g., fr-CH $\rightarrow$ fr-FR). To avoid bundling heavy CLDR distance tables across KMP targets, CMP uses deterministic alphabetical selection (.minOrNull()) when multiple alternate regions are available.
    • Prohibits cross-script substitution for Chinese (zh-Hans / zh-CN never substitutes for zh-Hant / zh-TW), covering both region-qualified and script-qualified folders (values-b+zh+Hans/). Explicit script qualifiers are respected across all languages.
  2. Full Preferred Locale Reporting Across Platforms:

    • Android: Reads LocaleList.getDefault(), respecting per-app language settings and Android 13+ app locales.
    • Apple: Reads NSLocale.preferredLanguages directly (retaining region and script qualifiers without Xcode bundle filtering).
    • Web (JS/Wasm): Reads navigator.languages.
    • Desktop (JVM): Queries the system preferred language list via Windows registry (HKCU\Control Panel\International\User Profile), macOS (/usr/bin/defaults read -g AppleLanguages), or POSIX environment variables (LANGUAGE, LC_ALL, LANG), with a 2-second timeout and lazy single-instance caching. Falls back safely to Locale.getDefault() if subprocess execution is restricted (e.g., sandboxed macOS environments).
    • Retains androidx.compose.ui.text.intl.LocaleList.current at the head of the chain when set (preserving explicit app overrides like Locale.setDefault).
  3. Base Language Convention for Unqualified Resources:

    • By convention in Compose Multiplatform, composeResources/values/ represents the base development language (English by default).
    • When evaluating English without an explicit values-en/ directory, unqualified values/ resources satisfy unregionalized English candidates. This ensures bilingual English users (e.g., [en-US, es-US]) receive base English rather than unintentionally skipping to a secondary language override.
    • Explicit values-en/ and regional values-en-rXX/ folders continue to take strict precedence when provided.
  4. Resource-Aware Pluralization:

    • Evaluates plural categories using the locale of the matched resource item rather than the user's primary preference. Unqualified resources default to English plural rules.
  5. API Stability:

    • Synced desktop and KLib API dumps (apiDump) for the @InternalResourceApi data class LocaleQualifiers addition.

Fixes CMP-6840

Testing

Run from the components directory:

./gradlew :resources:library:desktopTest \
  :resources:library:testAndroidHostTest \
  :resources:library:compileKotlinJs \
  :resources:library:compileKotlinWasmJs
  • Added comprehensive unit tests in ResourceTest covering multi-locale fallback chains, regional fallback, script mismatch isolation, pluralization locale selection, and unqualified resource precedence.
  • Verified test suites on :resources:library:desktopTest and :resources:library:testAndroidHostTest.
  • Verified multiplatform compilation across iOS, macOS, JS, and WasmJS targets.

Release Notes

Fixes - Resources

  • Resources are matched against the user's full list of preferred languages instead of only the first one, so a language further down the list is used when preceding languages have no matching resources
    • A locale falls back to another region of the same language (e.g., fr-CH uses values-fr-rFR). Script compatibility is strictly enforced so zh-Hans never matches values-zh-rTW
    • Unqualified values/ resources satisfy base English when no explicit values-en/ folder exists, preventing bilingual users with English as a primary preference from unintentionally falling back to secondary language overrides
    • Plural categories follow the language of the matched resource rather than the primary device preference
    • Per-app language settings are respected on Android, and the system preference list is read on Desktop instead of the single JVM default locale

Note: AI assistance was used for this PR

@2KAbhishek
Abhishek Keshri (2KAbhishek) marked this pull request as ready for review October 13, 2026 07:46
@2KAbhishek
Abhishek Keshri (2KAbhishek) force-pushed the fix/resource-locale-fallback-chain branch 2 times, most recently from 840049a to abb46ae Compare October 13, 2026 08:00
@2KAbhishek Abhishek Keshri (2KAbhishek) changed the title Match resources against the user's preferred locale list [resources] Support prioritized locale fallback chain for resources (CMP-6840) Oct 13, 2026
@2KAbhishek
Abhishek Keshri (2KAbhishek) force-pushed the fix/resource-locale-fallback-chain branch 2 times, most recently from 9012f2a to d856db1 Compare October 13, 2026 10:10
Resource matching only looked at the first preferred language, so an app
that ships Spanish but no French gave a user who prefers French, then
Spanish, the unqualified values/ folder.

Walk the list and take the first locale that matches, as in table 3 of
Android's multilingual support guide. A locale also falls back to
another region of the same language first, as in table 2, but never
across scripts: a locale that names one is matched on it, and otherwise
the script the region implies is compared, so neither zh-Hans nor zh-CN
lands on values-zh-rTW.

Fixes https://youtrack.jetbrains.com/issue/CMP-6840
Every platform handed the matcher a list of one, so a user whose first
language has no resources could not reach their second.

Android reads LocaleList.getDefault(), which follows a per-app language
where Resources.getSystem() does not. Apple reads
NSLocale.preferredLanguages, and the web reads navigator.languages. A
single locale from LocaleList.current stays in front of the list, since
an app that calls Locale.setDefault to choose its language cannot be
told apart from a platform default here.

The new internal systemPreferredLocales() keeps composition from also
resolving a theme and a screen DPI that it takes from Compose anyway.

Fixes https://youtrack.jetbrains.com/issue/CMP-6840
Plural categories came from the first preferred locale, which is no
longer the language the string is written in. A user who prefers French
and then English, in an app shipping only English plurals, got the
French rule for zero.

Take the language and the region from the matched resource item instead.

Fixes https://youtrack.jetbrains.com/issue/CMP-6840
Desktop was the last target still reporting a single locale. Neither the
JDK nor a dependency of this module exposes the list, so the Windows one
is read from the registry, the macOS one through defaults, and other
systems fall back to the gettext environment variables. Both
subprocesses resolve once per process behind a timeout, and any failure
leaves the single platform locale in place.

A POSIX locale has no script field and carries it in the modifier, so
the script-bearing modifiers from glibc localedata/SUPPORTED are mapped
and the rest, which name a currency or a dialect, are dropped.

Fixes https://youtrack.jetbrains.com/issue/CMP-6840
An application shipping base development resources without language
qualifiers in values/ (by convention, English) must not skip those
resources when a user's prioritized locale list includes English ahead of
a secondary language that has explicit overrides (e.g. values-hi/).

Allow unqualified resources to provide the unregionalized base candidates
for the default language when no explicit values-en/ folder is present.

Fixes https://youtrack.jetbrains.com/issue/CMP-6840
Sync golden API and KLib ABI files after introducing LocaleQualifiers
and syncing upstream changes.
@2KAbhishek
Abhishek Keshri (2KAbhishek) force-pushed the fix/resource-locale-fallback-chain branch from d856db1 to 4b0e568 Compare October 13, 2026 10:12
@2KAbhishek Abhishek Keshri (2KAbhishek) changed the title [resources] Support prioritized locale fallback chain for resources (CMP-6840) [Resources] Support prioritized locale fallback chain for resources (CMP-6840) Oct 13, 2026
@2KAbhishek

Copy link
Copy Markdown
Contributor Author

Hi Konstantin (@terrakok)

I have raised a fix for https://youtrack.jetbrains.com/issue/CMP-6840 here and was hoping to get your feedback, can you please take a look when you have a chance 🙏🏽

If you think I should reach out to someone else please let me know, Thank you

@terrakok

Copy link
Copy Markdown
Member

It is already WIP: #5759

@terrakok

Copy link
Copy Markdown
Member

(and next time, please, don't send as AI slop PRs)

@2KAbhishek
Abhishek Keshri (2KAbhishek) deleted the fix/resource-locale-fallback-chain branch October 15, 2026 12:35
@2KAbhishek
Abhishek Keshri (2KAbhishek) restored the fix/resource-locale-fallback-chain branch October 15, 2026 12:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants