Repository navigation
chore(i18n): gate unreferenced catalog keys in the extraction check and retire 1,644 of them - #22056
Conversation
A key can leave the code without leaving the catalog: a feature is reverted, a
setting is rewritten in place, a component is deleted. Nothing fails, so the
entries stay. Every locale carries them, translators spend effort on strings no
screen renders, and coverage numbers understate how much is actually translated.
`verify:localization-extraction` already reports an unreferenced set, but it
cannot be deleted as-is: i18next-cli does not see a key held in a data structure
(`{ key: '…' }`) or passed as a variable, and 18 files do exactly that. So this
requires two independent conditions — extraction misses the key AND the exact
string appears in no source literal under src/ or mobile/.
Two properties matter and both were found the hard way:
- Plural variants are judged by their base key. i18next appends the CLDR
category at runtime, so `…_one` never appears in a source file even though the
base does. Without this the scan eats every plural in the catalog.
- The scan is iterative. Removing a base key can strand the variants that
survived on its account, so it must be re-run until it reports none.
Registered as `verify:localization-orphans` but deliberately left out of the
lint chain: it reports 1,401 keys on current main, so gating now would fail CI.
The prune and the gate come together in a follow-up.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
No actionable comments were generated in the recent review. ℹ️ Recent review info
Walkthrough
Priority: Merge Risk:
|
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Advanced
Run ID: 69ff5c3e-99b6-4ebe-aaef-4fcd95091020
📒 Files selected for processing (2)
config/scripts/find-orphaned-catalog-keys.mjspackage.json
Included review availability: Your plan provides up to 10 included reviews per hour; 8 remain after this review.
There was a problem hiding this comment.
ℹ️ Minor suggestions only — I ran the detector and could not find a correctness problem; two cleanup items inline.
Reviewed changes
- New orphan detector —
config/scripts/find-orphaned-catalog-keys.mjsreports keys that i18next extraction misses and that appear as no quoted literal undersrc/mobile;--checkexits 1 with remediation text. - npm script — adds
verify:localization-orphans, deliberately left out oflint/pr.ymluntil the follow-up prune lands.
I verified the script end to end: it reports 14,459 en.json keys / 12,887 extraction-referenced / 1,401 orphans (matching the PR body), exits 1 under --check, and leaves no working-tree side effects. A repo-wide exact-token scan of the 1,401 orphans found no runtime consumers outside src/mobile; the only non-source hits are localization tooling (see the second inline note). The plural-base rule and the ${NS}.status… template-literal case both behave as described.
ℹ️ Nitpicks
- The two non-obvious invariants (plural base-key rule, literal scan) have no regression test, and the author's stated safety argument (the follow-up deletes the list and keeps suites green) validates the current list, not the logic. Factoring
isReferenced/the literal scan into exported pure functions and unit-testing the plural and data-structure cases would guard future edits; optional given the follow-up plan.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
Both rules decide whether a key is live, and both are easy to break without noticing: the plural rule keeps `…count_one` when only `…count` appears in source, and the literal scan is the sole evidence for keys extraction cannot see — held in a data structure or passed as a variable. Extracting them as pure functions changes no behaviour: the detector still reports 14,459 keys, 12,887 referenced, 1,401 orphans, and --check still exits 1.
|
Took the nitpick — both invariants now have tests.
Behaviour is unchanged — the detector still reports 14,459 keys / 12,887 referenced / 1,401 orphans, and |
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- Exported the two pure helpers —
collectLiteralKeysandisKeyReferencedare lifted out ofcollectLiteralStrings/findOrphanedKeysand exported so tests can drive them directly; the regex, plural rule, and call sites are otherwise unchanged. - Added
find-orphaned-catalog-keys.test.mjs— 21 vitest cases covering the plural-base rule across all six CLDR categories, the literal shape filter and its three quoting forms, cross-call accumulation into a shared set, and the no-base fallback for non-CLDR suffixes. The suite is included byconfig/vitest.config.ts(config/scripts/**/*.test.mjs), and it is failable: droppingenglish.has(base)fromisKeyReferencedturns the English-catalog case red, and removing the base-key fallback turns the plural cases red.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
main gained the anti-slop rule no-shape-in-symbol-names after this branch was cut, and KEY_SHAPED trips it once the branch is merged with main. Renamed to CATALOG_KEY_PATTERN; no behaviour change.
|
Pushed one mechanical commit, |
Brings in the exported helpers, the unit tests and the constant rename the detector gained in its own PR (stablyai#22056), so the script here matches the one this branch is stacked on.
…ifiers CODEOWNERS routes /config/scripts/*localization*.mjs to the localization owner, and find-orphaned-catalog-keys.mjs matched neither that pattern nor *locale*, so a change to it asked nobody for review. The file now carries the name of the npm script that runs it, verify:localization-orphans.
…base key isKeyReferenced kept a plural variant alive whenever en.json still carried its base key, even when no source reached that base key. The base key was reported and the variants were not, so a prune needed a second run to find them. Only a base key that extraction or a source literal references keeps its variants now; the --check message drops the re-run advice and names the override tables, which sit outside the scan roots.
The scan captured only literals of seven characters or more and then applied CATALOG_KEY_PATTERN, which has no such floor. A live key shorter than that, held in a data structure, would have been reported as an orphan. en.json has no key that short today, so the result is unchanged; the capture now leaves the shape decision to the one pattern.
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
- Configuration used: Repository UI
- Review profile: CHILL
- Plan: Advanced
- Run ID:
fa775867-4647-490f-8bdb-5bb6511dd850
📒 Files selected for processing: 3
config/scripts/verify-localization-orphans.mjsconfig/scripts/verify-localization-orphans.test.mjspackage.json
Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 9 remain after this review.
collectSourceFiles swallowed every readdir error, so a directory it could not read contributed no literals and a key named only there would have been reported as safe to delete. Only a missing mobile/ root is skipped now, since a checkout without the mobile app is still a complete desktop scan; any other error stops the scan.
…nd retire 1,644 unreferenced keys verify-localization-extraction.mjs now filters its own unreferenced set by source literals (src/ and mobile/), plural base keys and dynamic prefixes composed inside translate calls, and fails when any key is named nowhere. That replaces the standalone verify-localization-orphans.mjs script and its second extraction pass. The prune is recomputed on current main: 1,644 keys leave all seven locale catalogs, 285 stale entries leave the key-override tables, and en-runtime-required.json is regenerated. Supersedes stablyai#19801. Co-authored-by: Evgenii <413011+smwbev@users.noreply.github.com>
…ynamic prefix mobile/ edits do not trigger the extraction check, so literals there must not decide its verdict; a dotless runtime-built prefix would keep unrelated keys alive. Co-authored-by: Evgenii <413011+smwbev@users.noreply.github.com>
Orca v1.4.244 ships Russian built in (stablyai/orca#34717), so this is the pack's last release. Both READMEs now say so and point to Settings → Appearance → Language → Русский. v1.4.244 is cut from main after #35365, so 908 strings from the twenty-four drafts that waited for it are released, with 142 plural forms: the rest of the source control dropdown, WSL projects moving to their host, Linear and Jira errors, SSH, workspace and host status, the Google cookie cleanup, editor conflicts, and more. 36 strings follow rewritten English. 89 strings and 54 plural forms leave the pack because v1.4.244 removed them, among them the forms that #34693 made unreachable. stablyai/orca#22056 removed the catalog keys no code read, so tools/unread-catalog-keys.json is empty. Plural forms render since v1.4.243 (#12505); the READMEs, the glossary and the tools now say so. Pack 16,476 plus 331 plural forms, 18,958 entries; coverage of the strings Orca reads that a pack may translate is 99.9%.
…te keys Upstream stablyai#22056 gates catalog keys no source references; these two have had no caller since the Git Graph branch list moved to Local/remote sections. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…te keys Upstream stablyai#22056 gates catalog keys no source references; these two have had no caller since the Git Graph branch list moved to Local/remote sections. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Summary
This PR combines #22056 (the detector) and #19801 (the prune and the gate). It supersedes #19801. Thanks to @smwbev for the design, the plural-base rule and the investigation behind both PRs.
The detector is now part of the extraction check.
verify-localization-extraction.mjsalready computes the set of English keys that extraction misses. It now narrows that set to keys that no source names, using its own extraction pass. There is no second i18next run, no separate script and no newpackage.jsonentry. A key stays live when any of these is true:src/ormobile/names it (tests excluded). This covers search-keyword maps, title-copy maps,labelKeytables, the plugin chrome allowlist andtranslateMain;_zero/_one/_two/_few/_many/_other) and its base key is live;t/translate*call builds at runtime, such astranslate(`…AutomationRunsDashboard.status.${key}`).The source walk reuses
collectSourceFilesfromverify-localization-catalog.mjs.The prune was recomputed on current main, not replayed. 1,644 keys come out of all 7 catalogs.
en-runtime-required.jsonwas regenerated: 676 entries, all removals are pruned keys, nothing was added. 285 stale entries come out of the override tables: 260 inlocale-ko-key-overrides.json, 18 inlocale-key-overrides.mjs, 5 inlocale-cross-locale-key-overrides.mjsand 2 inlocale-ja-key-overrides.mjs.The check now blocks. After the prune it reports 0 keys on main, so
verify:localization-extractionfails on any key no source names. I tested this by planting a stale key and a_one/_otherpair: the check reported all three and exited 1. The fallback-drift failure and the existing checks are unchanged.Tests: I removed the 27-case standalone suite. Two cases in
verify-localization-extraction.test.mjscover the real edge cases: plural variants and keys held in data structures, and dynamic prefixes in translate calls (a non-translate template is ignored).Verification of the prune
src/ormobile/, tests included.browser.,jira.), not i18n keys.settings.browser.sshWorkspaceRouting.titleis nowrowTitle, andAiVaultSessionDetails.sessionis nowsessionRole.Testing
pnpm verify:localization-extraction,verify:localization-catalogs,verify:localization-catalog,verify:localization-coverageandverify:localization-runtime-catalogall pass.pnpm test src/renderer/src/i18nplus allconfig/scripts/*locali*and*locale*tests plus every test that loads catalogs: 89 files, 1,161 tests, all passing.pnpm check:changedpassed all 21 checks.pnpm tc:webpasses.Note:
mobile/is in the literal scan but not in the extraction check's change scope. Mobile does not read the desktop catalog today, so a key named only from mobile would be unusual.