* i18n: add full Thai translation (th-TH) * i18n: fix placeholders, update tests and complete documentation for Thai (th) * i18n: fix placeholders, update tests and complete documentation for Thai locale * chore: revert unrelated docker deployment changes (fix scope drift) --------- Co-authored-by: ryu <ryu@example.com>
14 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 the boundary between translated surfaces and agent-facing source material. This file covers how to add and maintain a locale across the surfaces contributors touch most often: UI chrome, root READMEs, core docs, and display metadata.
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 dictionaries live in apps/web/src/i18n/locales/.
Root README translations live beside README.md. Core doc
translations live beside QUICKSTART.md and
CONTRIBUTING.md. Display metadata translations live in
apps/web/src/i18n/content*.ts.
The LOCALES array in apps/web/src/i18n/types.ts
is the authoritative list for the UI dict. Root README language
switchers cover every locale that has a root README; this set can differ
from LOCALES.
| Code | Language | UI dict | Root README | Core docs | Display metadata | Status |
|---|---|---|---|---|---|---|
en |
English | en.ts (source) |
README.md |
source | content.ts |
active |
id |
Bahasa Indonesia | id.ts |
— | — | — | active |
ar |
العربية | ar.ts |
README.ar.md |
— | — | active |
de |
Deutsch | de.ts |
README.de.md |
yes | — | active |
es-ES |
Español (España) | es-ES.ts |
README.es.md |
— | — | active |
fa |
فارسی | fa.ts |
— | — | — | active |
hu |
Magyar | hu.ts |
— | — | — | active |
ja |
日本語 | ja.ts |
README.ja-JP.md |
yes | — | active |
ko |
한국어 | ko.ts |
README.ko.md |
— | — | active |
pl |
Polski | pl.ts |
— | — | — | active |
pt-BR |
Português (Brasil) | pt-BR.ts |
README.pt-BR.md |
yes | — | active |
ru |
Русский | ru.ts |
README.ru.md |
— | content.ru.ts |
active |
zh-CN |
简体中文 | zh-CN.ts |
README.zh-CN.md |
yes | — | active |
zh-TW |
繁體中文 | zh-TW.ts |
README.zh-TW.md |
— | — | active |
fr |
Français | fr.ts |
README.fr.md |
yes | content.fr.ts |
active |
uk |
Українська | uk.ts |
README.uk.md |
— | — | active |
tr |
Türkçe | tr.ts |
README.tr.md |
— | — | active |
th |
ภาษาไทย | th.ts |
— | — | — | active |
A locale may ship a UI dict, a root README, core docs, display metadata, or any subset of those surfaces. The English locale is the source of truth. Runtime lookup falls back to English for missing UI keys, while TypeScript requires registered dictionaries to satisfy the full
Dictshape. Partial dictionaries can use...enplus translated overrides, and reviewers should treat remaining English strings as drift.
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 root README — copy
README.mdtoREADME.<code>.md. Repository precedent may use a documentation-region code that differs from the UI dict code when that is the familiar docs filename, such asREADME.ja-JP.mdwith UI localeja, orREADME.es.mdwith UI localees-ES. Use OpenCCs2twp.jsonfor zh-CN ↔ zh-TW; use your judgment elsewhere. -
Update the language switcher in every root README (line ~30 of each root
README*.md). Match the order used in the English README and include the same set everywhere. The switcher set is the set of root README translations, so it may differ fromLOCALES. -
(Optional) Translate core docs — copy
QUICKSTART.mdand/orCONTRIBUTING.mdto the matching docs filename, following existing examples such asQUICKSTART.fr.md,CONTRIBUTING.pt-BR.md, andCONTRIBUTING.ja-JP.md. Update links from the translated README to the translated core docs that exist for that locale. -
(Optional) Translate display metadata in
apps/web/src/i18n/content*.ts. Keep this to display-only metadata for examples, gallery cards, and localized content chrome. Agent-executed prompts, skill instructions, design systems, and prompt bodies stay in their source language so prompt QA remains centralized. -
Run checks:
pnpm typecheckconfirms the locale union andDICTSmap agree.pnpm --filter @open-design/web testcovers locale/content drift tests for the web package.
Maintaining existing translations
When a PR changes English copy, check which surface changed and update the matching translated surfaces deliberately:
- UI chrome: update
apps/web/src/i18n/locales/en.tsfirst, then add translated values to active locale dictionaries when the PR owns that refresh. Partial dictionaries may inherit from English with...en. - Root README: keep root README language switchers in sync across all
root
README*.mdfiles. Check badge counts, Quickstart links, supported agent lists, and release/download links againstREADME.mdduring a refresh. - Core docs: keep translated
QUICKSTART.*.mdandCONTRIBUTING.*.mdaligned with their English source when the locale owns those docs. - Display metadata: update
apps/web/src/i18n/content*.tsalongsidecontent.tswhen that locale maintains display metadata.
Automated P0 check:
pnpm i18n:checkenforces UI locale registration, root README switcher consistency, and root README links to translated core docs. CI runs this as a hard-fail check because these are structural issues.
Known current drift to clean up in focused PRs:
- Several translated READMEs lag behind current English badge counts, supported agent lists, and Quickstart/download links.
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.
- README template-driven generation (e.g. NRG,
custom
.src.mdbuild scripts, All Contributors-style tooling). Re-evaluate once the project hits ≥15 locales or README structural edits become more frequent than monthly. Discussion in #195: template-driven generation solves the "update line 27 in 10 README variants" brittleness, but forces a shared structure that today's locale variants intentionally diverge from (e.g.README.zh-TW.md's "上手體驗" section, the pt-BR / pt-PT precedent for content-level — not just translation-level — differences). Worth revisiting once locale voice is more settled or the manual-update cost grows.
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.