6.8 KiB
UI i18n Audit (Remaining Work)
Scope: packages/ui/ (and consumers: packages/app/, packages/enterprise/)
Date: 2026-01-20
This report documents the remaining user-facing strings in packages/ui/src that are still hardcoded (not routed through a translation function), and proposes an i18n architecture that works long-term across multiple packages.
Current State
packages/app/already has i18n viauseLanguage().t("...")with dictionaries inpackages/app/src/i18n/en.tsandpackages/app/src/i18n/zh.ts.packages/ui/is a shared component library used by:packages/app/src/pages/session.tsx(Session UI)packages/enterprise/src/routes/share/[shareID].tsx(shared session rendering)
packages/ui/currently has hardcoded English UI copy in several components (notablysession-turn.tsx,session-review.tsx,message-part.tsx).packages/enterprise/does not currently have an i18n system, so any i18n approach must be usable without depending onpackages/app/.
Decision: How We Should Add i18n To @opencode-ai/ui
Introduce a small, app-agnostic i18n interface in packages/ui/ and keep UI-owned strings in UI-owned dictionaries.
Why this is the best long-term shape:
- Keeps dependency direction clean:
packages/enterprise/(and any future consumer) can translate UI without importingpackages/app/dictionaries. - Avoids prop-drilling strings through shared components.
- Allows each package to own its strings while still rendering a single, coherent locale in the product.
Proposed Architecture
- UI provides an i18n context (no persistence)
- Add
packages/ui/src/context/i18n.tsx:- Exports
I18nProvideranduseI18n(). - Context value includes:
t(key, params?)translation function (template interpolation supported by the consumer).locale()accessor for locale-sensitive formatting (Luxon/Intl).
- Context should have a safe default (English) so UI components can render even if a consumer forgets the provider.
- Exports
- UI owns UI strings (dictionaries live in UI)
- Add
packages/ui/src/i18n/en.tsandpackages/ui/src/i18n/zh.ts. - Export them from
@opencode-ai/uiviapackages/ui/package.jsonexports (e.g."./i18n/*": "./src/i18n/*.ts"). - Use a clear namespace prefix for all UI keys to avoid collisions:
- Recommended:
ui.*(e.g.ui.sessionReview.title).
- Recommended:
- Consumers merge dictionaries and provide
t/localeonce
-
packages/app/:- Keep
packages/app/src/context/language.tsxas the source of truth for locale selection/persistence. - Extend it to merge UI dictionaries into its translation table.
- Add a tiny bridge provider in
packages/app/src/app.tsxto feeduseLanguage()into@opencode-ai/ui'sI18nProvider.
- Keep
-
packages/enterprise/:- Add a lightweight locale detector (similar to
packages/app/src/context/language.tsx), likely based onAccept-Languageon the server and/ornavigator.languageson the client. - Merge
@opencode-ai/uidictionaries and (optionally) enterprise-local dictionaries. - Wrap the share route in
I18nProvider.
- Add a lightweight locale detector (similar to
Key Naming Conventions (UI)
-
Prefer component + semantic grouping:
ui.sessionReview.titleui.sessionReview.diffStyle.unifiedui.sessionReview.diffStyle.splitui.sessionReview.expandAllui.sessionReview.collapseAll
-
For
SessionTurn:ui.sessionTurn.steps.showui.sessionTurn.steps.hideui.sessionTurn.summary.responseui.sessionTurn.diff.more(use templating:Show more changes ({{count}}))ui.sessionTurn.retry.retrying/ui.sessionTurn.retry.inSeconds/ etc (avoid string concatenation that is English-order dependent)- Status text:
ui.sessionTurn.status.delegatingui.sessionTurn.status.planningui.sessionTurn.status.gatheringContextui.sessionTurn.status.searchingCodeui.sessionTurn.status.searchingWebui.sessionTurn.status.makingEditsui.sessionTurn.status.runningCommandsui.sessionTurn.status.thinkingui.sessionTurn.status.thinkingWithTopic(template:Thinking - {{topic}})ui.sessionTurn.status.gatheringThoughtsui.sessionTurn.status.consideringNextSteps(fallback)
Locale-Sensitive Formatting (UI)
SessionTurn currently formats durations via Luxon Interval.toDuration(...).toHuman(...) without an explicit locale.
When i18n is added:
- Use
useI18n().locale()and pass locale explicitly:- Luxon:
duration.toHuman({ locale: locale(), ... })(or set.setLocale(locale())where applicable). - Intl numbers/currency (if added later):
new Intl.NumberFormat(locale(), ...).
- Luxon:
Initial Hardcoded Strings (Audit Findings)
These are the highest-impact UI surfaces to translate first.
1) packages/ui/src/components/session-review.tsx
Session changesUnified/SplitCollapse all/Expand all
2) packages/ui/src/components/session-turn.tsx
- Tool/task status strings (e.g.
Delegating work,Searching the codebase) - Steps toggle labels:
Show steps/Hide steps - Summary section title:
Response - Pagination CTA:
Show more changes ({{count}})
3) packages/ui/src/components/message-part.tsx
Examples (non-exhaustive):
ErrorEditWriteType your own answerReview your answers
4) Additional Hardcoded Strings (Full Audit)
Found during a full packages/ui/src/components + packages/ui/src/context sweep:
packages/ui/src/components/list.tsxLoadingNo resultsNo results for "{{filter}}"
packages/ui/src/components/message-nav.tsxNew message
packages/ui/src/components/text-field.tsxCopiedCopy to clipboard
packages/ui/src/components/image-preview.tsxImage preview(alt text)
Prioritized Implementation Plan
- Completed (2026-01-20): Add
@opencode-ai/uii18n context (packages/ui/src/context/i18n.tsx) + export it. - Completed (2026-01-20): Add UI dictionaries (
packages/ui/src/i18n/en.ts,packages/ui/src/i18n/zh.ts) + export them. - Completed (2026-01-20): Wire
I18nProviderinto:packages/app/src/app.tsxpackages/enterprise/src/app.tsx
- Completed (2026-01-20): Convert
packages/ui/src/components/session-review.tsxandpackages/ui/src/components/session-turn.tsxto useuseI18n().t(...). - Completed (2026-01-20): Convert
packages/ui/src/components/message-part.tsx. - Completed (2026-01-20): Do a full
packages/ui/src/components+packages/ui/src/contextaudit for additional hardcoded copy.
Notes / Risks
- SSR: Enterprise share pages render on the server. Ensure the i18n provider works in SSR and does not assume
window/navigator. - Key collisions: Use a consistent
ui.*prefix to avoid clashing with app keys. - Fallback behavior: Decide whether missing keys should:
- fall back to English, or
- render the key (useful for catching missing translations).