Internationalization
The Castor UI supports 简体中文 (zh-CN), English (en-US) and 日本語 (ja-JP). Users pick a language from the language switcher in the top bar, and the choice is saved in the browser. On the first visit, the language is matched against the browser language, falling back to English if nothing matches.
Core convention: the Chinese source text is the key
Code contains the Chinese source text directly, and that text doubles as the translation key:
const { t } = useTranslation()
t('保存')
t('共 {{count}} 条', { count })- Chinese needs no translation file: when no translation is found, the key itself, i.e. the Chinese text, is displayed.
- English and Japanese translations live in JSON files, mapping Chinese → translation.
This way you don't have to invent a key name for every string, and the code reads the same as the UI.
Frontend
Translation files
| Location | Contents |
|---|---|
locales/en-US.json, locales/ja-JP.json in the page directory | That page's text, e.g. apps/web/src/modules/admin/pages/users/locales/ |
apps/web/src/locales/en-US.json, ja-JP.json | Shared text |
apps/web/src/locales/menus/en-US.json, ja-JP.json | Menu names, translated by menu code |
All locales/*.json files are merged into a single namespace at build time (the menu files under locales/menus/ go into a separate menu namespace). The en-US.json and ja-JP.json in the same directory must contain the same keys.
Example page translations (locales/en-US.json):
{
"导入用户": "Import users",
"删除用户 {{name}}?": "Delete user {{name}}?"
}Which text needs t()
Shared components automatically translate the string props you pass them, so just write the Chinese and add the translations:
- Titles of
PageHeaderandPanel - Column
titleinDataTable - Text in
label,placeholder,optionsandrulesofFormFields - Text props of components such as
FilterSelect,SegmentedTabs,StatusBadge,StatCard,RowActions,ConfirmActionandFormDialog - Fixed strings like
toast.success('固定中文')
These cases must be wrapped in t():
- Chinese text written directly in JSX
aria-label,titleandplaceholderon native elements- Text with variables: write
t('删除用户 ?', { name }), not a Chinese template literal - Other display channels such as chart axes and legends
Demo content is not translated
Demo content such as sample data and sample documents is data, not UI text. Mark it with a comment and the scanner will skip it:
- Single line: put
// i18n-ignore-next-lineon the line above - Whole file: add an
i18n-ignore-filecomment to the file
Menu name translations
Menu names are stored in the database (in Chinese). The frontend looks up the translated name by menu code in apps/web/src/locales/menus/<lang>.json, and falls back to the name from the database if none is found:
{
"system_users": "Users",
"system_roles": "Roles"
}When you add a menu, add its code to both en-US.json and ja-JP.json. A frontend test checks that every menu code in seed-rbac.ts has English and Japanese names.
Translating backend errors
Backend code keeps throwing errors in Chinese:
throw new ServiceError('用户名已存在')The frontend's request.ts sends an Accept-Language header with every request. Before the response goes out, the backend uses that header to translate error, message and the error_rows[].reason of import error rows in the JSON response. A request without a supported language (for example from curl or an API-token client) gets English. Messages without a registered translation are returned in Chinese as-is, without raising an error.
Register English and Japanese translations for new error or notice messages in apps/api/src/i18n/messages.ts:
// Exact messages
export const MESSAGES = {
'用户名已存在': { 'en-US': 'Username already exists', 'ja-JP': 'ユーザー名は既に存在します' },
}
// Messages built from template literals: regex on the Chinese text, $1… in the translation
export const PATTERNS = [
{ re: /^菜单编码 (.+) 已存在$/, 'en-US': 'Menu code $1 already exists', 'ja-JP': 'メニューコード $1 は既に存在します' },
]This is only a sketch; the real entries in messages.ts are grouped by module.
Import/export headers stay in Chinese
Column headers in import/export files don't change with the UI language; they are always Chinese. In error messages that quote a header, the header part stays Chinese too.
Scanner and test guards
Frontend scanner
apps/web/scripts/i18n-scan.mjs reports three kinds of issues:
| Issue | Meaning |
|---|---|
missing | A Chinese string has no English or Japanese translation in any locales/*.json |
jsx-text | Chinese written directly as JSX text without going through t() |
template | A template literal contains Chinese |
node apps/web/scripts/i18n-scan.mjs # Scan all of src; exits with code 1 if there are issues
node apps/web/scripts/i18n-scan.mjs src/modules/admin/pages/users # Scan one directory only (relative to apps/web)
node apps/web/scripts/i18n-scan.mjs --json src/modules/admin/pages/users # JSON outputNew pages must scan with 0 issues.
Tests
| Test | What it checks |
|---|---|
apps/web/test/i18n.test.ts | en-US and ja-JP have the same keys in every locales/ directory; the same key has no conflicting translations across files; every menu code in seed-rbac.ts has English and Japanese names; the whole frontend source scans clean |
apps/api/test/i18n-messages.test.ts | Parses the backend source, finds every Chinese message that could be returned to users, and checks that each one is registered in messages.ts |
These tests run as part of pnpm test and pnpm verify, so a missing translation fails the gate.
Code comments must be in English
All code comments are in English, including frontend, backend, scripts, tests and code generated by scaffold. UI text is still written as the Chinese source text that serves as the key.
When a comment needs to refer to a Chinese string that actually exists in the code (such as an error message or translation key), put it in quotes or backticks. apps/web/test/english-comments.test.ts checks the comments under apps/api, apps/web, apps/mcp and docs/templates.
Checklist for a new page
- Write the UI text as the Chinese source text.
- Use
t()for JSX text, native attributes and text with variables. - Create
locales/en-US.jsonandlocales/ja-JP.jsonin the page directory. - Add translated names for new menus to both files in
apps/web/src/locales/menus/. - Register new backend errors in
apps/api/src/i18n/messages.ts. - Run
node apps/web/scripts/i18n-scan.mjs <page-dir>and confirm 0 issues.
