| 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.
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. Do not invent a fallback policy.
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/i18n and render t('key').
- For locale names or language picker labels, use
label(locale) from useI18n().
- Keep locale state in
packages/ui/src/lib/i18n/*; do not add locale fields to broad stores like useUIStore.
- 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 with t(...) inside the component.
- For non-React helpers, pass translated strings in from the component or pass
t explicitly.
Key Style
Use stable semantic keys, not English text as keys.
Keys should describe location + UI role + meaning. They should not encode current copy wording.
Use existing nearby naming when extending a surface. If no nearby pattern exists, choose a short path that mirrors the UI ownership.
Namespaces like layout.*, settings.*, chat.*, git.*, session.*, toast.*, and dialog.* are examples, not a fixed exhaustive list.
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) })
Do not pass grammar fragments as params. Never use params like {suffix}, {plural}, {article}, {prefix}, {dateSuffix}, or pieces of words/sentences.
Bad:
t('dialog.delete.description', { count, suffix: count === 1 ? '' : 's' })
Good:
count === 1
? t('dialog.delete.descriptionSingle', { count })
: t('dialog.delete.descriptionPlural', { count })
Plural/count-dependent text must use separate complete-message keys unless all supported locales can use one identical complete sentence. Placeholders are only for real values ({count}, {name}, {path}), not grammar.
Optional clauses must also be complete-message keys. Do not build a sentence by injecting a translated phrase into another translated 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 })
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.
- 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.