Skip to content

feat: resolve consumer locales against shipped translation bundles#491

Closed
jkmassel wants to merge 1 commit intotrunkfrom
jkmassel/issue-490
Closed

feat: resolve consumer locales against shipped translation bundles#491
jkmassel wants to merge 1 commit intotrunkfrom
jkmassel/issue-490

Conversation

@jkmassel
Copy link
Copy Markdown
Contributor

@jkmassel jkmassel commented May 4, 2026

Summary

  • Adds a single resolver — full normalised tag → language-only tag → en — implemented identically on JS, iOS, and Android, fed by a build-emitted supported-locales.json manifest so the "what do we ship?" answer has exactly one source of truth.
  • Adds setLocale(_ locale: Locale) (iOS) and setLocale(context: Context, locale: Locale) (Android) overloads on EditorConfiguration builders. The wire format stays a string; only the consumer-facing API gains type safety.
  • JS-side localization.js now applies the same resolution chain instead of hoping the consumer's tag matches a shipped filename exactly.

Fixes #490.

Root Cause

EditorConfiguration.locale is an opaque string. Consumers (WP-iOS, WP-Android, the upcoming WP-Android preloader in wordpress-mobile/WordPress-Android#22579) historically passed whatever the platform handed back — often Locale.getLanguage() on Android, ISO 639-1 only — and src/utils/localization.js did a single-level dynamic import that silently fell back to English on any miss. The supported list lived only in bin/prep-translations.js, so each client had to mirror that table to do the right thing — and historically hadn't.

Changes

Manifest

vite.config.js: New emitSupportedLocalesManifest plugin scans src/translations/ at build time and emits dist/supported-locales.json (sorted array of locale tags). The existing copy-dist-{ios,android} Make targets ship it into both bundles.

Resolution chain

For an input xx-yy, normalised to lowercase with _-:

  1. Full tag (xx-yy) — match if shipped ✅
  2. Language-only tag (xx) — match if shipped ✅
  3. Fall back to en

Implemented in:

  • src/utils/localization.jsresolveLocale(input, supported?). Default supported set comes from import.meta.glob('../translations/*.json'), so JS is statically analysable and gets the same single source of truth without a separate fetch.
  • ios/Sources/GutenbergKit/Sources/Model/LocaleResolver.swiftLocaleResolver struct, fed by GutenbergKitResources.loadSupportedLocales() reading the manifest from Bundle.module.
  • android/Gutenberg/src/main/java/org/wordpress/gutenberg/model/LocaleResolver.ktLocaleResolver class with a fromAssets(context) factory that reads the manifest from the library's assets/.

New consumer APIs

// iOS — runs the resolution chain
public func setLocale(_ locale: Locale) -> EditorConfigurationBuilder
// Android — Context required to read the manifest from assets
fun setLocale(context: Context, locale: Locale)

The existing setLocale(String) overloads stay untouched as a power-user / test escape hatch.

What We Explored

  • Hard-coding the supported set in native code — explicitly called out as the footgun in the issue. Rejected.
  • Reading the locale list from filenames in assets/ — Vite hashes the chunk filenames (pt-br-UCkBcRdR.js), and the prefixes can collide (nl-be-... vs nl-...), so a parser would have to re-encode the SUPPORTED_LOCALES list anyway. A manifest is simpler and unambiguous.
  • androidx.startup / hidden static Context — would let setLocale(Locale) skip the context: parameter on Android. Rejected for now: adds a dependency and lifecycle surprise for one parameter; consumers calling this from an editor activity already have a Context in hand.

Behaviour change for the cases the issue called out

Input Before After
pt_BR device locale pt → matches → ✅ Brazilian pt-br → matches → ✅ Brazilian
pt_BR via Locale.getLanguage() pt → matches → ✅ Brazilian (consumer can pass full Locale) pt-br → matches → ✅ Brazilian
nl_BE nl → matches → 🇳🇱 Dutch nl-be → matches → 🇧🇪 Belgian Dutch
fr_CA (no regional bundle) fr-CA → ❌ English fr → ✅ French
Locale("zh","CN") zh-CN → ❌ English zh-cn → ✅ Simplified Chinese
zh (language-only, no zh bundle) ❌ English ❌ English (unchanged — language-only zh isn't shipped)

Test plan

  • npx vitest run src/utils/localization.test.js — 4/4 pass
  • swift test --filter "EditorConfigurationBuilderTests|LocaleResolverTests" — 37/37 pass
  • ./android/gradlew :Gutenberg:testDebugUnitTest --tests "org.wordpress.gutenberg.model.LocaleResolverTest" — 5/5 pass
  • ./android/gradlew :Gutenberg:testDebugUnitTest --tests "org.wordpress.gutenberg.model.EditorConfigurationBuilderTest" — existing suite still green
  • npm run lint:js -- src/utils/localization.js src/utils/localization.test.js vite.config.js — clean
  • ./android/gradlew detekt — clean
  • Manual smoke: build the demo apps, point them at Locale("pt","BR") / Locale("nl","BE") / Locale("fr","CA"), confirm the editor UI renders in the expected language.

Out of scope

  • Changing the SUPPORTED_LOCALES list itself.
  • Changing the wire format of EditorConfiguration.locale — still an opaque string on the JS side.
  • Pluralization / RTL handling — separate concerns.

Related

Move locale resolution into the library so consumers stop having to
mirror the `SUPPORTED_LOCALES` table to avoid silently falling back to
English. The resolver runs full-tag → language-only → `en` against a
manifest emitted by the JS build, and is exposed on each platform's
configuration builder via a new `Locale` overload.

Fixes #490
@github-actions github-actions Bot added the [Type] Enhancement A suggestion for improvement. label May 4, 2026
@jkmassel
Copy link
Copy Markdown
Contributor Author

jkmassel commented May 5, 2026

Splitting into separate iOS and Android PRs for independent review/landing. Replacement PRs incoming — iOS carries the foundational build manifest + JS resolver, Android stacks on top.

@jkmassel jkmassel closed this May 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Resolve consumer locales against shipped translation bundles

1 participant