Add English and Chinese localization#528
Conversation
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
Adds an i18n system (English + Chinese) to the web UI with a language switcher and persisted language preference, and updates Codex agent launch args for hook-trust bypass.
Changes:
- Introduces i18n infrastructure (translations, context provider, language persistence) and wires it into the app root.
- Replaces many user-facing strings with translated keys and adds a language switcher in the top bar.
- Adds
--dangerously-bypass-hook-trustto Codex CLI args (and tests) to ensure hook strategies run without trust gating.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| web-ui/src/storage/local-storage-store.ts | Adds a LocalStorage key for persisted language selection. |
| web-ui/src/main.tsx | Wraps the app in I18nProvider so translations are available globally. |
| web-ui/src/i18n/translations.ts | Adds language registry, translation dictionaries, and translate/interpolation helpers. |
| web-ui/src/i18n/i18n-context.tsx | Adds i18n React context/provider with localStorage persistence and <html lang> updates. |
| web-ui/src/components/top-bar.tsx | Localizes labels/tooltips and adds the LanguageSwitcher to the top bar. |
| web-ui/src/components/task-trash-warning-dialog.tsx | Localizes warning copy and guidance strings. |
| web-ui/src/components/task-prompt-composer.tsx | Localizes composer placeholder and file mention messages. |
| web-ui/src/components/task-inline-create-card.tsx | Localizes create/edit UI, including auto-review mode labels. |
| web-ui/src/components/task-create-dialog.tsx | Localizes dialog titles, buttons, and multi-create copy. |
| web-ui/src/components/task-agent-model-picker.tsx | Localizes labels (“Default”, “Provider”, etc.) and loading strings. |
| web-ui/src/components/shared/cline-setup-section.tsx | Localizes Cline setup UI strings and errors; localizes reasoning effort labels. |
| web-ui/src/components/shared/cline-add-provider-dialog.tsx | Localizes provider dialog UI strings and errors. |
| web-ui/src/components/shared/account-organization-section.tsx | Localizes account/org and credits UI strings and errors. |
| web-ui/src/components/search-select-dropdown.tsx | Adds i18n-backed default placeholder/empty/no-results text. |
| web-ui/src/components/runtime-settings-dialog.tsx | Localizes settings navigation/labels/errors and related UI copy. |
| web-ui/src/components/project-navigation-panel.tsx | Localizes sidebar, tips, shortcuts, and project list UI strings. |
| web-ui/src/components/open-workspace-button.tsx | Localizes “Open” and aria-labels for open targets. |
| web-ui/src/components/language-switcher.tsx | New language selector component using i18n context. |
| web-ui/src/components/detail-panels/column-context-panel.tsx | Localizes column actions/titles/empty states. |
| web-ui/src/components/clear-trash-dialog.tsx | Localizes clear-done confirmation dialog strings. |
| web-ui/src/components/card-detail-view.tsx | Localizes tab labels, diff toolbar labels, resize aria labels, and action text. |
| web-ui/src/components/branch-select-dropdown.tsx | Uses i18n defaults for empty/no-results texts. |
| web-ui/src/components/board-column.tsx | Localizes column titles and column action labels/tooltips. |
| web-ui/src/components/board-card.tsx | Localizes status text, labels, suffixes, and action aria-labels. |
| web-ui/src/App.tsx | Localizes top-level empty state strings and toast messages. |
| test/runtime/terminal/agent-session-adapters.test.ts | Adds coverage asserting the new Codex CLI flag is present and ordered correctly. |
| src/terminal/agent-session-adapters.ts | Adds helper to insert CLI flags before subcommands; applies new Codex hook-trust bypass flag. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| import { createContext, useCallback, useContext, useEffect, useMemo, useState } from "react"; | ||
| import { LocalStorageKey, readLocalStorageItem, writeLocalStorageItem } from "@/storage/local-storage-store"; | ||
| import { | ||
| type AppLanguage, | ||
| DEFAULT_LANGUAGE, | ||
| isAppLanguage, | ||
| type TranslationKey, | ||
| type TranslationValues, | ||
| translate, | ||
| } from "./translations"; | ||
|
|
||
| interface I18nContextValue { | ||
| language: AppLanguage; | ||
| setLanguage: (language: AppLanguage) => void; | ||
| t: (key: TranslationKey, values?: TranslationValues) => string; | ||
| } | ||
|
|
||
| const I18nContext = createContext<I18nContextValue | null>(null); | ||
| const fallbackI18nContextValue: I18nContextValue = { | ||
| language: DEFAULT_LANGUAGE, | ||
| setLanguage: () => {}, | ||
| t: (key, values) => translate(DEFAULT_LANGUAGE, key, values), | ||
| }; | ||
|
|
||
| function readStoredLanguage(): AppLanguage { | ||
| const storedLanguage = readLocalStorageItem(LocalStorageKey.Language); | ||
| return isAppLanguage(storedLanguage) ? storedLanguage : DEFAULT_LANGUAGE; | ||
| } | ||
|
|
||
| export function I18nProvider({ children }: { children: React.ReactNode }): React.ReactElement { | ||
| const [language, setLanguageState] = useState<AppLanguage>(readStoredLanguage); |
| function addCliOptionBeforeSubcommand(args: string[], optionName: string, subcommands: readonly string[]): void { | ||
| if (hasCliOption(args, optionName)) { | ||
| return; | ||
| } | ||
| const subcommandIndex = args.findIndex((arg) => subcommands.includes(arg)); | ||
| args.splice(subcommandIndex === -1 ? args.length : subcommandIndex, 0, optionName); | ||
| } |
| const hooks = resolveHookContext(input); | ||
| if (hooks) { | ||
| addCliOptionBeforeSubcommand(codexArgs, "--dangerously-bypass-hook-trust", ["resume", "fork"]); | ||
| configureCodexHooks(codexArgs); | ||
| Object.assign( |
| {APP_LANGUAGES.map((item) => ( | ||
| <button | ||
| type="button" | ||
| key={item.id} | ||
| className={cn( | ||
| "flex w-full cursor-pointer items-center gap-2 rounded-md px-2.5 py-1.5 text-left text-[13px] text-text-primary hover:bg-surface-3", | ||
| item.id === language && "bg-surface-3", | ||
| )} | ||
| onClick={() => handleSelect(item.id)} | ||
| > | ||
| <span className="flex-1"> | ||
| {item.id === "zh" ? t("language.option.zh") : t("language.option.en")} | ||
| </span> | ||
| {item.id === language ? <Check size={14} className="text-text-secondary" /> : null} | ||
| </button> | ||
| ))} |
| const columnTitle = | ||
| column.id === "backlog" | ||
| ? t("board.column.backlog") | ||
| : column.id === "in_progress" | ||
| ? t("board.column.inProgress") | ||
| : column.id === "review" | ||
| ? t("board.column.review") | ||
| : t("board.column.done"); |
| const columnTitle = | ||
| column.id === "backlog" | ||
| ? t("board.column.backlog") | ||
| : column.id === "in_progress" | ||
| ? t("board.column.inProgress") | ||
| : column.id === "review" | ||
| ? t("board.column.review") | ||
| : t("board.column.done"); |
Greptile SummaryThis PR introduces app-level i18n with English as default and Simplified Chinese as a switchable locale, covering all UI surfaces (board, task cards, top bar, settings, detail panels, Cline provider dialogs). It also adds
Confidence Score: 4/5Safe to merge. The i18n infrastructure is well-structured and the only behavioral change outside UI strings is the automatic insertion of The implementation is thorough: TypeScript enforces translation completeness, board-column.tsx (implicit "done" fallback for unknown column IDs) and task-create-dialog.tsx / task-inline-create-card.tsx (positional fileImageHint sentence fragments)
|
| Filename | Overview |
|---|---|
| web-ui/src/i18n/translations.ts | Adds 400+ translation keys for English and Chinese. TypeScript satisfies Record<TranslationKey, string> on the zh object guarantees compile-time completeness. |
| web-ui/src/i18n/i18n-context.tsx | New React context providing language, setLanguage, and t. Language preference is persisted to localStorage. useI18n returns a safe English fallback when used outside the provider. |
| web-ui/src/components/language-switcher.tsx | New popover component for switching between EN and ZH. Correctly uses useI18n and renders in the top bar. |
| web-ui/src/components/board-column.tsx | Column title now derived from column.id via i18n. The "trash" column ID correctly falls through to board.column.done, but any future/unknown column ID would also silently display "Done". |
| web-ui/src/components/board-card.tsx | Localizes all status text, labels, and aria-labels. getCardSessionActivity now takes t as a parameter; useMemo deps updated accordingly. |
| src/terminal/agent-session-adapters.ts | Adds --dangerously-bypass-hook-trust before any resume/fork subcommand when Kanban hooks are configured. Guards against double-insertion and covered by updated test. |
| web-ui/src/components/task-trash-warning-dialog.tsx | Replaces hardcoded strings with t() calls. task.changedFiles correctly called with count and fileLabel, producing well-formed sentences in both locales. |
| web-ui/src/components/task-create-dialog.tsx | Fully localized. AUTO_REVIEW_MODE_OPTIONS correctly simplified to TaskAutoReviewMode[]. The task.fileImageHint.* three-part pattern works but is order-dependent. |
| web-ui/src/storage/local-storage-store.ts | Adds Language = "kanban.language" enum value. No other changes. |
| web-ui/src/components/top-bar.tsx | All labels and aria-attributes localized. LanguageSwitcher added. Git tooltip pluralization uses correct deps. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
LS[localStorage kanban.language] -->|read on mount| IP[I18nProvider useState language]
IP -->|useCallback t| CTX[I18nContext]
IP -->|useEffect| LS2[write localStorage set document.lang]
CTX -->|useI18n| COMP[All UI Components]
COMP -->|t key values| TR[translate function]
TR -->|lookup| EN[en translations]
TR -->|lookup| ZH[zh translations]
EN -->|template string| OUT[Rendered string]
ZH -->|template string| OUT
LSUI[LanguageSwitcher in TopBar] -->|setLanguage| IP
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 2
web-ui/src/components/board-column.tsx:83-91
**Implicit fallback for "trash" and future column IDs**
The ternary chain falls through to `t("board.column.done")` for any `column.id` that isn't "backlog", "in_progress", or "review". This works today because `"trash"` is the Done column in this codebase, but the mapping is implicit and will silently render "Done" for any column ID added in the future. Previously, `column.title` from the data model handled this gracefully. Consider explicitly handling the `"trash"` case or using a lookup map (`columnTitleKeys[column.id] ?? column.title`) so unknown IDs don't silently fall back to "Done".
### Issue 2 of 2
web-ui/src/components/task-create-dialog.tsx:444-460
**Sentence-fragment translation pattern is fragile across locales**
The hint text is split into three positional keys (`task.fileImageHint.beforeFile`, `task.fileImageHint.afterFile`, `task.fileImageHint.afterShortcut`) that are interleaved with two JSX `<code>` elements in a fixed order. This works for English and Simplified Chinese, but the same pattern appears in `task-inline-create-card.tsx`. Any locale whose grammar requires a different word order around the inline code elements would require a code change to reorder the JSX nodes. A single key with named string placeholders, or a light rich-text helper, would make future locale additions self-contained.
Reviews (1): Last reviewed commit: "fix: bypass Codex hook trust prompts" | Re-trigger Greptile
| const createTaskButtonText = ( | ||
| <span className="inline-flex items-center gap-1.5"> | ||
| <span>Create task</span> | ||
| <span>{t("board.createTask")}</span> | ||
| <span aria-hidden className="text-text-secondary"> | ||
| (c) | ||
| </span> | ||
| </span> | ||
| ); | ||
| const columnTitle = |
There was a problem hiding this comment.
Implicit fallback for "trash" and future column IDs
The ternary chain falls through to t("board.column.done") for any column.id that isn't "backlog", "in_progress", or "review". This works today because "trash" is the Done column in this codebase, but the mapping is implicit and will silently render "Done" for any column ID added in the future. Previously, column.title from the data model handled this gracefully. Consider explicitly handling the "trash" case or using a lookup map (columnTitleKeys[column.id] ?? column.title) so unknown IDs don't silently fall back to "Done".
Prompt To Fix With AI
This is a comment left during a code review.
Path: web-ui/src/components/board-column.tsx
Line: 83-91
Comment:
**Implicit fallback for "trash" and future column IDs**
The ternary chain falls through to `t("board.column.done")` for any `column.id` that isn't "backlog", "in_progress", or "review". This works today because `"trash"` is the Done column in this codebase, but the mapping is implicit and will silently render "Done" for any column ID added in the future. Previously, `column.title` from the data model handled this gracefully. Consider explicitly handling the `"trash"` case or using a lookup map (`columnTitleKeys[column.id] ?? column.title`) so unknown IDs don't silently fall back to "Done".
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| onImagesChange={onImagesChange} | ||
| onSubmit={handleCreateSingle} | ||
| onSubmitAndStart={() => handleRunSingleStartAction("start")} | ||
| placeholder="Describe the task..." | ||
| placeholder={t("task.describePlaceholder")} | ||
| autoFocus | ||
| workspaceId={workspaceId} | ||
| showAttachImageButton={false} | ||
| /> | ||
| <div className="flex items-center justify-between mt-1.5"> | ||
| <p className="text-[11px] text-text-tertiary"> | ||
| Use <code className="rounded bg-surface-3 px-1 py-px font-mono text-[11px]">@file</code> to | ||
| reference files. Drag and drop or{" "} | ||
| {t("task.fileImageHint.beforeFile")}{" "} | ||
| <code className="rounded bg-surface-3 px-1 py-px font-mono text-[11px]">@file</code>{" "} | ||
| {t("task.fileImageHint.afterFile")}{" "} | ||
| <code className="rounded bg-surface-3 px-1 py-px font-mono text-[11px]"> | ||
| {pasteShortcutLabel} | ||
| </code>{" "} | ||
| to add images. | ||
| {t("task.fileImageHint.afterShortcut")} |
There was a problem hiding this comment.
Sentence-fragment translation pattern is fragile across locales
The hint text is split into three positional keys (task.fileImageHint.beforeFile, task.fileImageHint.afterFile, task.fileImageHint.afterShortcut) that are interleaved with two JSX <code> elements in a fixed order. This works for English and Simplified Chinese, but the same pattern appears in task-inline-create-card.tsx. Any locale whose grammar requires a different word order around the inline code elements would require a code change to reorder the JSX nodes. A single key with named string placeholders, or a light rich-text helper, would make future locale additions self-contained.
Prompt To Fix With AI
This is a comment left during a code review.
Path: web-ui/src/components/task-create-dialog.tsx
Line: 444-460
Comment:
**Sentence-fragment translation pattern is fragile across locales**
The hint text is split into three positional keys (`task.fileImageHint.beforeFile`, `task.fileImageHint.afterFile`, `task.fileImageHint.afterShortcut`) that are interleaved with two JSX `<code>` elements in a fixed order. This works for English and Simplified Chinese, but the same pattern appears in `task-inline-create-card.tsx`. Any locale whose grammar requires a different word order around the inline code elements would require a code change to reorder the JSX nodes. A single key with named string placeholders, or a light rich-text helper, would make future locale additions self-contained.
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
9a4b98e to
32c5161
Compare
Summary
Testing