test(i18n): pin every shipped string before moving any of them - #590
Merged
Conversation
Slice 1 of the locale de-duplication: nets only, no behaviour change. Both fixtures are generated from today's files, so this PR is green by construction — which is the point. It is what makes everything after it reversible. The two locale files hold 547 of the same keys between them, 523 identical and 24 already quietly diverged, and nothing checks that. Collapsing them onto one source means relocating 72 KB of JSON, and that diff is unreviewable. Worse, the hazards in it are invisible to a skim: `palette.*` versus `library.palette.*`, `library.sort.*` versus `library.sortRecent`, `tts.listen` versus `home.features.tts`. So the fixture becomes the review surface — a pure move leaves it untouched, and every line that does change is a shipped string somebody has to defend. A committed fixture with an explicit compare, NOT `toMatchFileSnapshot`: a snapshot is regenerated with one `vitest -u`, and the entire value here is that changing a production string costs a hand edit. The mobile half is the more important one. `packages/shared/src/i18n/en.json` is about to be edited for the WEB app's benefit, and `mobile-ota.yml` triggers on `paths: packages/**` — i18n is plain JS and does not move the Expo fingerprint, so a merge republishes the bundle to installed Android phones with no store review and nobody pressing anything. This fixture is what stands between "we tidied the shared file" and "an OTA changed the copy on someone's phone". It lives in `apps/mobile/src/lib/` because `vitest.config.ts` there sets `include: ['src/lib/**/*.test.ts']` — a test anywhere else in that app silently does not run. Both directions are asserted: a value that changed, a key that vanished, and a key that appeared without the fixture being updated. Failures name the key rather than counting them. Mutation-verified rather than assumed: changing `common.loading` in each file fails the matching test with `expected [ 'common.loading' ] to deeply equal []`, and reverting restores green. 780 web + 385 mobile tests green; both typechecks clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E
mrviduus
force-pushed
the
chore/locale-golden-nets
branch
from
September 9, 2026 03:16
29ac69b to
e771994
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.
Slice 1 of five. Nets only — no behaviour change. Both fixtures are generated from today's files, so this is green by construction. That is the point: it makes everything after it reversible.
Why a fixture and not a diff
The two locale files hold 547 of the same keys between them — 523 identical, 24 already quietly diverged — and nothing checks that. Collapsing them onto one source means relocating 72 KB of JSON, and that diff is unreviewable.
Worse, the hazards inside it are invisible to a skim:
palette.*vslibrary.palette.*library.sort.*vslibrary.sortRecenttts.listenvshome.features.ttsSo the fixture becomes the review surface. A pure move leaves it untouched; every line that does change is a shipped string somebody has to defend.
Committed fixture with an explicit compare, deliberately not
toMatchFileSnapshot— a snapshot is regenerated with onevitest -u, and the entire value here is that changing a production string costs a hand edit.The mobile half is the more important one
packages/shared/src/i18n/en.jsonis about to be edited for the web app's benefit. Andmobile-ota.ymltriggers onpaths: packages/**; i18n is plain JS and does not move the Expo fingerprint, so a merge to main republishes the JS bundle to installed Android phones — no store review, nobody pressing anything.This fixture is what stands between "we tidied the shared file" and "an OTA changed the copy on someone's phone".
It lives in
apps/mobile/src/lib/becauseapps/mobile/vitest.config.tssetsinclude: ['src/lib/**/*.test.ts']— a test anywhere else in that app silently does not run.What is asserted
All three directions, with failures naming the key rather than counting them:
expected [ 'common.loading' ] to deeply equal []Verification
Mutation-verified, not assumed. Changing
common.loadingin each file fails the matching test and names it; reverting restores green.780 web + 385 mobile tests green; both typechecks clean. Nothing but the four new files is touched.
🤖 Generated with Claude Code
https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E