refactor(i18n): delete 100 orphaned strings, and a detector so they cannot return - #595
Merged
Conversation
…annot return
Slice 5 of five. Nothing had ever checked whether a key was still used, and 100 of
656 were not — a whole `review` namespace, most of `vocabulary`, `onboarding` copy
for a flow that has since changed. They cost bundle size on a phone and they read
as intent: the next person opening the file could not tell which strings the apps
actually show.
**I got this wrong first and the tests caught it.** The detector originally looked
at the mobile app alone and reported 278 orphans. Deleting those removed 244 keys
the WEBSITE renders, and `missing-keys.test.ts` failed instantly with
`Footer.tsx → footer.privacy`. The reason is slice 3: web no longer keeps its own
copy of a shared string, so a key used only by the website now lives only in
shared. "Unused by mobile" and "unused" stopped being the same question the moment
the duplicates went away. Rewritten to check both consumers; 278 became 100.
That is also why the test lives in `packages/shared/src/i18n/unusedKeys.test.ts`
and not in either app: the catalogue has two consumers and a test that can only see
one of them is the bug I just made.
The detector is deliberately narrow, because a wrong answer here deletes a working
screen. An earlier general version called `privacy.*` dead — 49 keys behind a
Google Play store-listing URL — because they are reached through a shared list of
key names rather than a literal `t('…')`. A key counts as referenced if the exact
dotted path appears quoted anywhere in either app, or matches one of the dynamic
templates; DYNAMIC is the complete set of `` t(`…${…}`) `` shapes in both apps,
collected by grepping for them rather than guessed. Adding a template to an app
without listing it here makes this test call live keys dead — loudly, which is the
point.
The allowlist is asserted by set EQUALITY and is empty today. Equality also fails
when an allowlisted key starts being used, so the list cannot quietly rot.
Mutation-verified: planting `common.neverUsedAnywhere` fails with
`expected [ 'common.neverUsedAnywhere' ] to deeply equal []`.
Both golden fixtures: 100 keys removed, **zero values changed**. Browser-checked
home, /en/books, /en/vocabulary, /en/stats and /en/privacy — real headings, no key
literals leaking through.
786 web + 451 shared + 385 mobile tests green.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E
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 5 of five. Nothing had ever checked whether a key was still used, and 100 of 656 were not — a whole
reviewnamespace, most ofvocabulary,onboardingcopy for a flow that has since changed. They cost bundle size on a phone and they read as intent: the next person opening the file could not tell which strings the apps actually show.I got this wrong first, and the tests caught it
The detector originally looked at the mobile app alone and reported 278 orphans. Deleting those removed 244 keys the website renders, and
missing-keys.test.tsfailed instantly withFooter.tsx → footer.privacy.The reason is slice 3: web no longer keeps its own copy of a shared string, so a key used only by the website now lives only in shared. "Unused by mobile" and "unused" stopped being the same question the moment the duplicates went away. Rewritten to check both consumers, 278 became 100.
That is also why the test lives in
packages/shared/src/i18n/unusedKeys.test.tsand not in either app: the catalogue has two consumers, and a test that can only see one of them is the bug I just made.The detector is deliberately narrow
A wrong answer here deletes a working screen. An earlier general version called
privacy.*dead — 49 keys behind a Google Play store-listing URL — because they are reached through a shared list of key names rather than a literalt('…').A key counts as referenced if the exact dotted path appears quoted anywhere in either app, or it matches one of the dynamic templates.
DYNAMICis the complete set oft(`…${…}`)shapes in both apps, collected by grepping for them rather than guessed. Adding a template to an app without listing it here makes this test call live keys dead — loudly, which is the point.The allowlist is asserted by set equality and is empty today. Equality also fails when an allowlisted key starts being used, so the list cannot quietly rot.
Verification
Mutation-verified: planting
common.neverUsedAnywherefails withexpected [ 'common.neverUsedAnywhere' ] to deeply equal [].Both golden fixtures: 100 keys removed, zero values changed. Browser-checked home,
/en/books,/en/vocabulary,/en/statsand/en/privacy— real headings, no key literals leaking through.Where the five slices land
apps/web/src/locales/en.jsonpackages/shared/src/i18n/en.json🤖 Generated with Claude Code
https://claude.ai/code/session_011rgEMvYYi4Egj99dVtvm6E