|
@@ -0,0 +1,140 @@
|
|
|
|
|
+# 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 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/`.
|
|
|
|
|
+
|
|
|
|
|
+## 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 importing `packages/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
|
|
|
|
|
+
|
|
|
|
|
+1) **UI provides an i18n context (no persistence)**
|
|
|
|
|
+
|
|
|
|
|
+- Add `packages/ui/src/context/i18n.tsx`:
|
|
|
|
|
+ - Exports `I18nProvider` and `useI18n()`.
|
|
|
|
|
+ - 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.
|
|
|
|
|
+
|
|
|
|
|
+2) **UI owns UI strings (dictionaries live in UI)**
|
|
|
|
|
+
|
|
|
|
|
+- Add `packages/ui/src/i18n/en.ts` and `packages/ui/src/i18n/zh.ts`.
|
|
|
|
|
+- Export them from `@opencode-ai/ui` via `packages/ui/package.json` exports (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`).
|
|
|
|
|
+
|
|
|
|
|
+3) **Consumers merge dictionaries and provide `t`/`locale` once**
|
|
|
|
|
+
|
|
|
|
|
+- `packages/app/`:
|
|
|
|
|
+ - Keep `packages/app/src/context/language.tsx` as 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.tsx` to feed `useLanguage()` into `@opencode-ai/ui`'s `I18nProvider`.
|
|
|
|
|
+
|
|
|
|
|
+- `packages/enterprise/`:
|
|
|
|
|
+ - Add a lightweight locale detector (similar to `packages/app/src/context/language.tsx`), likely based on `Accept-Language` on the server and/or `navigator.languages` on the client.
|
|
|
|
|
+ - Merge `@opencode-ai/ui` dictionaries and (optionally) enterprise-local dictionaries.
|
|
|
|
|
+ - Wrap the share route in `I18nProvider`.
|
|
|
|
|
+
|
|
|
|
|
+### Key Naming Conventions (UI)
|
|
|
|
|
+
|
|
|
|
|
+- Prefer component + semantic grouping:
|
|
|
|
|
+ - `ui.sessionReview.title`
|
|
|
|
|
+ - `ui.sessionReview.diffStyle.unified`
|
|
|
|
|
+ - `ui.sessionReview.diffStyle.split`
|
|
|
|
|
+ - `ui.sessionReview.expandAll`
|
|
|
|
|
+ - `ui.sessionReview.collapseAll`
|
|
|
|
|
+
|
|
|
|
|
+- For `SessionTurn`:
|
|
|
|
|
+ - `ui.sessionTurn.steps.show`
|
|
|
|
|
+ - `ui.sessionTurn.steps.hide`
|
|
|
|
|
+ - `ui.sessionTurn.summary.response`
|
|
|
|
|
+ - `ui.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.delegating`
|
|
|
|
|
+ - `ui.sessionTurn.status.planning`
|
|
|
|
|
+ - `ui.sessionTurn.status.gatheringContext`
|
|
|
|
|
+ - `ui.sessionTurn.status.searchingCode`
|
|
|
|
|
+ - `ui.sessionTurn.status.searchingWeb`
|
|
|
|
|
+ - `ui.sessionTurn.status.makingEdits`
|
|
|
|
|
+ - `ui.sessionTurn.status.runningCommands`
|
|
|
|
|
+ - `ui.sessionTurn.status.thinking`
|
|
|
|
|
+ - `ui.sessionTurn.status.thinkingWithTopic` (template: `Thinking - {{topic}}`)
|
|
|
|
|
+ - `ui.sessionTurn.status.gatheringThoughts`
|
|
|
|
|
+ - `ui.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(), ...)`.
|
|
|
|
|
+
|
|
|
|
|
+## Initial Hardcoded Strings (Audit Findings)
|
|
|
|
|
+
|
|
|
|
|
+These are the highest-impact UI surfaces to translate first.
|
|
|
|
|
+
|
|
|
|
|
+### 1) `packages/ui/src/components/session-review.tsx`
|
|
|
|
|
+
|
|
|
|
|
+- `Session changes`
|
|
|
|
|
+- `Unified` / `Split`
|
|
|
|
|
+- `Collapse 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):
|
|
|
|
|
+
|
|
|
|
|
+- `Error`
|
|
|
|
|
+- `Edit`
|
|
|
|
|
+- `Write`
|
|
|
|
|
+- `Type your own answer`
|
|
|
|
|
+- `Review your answers`
|
|
|
|
|
+
|
|
|
|
|
+## Prioritized Implementation Plan
|
|
|
|
|
+
|
|
|
|
|
+1. Add `@opencode-ai/ui` i18n context (`packages/ui/src/context/i18n.tsx`) + export it.
|
|
|
|
|
+2. Add UI dictionaries (`packages/ui/src/i18n/en.ts`, `packages/ui/src/i18n/zh.ts`) + export them.
|
|
|
|
|
+3. Wire `I18nProvider` into:
|
|
|
|
|
+ - `packages/app/src/app.tsx`
|
|
|
|
|
+ - `packages/enterprise/src/routes/share/[shareID].tsx`
|
|
|
|
|
+4. Convert `packages/ui/src/components/session-review.tsx` and `packages/ui/src/components/session-turn.tsx` to use `useI18n().t(...)`.
|
|
|
|
|
+5. Convert `packages/ui/src/components/message-part.tsx`.
|
|
|
|
|
+6. Do a full `packages/ui/src/components` + `packages/ui/src/context` audit 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).
|