refactor(i18n): fold the supplementary table into the dictionary literal - #654
Merged
Conversation
The literal had drifted into three styles at once: 165 tab-indented lines among 6,600 space-indented ones, 26 values in double quotes where every other value uses single, and 25 objects with a trailing comma on their last property. Mechanical and semantically inert: the flattened dictionary (6,210 keys) is byte-identical before and after. Separated from the fold that follows so that diff contains nothing but the change in meaning. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`translations` was declared as a 6,700-line literal and then quietly
rewritten at module load: a second table, `interactiveLabelTranslations`,
was merged in by a top-level loop with `Object.assign`. The object the
app read was therefore never the object the source declares — 259 keys
existed only after the merge, and 20 locales got their whole `toc`
section from the loop's else-branch.
Four keys were declared twice, with different values, where whichever
table ran last silently won. The fold preserves every winner and drops
the dead row:
zh-TW settings.toolbarOnBar 列 → 工具列
ko settings.toolbarPlacement 툴바 위치 → 도구 모음 위치
ko settings.toolbarOnBar 표시줄 → 막대
ko settings.resetToolbar 툴바 초기화 → 도구 모음 초기화
Those are the real find. Two rows of source claiming one key, across 26
locales, is not something review catches — and a reader who greps for
the losing value finds it and believes it.
Proof the fold moved nothing: the fully-merged dictionary, flattened to
6,210 dotted keys and sorted, is byte-identical before and after
(sha256 cd46b9b1…). The literal was reprinted by a generator first
verified to reproduce the pre-fold literal exactly, so only the merge's
own effects appear in the diff.
The guardrail bans the mutation from coming back. It pins `Object.assign(`
unqualified rather than a pattern naming `translations`, because the loop
reached the dictionary through a local alias that such a pattern cannot
see; `src` contains no other use.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.
i18n.tsmerged a supplementary table into the exportedtranslationswithObject.assignat module load, so the source literal and the exported value were different objects.One
Object.assign, not several: a nested top-level loop merginginteractiveLabelTranslations(26 locales, sectionssettings/toc/common). Added wholesale by #196 — appending a table rather than editing 26 places in a 6,700-line literal. No locale-override machinery, no merge-conflict residue, nothing load-bearing. Folded.What the merge was actually doing: 259 keys added, 23 restated identically (dead rows), 20 locales got their entire
tocsection from the loop's else-branch, and 4 keys overwritten with a different value.The four keys that depended on the overwrite
settings.toolbarOnBarsettings.toolbarPlacementsettings.toolbarOnBarsettings.resetToolbarAll four winners preserved. The three losing Korean values no longer appear in the source at all — which is the point: two rows claimed the same key and only one could ever win, and now only one is written down.
The proof is the deliverable, not the fold
The fold is mechanical; showing nothing moved is not. A snapshot of the fully-merged dictionary, flattened to 6,210 sorted dotted keys, taken before and after: byte-identical, sha256
cd46b9b1…. Independently re-taken on the reviewer's side: same 6,210 keys, same digest.The literal is regenerated by a printer that was first proved to reproduce the pre-fold literal byte-for-byte, so only the merge's own effects appear in the semantic diff.
That forced two commits. The literal had drifted into three styles at once — 165 tab-indented lines, 26 double-quoted values, 25 trailing commas — which the printer would have silently normalised inside the interesting diff. Commit 1 is that normalisation alone, snapshot verified unchanged; commit 2 is the fold.
The two tests were not deleted, and the premise for deleting them was wrong
I asked for two
i18nCoveragetests to be removed as workarounds for the merge. They are not workarounds.dictionariesini18nCoverage.test.tsis built from the imported runtimetranslations— already post-merge. It is not static analysis of the literal, so no test ever had to reach past the merge; there was no barrier to work around.menu.*/toast.*/home.*, sections the supplementary table never carried. Measured against the pre-merge dictionary: 0 of its 11 keys missing. No relationship to the merge at all.Both still pass unchanged.
Guardrail
A RULES row, the translation dictionary is one literal,
allowed: [].The marker pins unqualified
Object.assign(rather than a pattern namingtranslations, because the loop reached the dictionary through a local alias (const currentSection = translations[language][section]) that atranslations-anchored regex cannot see — which is precisely how the mutation would come back.srccontains no otherObject.assign, so the blanket ban costs nothing. A second alternative catches the directtranslations[l][s] = …spelling of the else-branch.Numbers
npm test903 → 904 ·vitest357 ·check0 errors.The node count rises by one — the new rule — rather than dropping by the two tests I expected to delete.
i18n.ts7,326 → 7,114 lines, shorter despite the literal growing 299 lines (259 keys plus 40 lines of newtocbraces): removing the 510-line table, its two now-unused types and the loop more than paid for it. Duplicate-key scan across the folded literal: 0.🤖 Generated with Claude Code