locale-ui-patterns
GitHub规范 OpenChamber UI 本地化流程,禁止硬编码英文,要求新增键必须立即提供全量翻译。涵盖 i18n 库使用、Key 命名规范及组件集成规则,确保多语言体验一致且无缺陷。
触发场景
安装
npx skills add openchamber/openchamber --skill locale-ui-patterns -g -y
SKILL.md
Frontmatter
{
"name": "locale-ui-patterns",
"description": "Use when creating or modifying OpenChamber UI text, labels, buttons, placeholders, aria labels, empty states, toasts, dialogs, settings copy, navigation labels, or any user-facing strings."
}
Locale UI Patterns
Core Rule
User-facing UI text must go through @/lib/i18n; do not hardcode English strings in components.
What the text says
UI text tells the person what they get and what to do: what an option gives them, what happened, what fixes it. Engineering reasons (layout jumps, races, what the code avoids) belong in code comments and commit messages. Example: "Shows the model's thinking as it arrives. When off, the block stays folded and opens with a click." An error names the known cause and the action ("reconnect the repository's account in Settings"), never a generic "something went wrong" when the cause is known. An error that hides a known cause is a defect (root AGENTS.md, Pull Request Handoff), fixed without asking.
Translate everything immediately (no English placeholders)
Every key you add to a non-English dictionary MUST contain a real translation in that language — never the English source string as a stand-in. There is NO "leave it in English for now" convention in this project; if an agent told you there was, it was wrong. Copying the English value into es.ts/fr.ts/ko.ts/pl.ts/pt-BR.ts/uk.ts/zh-CN.ts/zh-TW.ts is a defect, not a deferral. The app ships every locale at once, so an untranslated key is a visible bug for those users.
If you genuinely cannot translate a language, say so explicitly to the user instead of silently pasting English.
Required Flow
- Add or reuse a key in
packages/ui/src/lib/i18n/messages/en.ts. - Add the same key — fully translated, not the English text — to every non-English dictionary in
packages/ui/src/lib/i18n/messages/. - In components, call
const { t } = useI18n()from@/lib/i18nand rendert('key'). - For locale names or language picker labels, use
label(locale)fromuseI18n(). - Keep locale state in
packages/ui/src/lib/i18n/*; do not add locale fields to broad stores likeuseUIStore. - Do not remount the app to update language. Components must re-render through
useI18n().
Component Usage Rules
- Import from
@/lib/i18n, not deep files. - Keep
t(...)calls inside React render/hook scope so locale changes re-render text. - Do not resolve translated text at module scope.
- For static option arrays, store
labelKey/descriptionKey; resolve witht(...)inside the component. - For non-React helpers, pass translated strings in from the component or pass
texplicitly.
Key Style
Keys are stable and semantic: location + UI role + meaning (settings.appearance.language.label), never the English copy. Extend the naming already used nearby; with none, mirror the UI ownership in a short path.
Good:
'settings.appearance.language.label': 'Language'
'layout.mainTab.chat': 'Chat'
'chat.input.placeholder': 'Ask OpenChamber...'
Bad:
'Language': 'Language'
'chatLabel': 'Chat'
'askOpenChamberDotDotDot': 'Ask OpenChamber...'
Avoid overly generic keys unless the text is truly global and context-independent. Prefer specific keys when button meaning can vary by surface.
Parameters
Use {name} placeholders for dynamic values.
'toast.language.changed': 'Language changed to {language}'
t('toast.language.changed', { language: label(locale) })
Placeholders carry only real values ({count}, {name}, {path}), never grammar pieces such as {suffix}, {plural} or {article}.
Bad:
t('dialog.delete.description', { count, suffix: count === 1 ? '' : 's' })
Good:
count === 1
? t('dialog.delete.descriptionSingle', { count })
: t('dialog.delete.descriptionPlural', { count })
Count-dependent text and optional clauses each get their own complete-message key; never inject one translated phrase into another. Count-dependent text can use one key if all supported locales use one identical complete sentence.
Bad:
t('dialog.delete.description', {
dateLabel: date ? t('dialog.delete.dateSuffix', { date }) : '',
})
Good:
date
? t('dialog.delete.descriptionWithDate', { count, date })
: t('dialog.delete.description', { count })
Desktop Native Menus
The Electron app menu and the right-click context menu are native, outside @/lib/i18n. Their labels live in packages/electron/menu-locales.mjs (MENU_LOCALE_DICTIONARIES, CONTEXT_MENU_LABEL_DICTIONARIES), keyed by the same locale codes as the UI. A new menu item gets its label in every locale there, and a new UI language gets its own entry there in the same change; menu-locales.test.mjs checks key parity.
Adding a UI Language
Russian is not added: a standing maintainer decision (triage-prs, template russian-locale).
A new locale code lands in all of these in one change:
packages/ui/src/lib/i18n/runtime.ts: theLocaleunion,LOCALES,LOCALE_LABEL_KEYS, andnormalizeLocale(include legacy tags the platforms still send, such asiwfor Hebrew). A right-to-left language also joinsRTL_LOCALES; the layout side is owned byrtl-layout.store.ts(lazy import),intl.ts(Intl tag),bootstrap.ts(startup strings),messages.test.ts.messages/<locale>.tsandmessages/<locale>.settings.ts, a<locale>block in everymessages/*.i18n.ts, and acommon.language.<name>label in every existing dictionary.packages/electron/menu-locales.mjs(see above).packages/web/server/lib/opencode/settings-helpers.jsUI_LOCALE_VALUES: without it the server drops the saved choice.packages/web/server/lib/walkthrough/languages.jsnames the language for walkthrough prompts.
Translation Boundary
Translate visible text, placeholders, tooltips, dialogs, toasts, empty/error/loading states, and user-facing aria-label, title, and alt text.
Keep these literal:
- Product names:
OpenChamber,OpenCode,GitHub - Protocol/tool acronyms:
MCP,SSE,WebSocket,API - Model/provider names
- File paths, command names, environment variables
- User/generated content
Completion Criteria
- No new hardcoded user-facing English in changed UI files.
- Every new key exists in all dictionaries with a real translation, including
packages/electron/menu-locales.mjsfor native menu labels. - All translated values are resolved inside a reactive render/hook boundary.
- No locale state added to broad/shared stores.
- No full app remount for locale changes.
- Locale switch preserves current UI state.
版本历史
-
6cd8058
当前 2026-11-03 06:04
修正了关于已知错误原因处理的工作性质描述;补充了禁止自行制定回退策略的要求。
-
bf977d2
2026-10-19 13:04
新增阿拉伯语和希伯来语支持,实现 RTL 布局镜像适配及移动端抽屉方向调整
-
2db90f7
2026-08-20 04:55
细化 Agent Skill 指导文档,明确技能用途和核心规则。
- 74b1bd8 2026-07-25 10:36


