10 KiB
Translations
Status: living document. Maintainers refine this as the project's i18n needs evolve. Contributions welcome.
For general contribution flow, see CONTRIBUTING.md. The "Localization maintenance" section there documents what gets translated (UI chrome, core docs, display metadata) and what does not (skills, design systems, prompt bodies). This file covers how to add and maintain a locale.
Why a separate file? i18n contributors usually only need this surface — keeping locale workflow out of the main contribution guide isolates jargon (BCP-47, fallback chains, regional glossaries) from the broader code-workflow audience. CONTRIBUTING.md cross-links here for discovery.
Maintained locales
UI dictionary lives in apps/web/src/i18n/locales/.
README translations live at the repo root. The LOCALES array in
apps/web/src/i18n/types.ts is the
authoritative list for the UI dict; the language switcher in the
READMEs covers any locale that has a translated README, dict or no dict.
| Code | Language | UI dict | README | Status |
|---|---|---|---|---|
en |
English | en.ts (source) |
README.md |
active |
ar |
العربية | ar.ts |
— | active |
de |
Deutsch | de.ts |
README.de.md |
active |
es-ES |
Español (España) | es-ES.ts |
— | active |
fa |
فارسی | fa.ts |
— | active |
hu |
Magyar | hu.ts |
— | active |
ja |
日本語 | ja.ts |
README.ja-JP.md |
active |
ko |
한국어 | ko.ts |
README.ko.md |
active |
pl |
Polski | pl.ts |
— | active |
pt-BR |
Português (Brasil) | pt-BR.ts |
— | active |
ru |
Русский | ru.ts |
README.ru.md |
active |
zh-CN |
简体中文 | zh-CN.ts |
README.zh-CN.md |
active |
zh-TW |
繁體中文 | zh-TW.ts |
README.zh-TW.md |
active |
fr |
Français | fr.ts |
README.fr.md |
active |
uk |
Українська | uk.ts |
README.uk.md |
active |
A locale may ship a UI dict, a README, or both. The two surfaces are independent — adding one without the other is a normal contribution. The English locale is the source of truth; missing UI keys fall back to English at runtime. Locales may also add translated core docs or display-only metadata in
apps/web/src/i18n/content*.ts; keep those companion surfaces aligned with the locale's active scope.
Adding a new locale
-
Pick a BCP-47 code. Use the regional form (
pt-BR,es-ES,zh-TW) when the variant matters; the bare code (fr,ru) when it doesn't.pt-BRand a hypotheticalpt-PTwould coexist as separate locales — the same precedent applies toen-US/en-GBif a contributor wants to maintain both. -
Update
apps/web/src/i18n/types.ts:- extend the
Localeunion - append your code to
LOCALES - add a
LOCALE_LABEL[<code>]entry — use the native name of the language (Deutsch,日本語, notde,ja)
- extend the
-
Create the dictionary at
apps/web/src/i18n/locales/<code>.ts— copy fromen.tsand translate the values. Keys must matchen.tsexactly; missing keys fall back to English. -
Register your dictionary in
apps/web/src/i18n/index.tsx— both the import and the map entry:import { fr } from './locales/fr'; // ... const DICTS: Record<Locale, Dict> = { // ...existing entries fr, }; -
(Optional) Translate the README — copy
README.mdtoREADME.<code>.md. Use OpenCCs2twp.jsonfor zh-CN ↔ zh-TW; use your judgment elsewhere. -
Update the language switcher in every existing README (line ~27 of each
README*.md). Match the order used in the existing switcher line and append your new locale at the end. The set of switcher links is the union of UI-dict locales and README-only locales — it is not identical toLOCALES. The constraint is simply that every switcher across all READMEs must list the same set in the same order, otherwise readers hit dead ends when jumping between languages. -
Run
pnpm typecheckto confirm the union andDICTSmap agree.
Backport policy
When the English README or UI dict gains new sections/keys, contributors are not required to backport. The English fallback covers missing keys at runtime. Locale maintainers (volunteers, often the original author) are encouraged to refresh in a follow-up PR.
Keep refresh PRs focused: one locale per PR, no mixed feature work.
Drift threshold
A locale is considered drifted when either:
- ≥20 untranslated UI keys vs.
en.ts(today this is checked manually with a key-diff; a CI warning is tracked as a follow-up — see Deferred decisions), or - No refresh PR in 6+ months while the English README or dict has changed
These are tripwires for moving a locale to stale status (below); they're not auto-rejection rules.
Stale locales
We don't delete locales. When a locale crosses a drift tripwire above:
-
Add a
⚠️ Stale (last refreshed YYYY-MM)cell to its row in the maintained-locales table. -
Drop a frontmatter comment at the top of the locale's
.tsfile:// ⚠️ Stale: last refreshed 2025-09. See TRANSLATIONS.md. export const fr: Dict = { ... }; -
The locale keeps compiling and rendering — readers still get partially-translated UI, which is better than removing it.
A new contributor can pick it up by submitting a refresh PR; the markers come off when the drift threshold is back under control.
Regional terminology
Translations follow the conventions of the target region's tech writing community. Maintainers trust contributors to make idiomatic choices and will not gate-keep on style.
zh-CN ↔ zh-TW glossary
When converting between Simplified and Traditional Chinese, prefer Taiwan-specific phrasing in zh-TW rather than character-only conversion. This list grew out of PR #194 and is meant as a starting point, not a rulebook.
Core terms
Easy mappings — most appear in OpenCC's s2twp.json and require no
human judgment:
| English | zh-CN | zh-TW |
|---|---|---|
| screen | 屏幕 | 螢幕 |
| stack | 栈 | 堆疊 |
| project | 项目 | 專案 |
| software | 软件 | 軟體 |
| video | 视频 | 影片 |
| file | 文件 | 檔案 |
| document | 文档 | 文件 |
| message | 信息 | 訊息 |
| network | 网络 | 網路 |
| database | 数据库 | 資料庫 |
| user | 用户 | 使用者 |
| default | 默认 | 預設 |
| real-time | 实时 | 即時 |
| install | 安装 | 安裝 |
| settings | 设置 | 設定 |
| menu | 菜单 | 選單 |
| compatible | 兼容 | 相容 |
| bind | 绑定 | 綁定 |
| desktop | 桌面端 | 桌面版 |
| mobile | 移动端 | 行動版 |
Idiomatic / domain-specific
Mappings that needed human judgment in #194 — OpenCC won't catch them and they're the most useful to record because the next translator will hit the same choices:
| English / context | zh-CN | zh-TW |
|---|---|---|
| fallback / safety net | 兜底 | 備援 |
| bundle / package up | 捆绑 | 納入 |
| live, dynamic | 活的 | 動態的 |
| plan (noun) | 计划 | 計畫 |
| color palette | 色板 | 色票 |
| spec doc | 规范文件 | 規格文件 |
| course-correction | 介入纠偏 | 介入修正 |
| crash, screw up (slang) | 翻车 | 出包 |
| go viral (slang) | 出圈 | 爆紅 |
Tooling: OpenCC with s2twp.json
handles roughly the Core terms automatically. The Idiomatic table is
where the human review pays off — start there when adapting an existing
zh-CN translation.
Other CJK / RTL glossaries can extend this section as locales mature. Don't pre-emptively fill empty tables — add a row when a contributor hits a real terminology choice that future PRs will face.
Native-speaker review
Strongly preferred but not blocking. Maintainers may merge a locale
PR with a nit label if no native speaker has reviewed within ~7 days
and CI passes. Subsequent fixes are welcome as separate PRs.
The 7-day window is a starting point, not a hard policy. Adjust based on your locale's contributor availability and the size of the change.
Deferred decisions
These items are decided to defer — the team has agreed not to act on them now, with rough triggers for revisiting:
- Translation memory tooling (Crowdin / Weblate / Lingui). Re-evaluate once the project hits ~12-15 active locales or when contributors start visibly duplicating effort across PRs.
Open questions
Genuinely undecided — flagged so contributors know they're live design discussions:
- Source-of-truth drift CI. A
pnpm i18n:diffscript that compares each locale's keys toen.tsand warns (not fails) when a locale exceeds the 20-key drift threshold. Tracked as a follow-up after this doc lands. - README freshness signal. A small badge or front-matter timestamp
on each
README.<code>.mdcould help readers gauge how current a translation is. - Native-speaker review window. Whether
~7 daysis too short for smaller language communities — adjust if real data shows otherwise.
If you have an opinion on any of the above, open an issue or comment on #195.