Use CLDR locale matching to pick the device's translation - #156
Open
spencerrlongg wants to merge 1 commit into
Open
Use CLDR locale matching to pick the device's translation#156spencerrlongg wants to merge 1 commit into
spencerrlongg wants to merge 1 commit into
Conversation
The previous prefix matching returned the first resource key sharing a base language, which is wrong when we ship several variants of one language. Five of our base languages have multiple variants (en, es, zh, pt, de), so this was structural rather than exotic -- most seriously, Traditional Chinese devices (zh-Hant, zh-MO) matched zh-CN and were served Simplified Chinese, a different character set. Replace it with @formatjs/intl-localematcher, a ponyfill of ECMA-402 best-fit matching. Verified against our real 75-locale set: zh-Hant -> zh-TW, zh-MO -> zh-HK, en-AU -> en-GB, pt-AO -> pt-PT, all previously resolving to the wrong variant. Two edge cases the library forced handling for: - match() throws RangeError on structurally invalid tags. This runs at module load, so an unguarded throw would break app startup over a bad device locale; catch and fall back to en-US. - match() canonicalizes casing, so Crowdin's informal-German "de-if" comes back as "de-IF" and misses our lowercase resource key. Map the result back to the real key case-insensitively. Chose this over a local Expo module wrapping Bundle.preferredLocalizations (from:) / LocaleListCompat.getFirstMatch(): same algorithm family and the same verified fixes, but identical on both platforms, no native code to maintain, and it still ships as an EAS Update rather than forcing new store builds via a changed native fingerprint.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Replaces the prefix-based device-language matching from #155 with
@formatjs/intl-localematcher(ECMA-402 best-fit ponyfill).The old logic returned the first resource key sharing a base language. That's wrong whenever we ship multiple variants of one language — and five of our base languages have multiple variants (
en,es,zh,pt,de), so it was structural, not exotic. The worst case was a real bug: Traditional Chinese devices (zh-Hant,zh-MO) matchedzh-CNand got served Simplified Chinese, a different character set.Why this over a native module
Bundle.preferredLocalizations(from:)is Apple's blessed answer per TN2418, and it is usable here — the class method takes an arbitrary array, no.lprojneeded — via a local Expo module paired withLocaleListCompat.getFirstMatch()on Android. Went with the JS ponyfill instead: same algorithm family, same verified fixes, but identical behavior across platforms instead of trusting two OS implementations to agree, no native code in two languages, and it still ships as an EAS Update rather than changing the native fingerprint and forcing new store builds.Two edge cases the library forced handling for
match()throwsRangeErroron structurally invalid tags. This runs at module load, so an unguarded throw would take down app startup over a bad device locale — caught, falls back toen-US.match()canonicalizes casing, so Crowdin's informal-Germande-ifcomes back asde-IFand misses our lowercase resource key. Result is mapped back to the real key case-insensitively.Test plan
Verified by extracting the actual
resolveDeviceLanguagesource fromi18n/index.jsand running it against the real installed package and the real 75-key resource map:zh-Hantzh-CN(Simplified)zh-TWzh-MOzh-CN(Simplified)zh-HKen-AUen-USen-GBpt-AOpt-BRpt-PTde-USde-DEde-DEzh-Hanszh-CNzh-CNde-ifde-ifde-if(case mapping)de_DE/C/xx/[]en-US, no throwresources(no silent fallback-to-English from a key miss)node --checkpasses🤖 Generated with Claude Code