Collapsing the three-file translation triplet into one module
Maintaining UI translations across three separate files created silent sync friction. Moving to a single per-key module enforced compile-time translation parity.
When Norwegian language support returned during norwegian-done-properly, UI copy was split across three separate files: a TypeScript interface defining 396 string keys, an English dictionary file, and a Norwegian dictionary file. Adding or updating a UI string meant editing three places in parallel. While TypeScript guaranteed that every key declared in the interface existed in both dictionaries, nothing checked that the Norwegian translation actually differed from the English source copy.
Co-locating translations per key#
To eliminate three-file edit friction, all 396 string definitions were collapsed into a single strings.ts module. Each entry pairs English and Norwegian copy directly inside a per-key object literal, placing both language variants side by side.
export const strings = defineStrings({
backspace: {
en: 'Backspace',
no: 'Slett'
},
submitWord: {
en: 'Submit word',
no: 'Send inn ord'
}
});
Co-locating translations ensures that any engineer or automated agent adding a UI string sees both language requirements in the same line of code.
Enforcing translation completeness at compile time#
Structural typing alone can let a missing or mistranslated string pass if the property types overlap. To turn translation parity into a hard compile constraint, defineStrings leverages TypeScript’s NoInfer<T[K]> utility on the Norwegian translation slot.
Using NoInfer on translation inputs forces TypeScript to evaluate Norwegian copy against English parameters directly at compile time.
If a function-valued UI string accepts parameters in English, NoInfer prevents TypeScript from inferring loose parameter types in Norwegian, making any parameter mismatch a build error before unit tests even run.
Guarding against copy-paste regression with tests#
Alongside compile-time type checks, a new unit test in ui.test.ts scans all 396 dictionary entries for byte-identical translations. While certain technical terms (such as PvP or iOS) legitimately share spelling across languages, un-translated strings now trigger test failures unless explicitly registered on a documented allowlist.
it('flags un-translated strings outside documented allowlist', () => {
for (const [key, { en, no }] of Object.entries(rawStrings)) {
if (en === no && !ALLOWED_IDENTICAL_STRINGS.has(key)) {
throw new Error(`Un-translated string key: ${key}`);
}
}
});
Migrating the 396 keys was executed using an automated AST transformation. An automated code-review pass caught that 32 per-key JSDoc comments were omitted in the initial transformation pass, prompting a fix before the refactor landed on main.