跪拜 Guibai
← All articles
Frontend · Architecture · JavaScript

Internationalizing a Legacy Codebase Isn't a Translation Job — It's an Architecture Overhaul

By 天天鸭 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

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.

Summary

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.

Takeaways
Display text that participates in `===`, `includes`, or `switch` statements will silently break after translation; these must be refactored to use stable `code` or `key` fields first.
Shared enums often have their `name` field used as API parameters, cache keys, or Map keys; adding a separate `nameI18nKey` field preserves existing logic while enabling translation.
Shared components that resolve `nameI18nKey` internally eliminate repetitive `t()` boilerplate across hundreds of consuming pages.
Stateful text in stores, caches, tabs, and task queues should store identifiers and parameters, not pre-translated strings, so language switches don't leave stale text.
Dynamic menus from the backend need a stable `nameI18nKey` field; mapping by Chinese menu name breaks on rename and can't distinguish duplicates.
Concatenated sentences must be moved entirely into language packs with interpolation parameters, because word order differs across languages.
Export language and UI language are separate concerns; print templates need a dedicated `printLabel` field distinct from the UI `nameI18nKey`.
A page-refresh strategy for language switching is more stable for legacy projects than attempting real-time hot-switching across all cached state.
Language pack loading should move to CDN only after namespace boundaries and key naming stabilize; swapping the loader later avoids re-touching business pages.
A fallback chain — IndexedDB → CDN → built-in core pack → default language → Chinese default text → raw key — prevents users from ever seeing raw i18n keys.
CI should enforce i18n checks progressively: report-only first, then block new violations in changed lines, and finally block outright once core modules stabilize.
Conclusions

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.

Concepts & terms
i18n key vs. stable identifier
An i18n key (e.g., `orders.status.pending`) maps to translated display text. A stable identifier (e.g., `code: 'pending_review'`) is used for all business logic, API calls, and comparisons. They must be separate fields because translation changes the display value but must never change the data the system acts on.
Namespace-based language pack splitting
Language packs are split by business module (e.g., `admin_orders.json`) rather than by individual page or one giant file. Each route declares which namespaces it needs, and a unified loader fetches and caches them, deduplicating concurrent requests.
Fallback chain for i18n resilience
A sequence of fallback sources when loading translations: IndexedDB cache → CDN → last successful old version → built-in core pack → default language → Chinese default text from the `t()` call → raw key. This ensures users never see a raw key, and missing translations are reported for later fixing.
From the discussion

The discussion is sparse. One remark praises the article's rigor, while another draws a blunt equivalence between legacy codebases and unmanageable messes.

Legacy codebases are often perceived as intractable piles of technical debt.
Featured comments
小小程序元

Old project = mountain of shit? [look]

天天鸭

[facepalm]

See top comments, translated →
Source: juejin.cn ↗ Google Translate ↗ Backup ↗