Internationalizing a Legacy Codebase Isn't a Translation Job — It's an Architecture Overhaul
Legacy frontends in any language accumulate the same problem: display strings that also control logic. A naive i18n pass breaks features without errors, and the breakage only surfaces in production. The separation pattern shown here — stable identifiers for logic, i18n keys for display, and a phased migration that doesn't freeze the codebase — applies to any large, actively developed system being retrofitted for multiple languages.
A scan of a long-running project revealed roughly 50,000 Chinese strings across pages, shared enums, stores, and export templates. The immediate reflex — global search-and-replace with translation functions — would have broken business logic everywhere, because many strings double as API parameters, cache keys, or conditional checks. The core rule established was that translation must only affect display; stable `code` or `key` fields must drive all logic, with a separate `nameI18nKey` field for UI text. The refactoring was phased: first build the i18n runtime and loader with a full pilot module, then migrate business modules incrementally, move language packs to a CDN only after key structures stabilize, and finally build a management backend for canary releases and rollbacks. Shared components were made `nameI18nKey`-aware to avoid repetitive boilerplate across hundreds of pages. Stateful text in stores, caches, and dynamic menus was redesigned to store identifiers rather than translated strings, and a page-refresh strategy was chosen over real-time hot-switching to avoid destabilizing legacy state. A layered fallback chain — IndexedDB cache, CDN, built-in core pack, default language, and finally the Chinese default text — ensures resilience. Scanning scripts, ESLint rules, and CI were layered to progressively block new hardcoded strings without immediately failing on all 50,000 historical instances.
The hardest part of i18n in a legacy system is not missing translations but logic that silently breaks because it was coupled to display strings. A missed translation is visible; a broken conditional is invisible until a user hits it.
Treating i18n as an architectural refactoring rather than a translation task shifts the focus from 'how many strings remain' to 'which strings are actually data, and which are just text.' The latter can be translated; the former must be untangled first.
Phased adoption — local JSON first, CDN later, management backend last — prevents the tooling from becoming a moving target while the key structure is still evolving. Reversing this order guarantees rework.
The decision to use a full-page refresh on language switch, rather than reactive hot-switching, is a pragmatic trade-off: it accepts a minor UX cost to avoid destabilizing deeply cached state across routes, stores, and component libraries.
Progressive enforcement in CI — report, then warn on new code, then block — is the only way to govern a 50,000-string legacy surface without developers disabling the checks entirely.
The discussion is sparse. One remark praises the article's rigor, while another draws a blunt equivalence between legacy codebases and unmanageable messes.
Old project = mountain of shit? [look]
[facepalm]