Files
opencode/specs/07-ui-i18n-audit.md
GitHub Action bb8bf32abe chore: generate
2026-01-20 23:58:59 +00:00

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 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.
  1. 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).
  1. 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

4) Additional Hardcoded Strings (Full Audit)

Found during a full packages/ui/src/components + packages/ui/src/context sweep:

  • packages/ui/src/components/list.tsx
    • Loading
    • No results
    • No results for "{{filter}}"
  • packages/ui/src/components/message-nav.tsx
    • New message
  • packages/ui/src/components/text-field.tsx
    • Copied
    • Copy to clipboard
  • packages/ui/src/components/image-preview.tsx
    • Image preview (alt text)

Prioritized Implementation Plan

  1. Completed (2026-01-20): Add @opencode-ai/ui i18n context (packages/ui/src/context/i18n.tsx) + export it.
  2. Completed (2026-01-20): Add UI dictionaries (packages/ui/src/i18n/en.ts, packages/ui/src/i18n/zh.ts) + export them.
  3. Completed (2026-01-20): Wire I18nProvider into:
    • packages/app/src/app.tsx
    • packages/enterprise/src/app.tsx
  4. Completed (2026-01-20): Convert packages/ui/src/components/session-review.tsx and packages/ui/src/components/session-turn.tsx to use useI18n().t(...).
  5. Completed (2026-01-20): Convert packages/ui/src/components/message-part.tsx.
  6. Completed (2026-01-20): 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).