feat(ios): resolve consumer locales against shipped translation bundles#492
Draft
jkmassel wants to merge 2 commits intojkmassel/locale-resolver-androidfrom
Draft
feat(ios): resolve consumer locales against shipped translation bundles#492jkmassel wants to merge 2 commits intojkmassel/locale-resolver-androidfrom
jkmassel wants to merge 2 commits intojkmassel/locale-resolver-androidfrom
Conversation
5 tasks
73db09a to
80c4ac3
Compare
Counterpart to the Android `LocaleResolver` that landed in the parent
commit on this branch. Moves locale resolution into the library so
WP-iOS stops handing the editor an opaque tag and getting silently
dropped into the English bundle whenever the device locale doesn't
match a shipped `translations/<tag>.json` filename.
Resolution chain (mirrors Android):
1. Full `language-region` tag
2. Script-implied region — `zh-Hant-HK` → `zh-tw`, `zh-Hans` → `zh-cn`
3. Language-only tag
4. `en`
Inputs are parsed via `Locale(identifier:)` after `_` → `-`
normalisation, so `setLocale(Locale(identifier: "pt_BR"))` lands on
`pt-br`, `Locale(identifier: "zh-Hant-HK")` on `zh-tw`, and
`Locale(identifier: "iw_IL")` on `he`. Foundation already canonicalises
the legacy ISO 639-1 codes (`iw`/`in`/`no`) when parsing identifiers, so
the alias map is defense-in-depth on iOS — kept for symmetry with
Android in case Foundation ever changes.
One iOS-specific behaviour worth knowing: Foundation supplies an
implicit script for bare-language tags (`zh` → `Hans`, `ja` → `Jpan`),
so a bare `zh` lands on `zh-cn` via the script-implied step instead of
falling through to English. Android's `Locale.forLanguageTag` leaves the
script unset, so the same bare tag falls through there. The exhaustive
manifest round-trip test asserts the contract either way.
API change: the previously-public `setLocale(_ String)` is removed in
favour of `setLocale(_ Locale)`. The internal `setLocaleTag(_ String)`
exists so `toBuilder` can round-trip a stored tag without re-running
resolution. Migration is mechanical
(`setLocale("pt_BR")` → `setLocale(Locale(identifier: "pt_BR"))`).
Resources: `GutenbergKitResources.loadSupportedLocales()` reads the
manifest from `Bundle.module` at runtime. The follow-up build-plugin
commit on this branch replaces it with a compile-time constant.
Introduce a `BuildToolPlugin` that reads the JS-emitted
`supported-locales.json` manifest before `GutenbergKit` compiles and
emits an `internal enum SupportedLocales { static let all: Set<String> }`
constant. The resolver consumes the constant directly, dropping the
runtime IO that the previous commit shipped.
This mirrors the Android sibling's `:Gutenberg:generateSupportedLocales`
gradle task: a missing manifest fails the build (SwiftPM reports it as a
missing input), so a release that silently degrades every consumer to
English is unreachable in shipped artifacts. SwiftPM's incremental graph
treats the manifest as an input file, so changes from `make build`
trigger regeneration without rebuilding the whole target.
Layout:
- `ios/Plugins/SupportedLocalesPlugin/` — the plugin itself; computes the
manifest URL via `context.package.directoryURL` and wires up a
buildCommand against the generator tool.
- `ios/Plugins/GenerateSupportedLocales/` — small `executableTarget` the
plugin invokes. Reads the manifest, validates it's a JSON array of
strings, writes the Swift source. Friendly error messages for
missing/malformed input as a backstop in case SwiftPM's prebuild check
is bypassed.
`GutenbergKitResources.loadSupportedLocales()` is removed since it now
has no callers.
38a4063 to
c59cf00
Compare
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
setLocaleresolution work in Resolve consumer locales against shipped translation bundles #490: a Vite plugin that emitsdist/supported-locales.jsonand a JS-side resolver that runs the chain full-tag → language-only →en.LocaleResolverand a newEditorConfigurationBuilder.setLocale(_ locale: Locale)method that resolves against the shipped manifest before storing the tag for serialization.setLocale(_ String)overload. It is replaced by aninternalsetLocaleTag(_ String)reserved fortoBuilderround-trip and tests; external consumers must go through theLocaleAPI.trunk); the shared JS pieces appear in both and the second to land is a no-op for those files.Refs #490.
Root Cause
EditorConfiguration.localeis an opaque string. iOS consumers (WP-iOS) hand whatever the platform returns (Locale.identifier,Locale.preferredLanguages.first, etc.) intosetLocale(String), andsrc/utils/localization.jsdoes a single-level dynamicimportthat silently falls back to English on any miss. The supported list lived only inbin/prep-translations.js, so consumers had to mirror that table to do the right thing — and historically hadn't.Changes
Build
vite.config.js: New
emitSupportedLocalesManifestplugin scanssrc/translations/at build time and emitsdist/supported-locales.json(sorted array of locale tags). The existingmake copy-dist-iostarget ships it into the resource bundle.Resolution chain
For an input
xx-yy, normalised to lowercase with_→-:xx-yy) — match if shippedxx) — match if shippedenImplemented in:
resolveLocale(input, supported?). Default supported set comes fromimport.meta.glob('../translations/*.json'), so JS is statically analysable and gets the same single source of truth without a separate fetch. The catch-and-warn aroundsetLocaleDatastays as defense-in-depth.LocaleResolverstruct, fed byGutenbergKitResources.loadSupportedLocales()reading the manifest fromBundle.module.API change
The previously-public
setLocale(_ String)is removed. Consumers must pass aLocalevalue; the resolver decides the wire-format string. The internalsetLocaleTagexists sotoBuildercan round-trip a stored tag without re-running resolution.Tests
enfallback, normalisation ofpt_BR/EN_GB/ etc.).GutenbergKitResources.loadSupportedLocales()(Swift) andimport.meta.glob(JS), both reading the same source of truth as the production resolver.What We Explored
pt-br-UCkBcRdR.js) and prefixes can collide (nl-be-...vsnl-...), so a parser would have to re-encode the SUPPORTED_LOCALES list anyway. A manifest is simpler and unambiguous.Behaviour change
setLocale(Locale(identifier: "pt_BR"))pt-br✅setLocale(Locale(identifier: "fr_CA"))fr✅ (regional bundle absent → language fallback)setLocale("pt_BR")(was opaque)pt_BR(no match → English)pt_BRfrom nativept-br✅ (JS-side resolver)Removing the string overload is a breaking change for any caller that was passing a raw string. The migration is mechanical (
setLocale("pt_BR")→setLocale(Locale(identifier: "pt_BR"))).Test plan
npx vitest run src/utils/localization.test.js— 54/54 pass (4 curated + 1 sanity + 49 per-locale)swift test --filter "EditorConfigurationBuilderTests|LocaleResolverTests"— 38 + 49-case parameterised, all passnpm run lint:json the changed files — cleanLocale(identifier: "pt_BR")/Locale(identifier: "fr_CA"), confirm the editor UI renders in Brazilian Portuguese / French (not English).Out of scope
SUPPORTED_LOCALESlist itself.EditorConfiguration.locale— still an opaque string on the JS side.Related
trunk): see sibling PR.