2026-04-26 14:03:39 +03:00
---
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.
2026-07-01 09:55:41 +03:00
## 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.
2026-04-26 14:03:39 +03:00
## Required Flow
1. Add or reuse a key in `packages/ui/src/lib/i18n/messages/en.ts` .
2026-07-01 09:55:41 +03:00
2. Add the same key — fully translated, not the English text — to every non-English dictionary in `packages/ui/src/lib/i18n/messages/` .
2026-04-26 14:03:39 +03:00
3. In components, call `const { t } = useI18n()` from `@/lib/i18n` and render `t('key')` .
4. For locale names or language picker labels, use `label(locale)` from `useI18n()` .
5. Keep locale state in `packages/ui/src/lib/i18n/*` ; do not add locale fields to broad stores like `useUIStore` .
6. 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:
```ts
'settings.appearance.language.label' : 'Language'
'layout.mainTab.chat' : 'Chat'
'chat.input.placeholder' : 'Ask OpenChamber...'
```
Bad:
```ts
'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.
```ts
'toast.language.changed' : 'Language changed to {language}'
```
```tsx
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:
```tsx
t ( 'dialog.delete.description' , { count , suffix : count === 1 ? '' : 's' })
```
Good:
```tsx
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:
```tsx
t ( 'dialog.delete.description' , {
dateLabel : date ? t ( 'dialog.delete.dateSuffix' , { date }) : '' ,
})
```
Good:
```tsx
date
? t ( 'dialog.delete.descriptionWithDate' , { count , date })
: t ( 'dialog.delete.description' , { count })
```
2026-08-13 13:57:00 +03:00
## Translation Boundary
2026-04-26 14:03:39 +03:00
2026-08-13 13:57:00 +03:00
Translate visible text, placeholders, tooltips, dialogs, toasts, empty/error/loading states, and user-facing `aria-label` , `title` , and `alt` text.
2026-04-26 14:03:39 +03:00
2026-08-13 13:57:00 +03:00
Keep these literal:
2026-04-26 14:03:39 +03:00
- Product names: `OpenChamber` , `OpenCode` , `GitHub`
- Protocol/tool acronyms: `MCP` , `SSE` , `WebSocket` , `API`
- Model/provider names
- File paths, command names, environment variables
- User/generated content
2026-08-13 13:57:00 +03:00
## Completion Criteria
2026-04-26 14:03:39 +03:00
- No new hardcoded user-facing English in changed UI files.
2026-08-13 13:57:00 +03:00
- Every new key exists in all dictionaries with a real translation.
- All translated values are resolved inside a reactive render/hook boundary.
2026-04-26 14:03:39 +03:00
- No locale state added to broad/shared stores.
- No full app remount for locale changes.
- Locale switch preserves current UI state.