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.
packages/app/ already has i18n via useLanguage().t("...") with dictionaries in packages/app/src/i18n/en.ts and packages/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 (notably session-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 on packages/app/.@opencode-ai/uiIntroduce 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:
packages/enterprise/ (and any future consumer) can translate UI without importing packages/app/ dictionaries.packages/ui/src/context/i18n.tsx:
I18nProvider and useI18n().t(key, params?) translation function (template interpolation supported by the consumer).locale() accessor for locale-sensitive formatting (Luxon/Intl).packages/ui/src/i18n/en.ts and packages/ui/src/i18n/zh.ts.@opencode-ai/ui via packages/ui/package.json exports (e.g. "./i18n/*": "./src/i18n/*.ts").ui.* (e.g. ui.sessionReview.title).t/locale oncepackages/app/:
packages/app/src/context/language.tsx as the source of truth for locale selection/persistence.packages/app/src/app.tsx to feed useLanguage() into @opencode-ai/ui's I18nProvider.packages/enterprise/:
packages/app/src/context/language.tsx), likely based on Accept-Language on the server and/or navigator.languages on the client.@opencode-ai/ui dictionaries and (optionally) enterprise-local dictionaries.I18nProvider.Prefer component + semantic grouping:
ui.sessionReview.titleui.sessionReview.diffStyle.unifiedui.sessionReview.diffStyle.splitui.sessionReview.expandAllui.sessionReview.collapseAllFor 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)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)SessionTurn currently formats durations via Luxon Interval.toDuration(...).toHuman(...) without an explicit locale.
When i18n is added:
useI18n().locale() and pass locale explicitly:
duration.toHuman({ locale: locale(), ... }) (or set .setLocale(locale()) where applicable).new Intl.NumberFormat(locale(), ...).These are the highest-impact UI surfaces to translate first.
packages/ui/src/components/session-review.tsxSession changesUnified / SplitCollapse all / Expand allpackages/ui/src/components/session-turn.tsxDelegating work, Searching the codebase)Show steps / Hide stepsResponseShow more changes ({{count}})packages/ui/src/components/message-part.tsxExamples (non-exhaustive):
ErrorEditWriteType your own answerReview your answersFound during a full packages/ui/src/components + packages/ui/src/context sweep:
packages/ui/src/components/list.tsx
LoadingNo resultsNo results for "{{filter}}"packages/ui/src/components/message-nav.tsx
New messagepackages/ui/src/components/text-field.tsx
CopiedCopy to clipboardpackages/ui/src/components/image-preview.tsx
Image preview (alt text)@opencode-ai/ui i18n context (packages/ui/src/context/i18n.tsx) + export it.packages/ui/src/i18n/en.ts, packages/ui/src/i18n/zh.ts) + export them.I18nProvider into:
packages/app/src/app.tsxpackages/enterprise/src/app.tsxpackages/ui/src/components/session-review.tsx and packages/ui/src/components/session-turn.tsx to use useI18n().t(...).packages/ui/src/components/message-part.tsx.packages/ui/src/components + packages/ui/src/context audit for additional hardcoded copy.window/navigator.ui.* prefix to avoid clashing with app keys.