Internationalisation
English and Chinese are peers here. Neither is a translation of the other, and CI enforces it.
The guard
src/i18n/i18n.test.ts fails the build when:
- A key exists in one locale and not the other.
- Any value is empty.
- A
zh-CNvalue is identical to its English counterpart. - A
zh-CNvalue contains Latin letters and no Han characters.
Rule 4 exists because rule 3 is not enough. profile.technical once held the English word "technical" against English "Technical contact" — different, and still plainly untranslated.
Brands (Ant Design, React), acronyms (CPU, API) and filenames (theme.ts, hero-dark.png) are explicitly allowed. Everything else must contain Han characters.
Typography is not shared
Latin and 汉字 do not share a metric. Han glyphs are dense and full-width with no ascender/descender rhythm, so Latin line-heights crush them and Latin tracking pulls them apart. tokens.css keeps two parallel scales:
:root {
--text-base: 14px;
--leading-normal: 1.55;
--tracking-tight: -0.011em;
}
:root:lang(zh), :root:lang(ja), :root:lang(ko) {
--text-base: 15px; /* Han reads smaller at the same px size */
--leading-normal: 1.75;
--tracking-tight: 0; /* negative tracking is actively wrong here */
--weight-bold: 600; /* avoid synthesised fake-bold */
}The lang attribute on <html> is doing real work — it is the switch. buildTheme() mirrors the same split into antd's ConfigProvider.
Loading
Bundles are fetched on demand, so a reader in Shanghai does not download English copy they will never see:
const LOADERS = {
'en-US': () => import('./locales/en-US/common.json'),
'zh-CN': () => import('./locales/zh-CN/common.json'),
};Use changeLocale(), not i18n.changeLanguage() — the former loads the bundle before switching. Calling changeLanguage directly swaps the active language ahead of its strings and paints raw keys for a frame.
Adding a locale
- Add the folder under
src/i18n/locales/. - Add it to
SUPPORTED_LOCALESandLOADERS. - If it is CJK, add it to
CJK_LOCALESand to the:lang()selector intokens.css. Missing the second step is why a new Japanese locale looks subtly wrong. - Map it to an antd locale in
ThemeProvider.