Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .codev/glossary/vi.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# vi Glossary

## Sources

- Fork decision (2026-08-23): keep common developer terms in English; translate UI chrome fully.

## Do Not Translate (Locale Additions)

- `OpenCode`, `OpenCode Zen`, `OpenCode Go`, `CoDev`, `CoDev Hub` (product names; preserve casing)
- `CLI`, `TUI`, `MCP`, `OAuth`, `LSP`, `API`
- Common developer terms stay in English: `session`, `prompt`, `token`, `commit`, `branch`, `worktree`, `agent`, `model`, `sandbox`, `cache`, `provider`, `terminal`, `repo`/`repository`, `pull request`, `merge`, `rebase`, `diff`, `checkpoint`, `webview`
- Commands, flags, file paths, keybinds, and code literals (keep exactly as written)

## Preferred Terms

- Settings → "Cài đặt"
- Appearance → "Giao diện"
- Theme → "Chủ đề" (distinct from Appearance; both appear as sibling settings rows)
- Color scheme → "chế độ màu"
- Context (LLM context window) → keep "context" in English
- Steer (mid-run follow-up) → "Điều hướng ngay"
- Language → "Ngôn ngữ"
- Update → "Cập nhật"
- Install → "Cài đặt"
- Download → "Tải xuống"
- Upload → "Tải lên"
- Delete → "Xóa"
- Cancel → "Hủy"
- Retry → "Thử lại"
- Undo/Redo → "Hoàn tác"/"Làm lại"
- Copy/Paste → "Sao chép"/"Dán"
- Share → "Chia sẻ"
- Search → "Tìm kiếm"
- Permission(s) → "quyền"
- Tool → "công cụ" in flowing prose descriptions; keep "tool" in compact technical labels (e.g. "Lệnh gọi tool")
- Error → "lỗi"
- Workspace → "workspace" (keep English; recurring identifier in the product)
- File → "tệp"; Folder/Directory → "thư mục"
- Sign in / Log in → "Đăng nhập"; Sign out → "Đăng xuất"

## Guidance

- Address the user as "bạn"; keep tone clear, direct, and friendly in UI labels.
- Use Vietnamese sentence casing: capitalize only the first word of a label or sentence (and proper nouns) — no English-style Title Case.
- Prefer natural Vietnamese phrasing over literal translation; keep labels short.
- Keep `{{placeholder}}` tokens exactly as in the English source.
- Docs links may point at the Vietnamese docs locale (`/vi`), which is published.

## Avoid

- Avoid translating product and protocol names that are fixed identifiers.
- Avoid mixing multiple Vietnamese terms for the same recurring UI action once a preferred term is established (e.g. always "Hủy" for Cancel, never "Bỏ qua").
- Avoid stiff, overly formal register ("quý khách", "vui lòng" everywhere) — one "vui lòng" per message at most, only where English says "please".
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,19 @@ file is the human-readable summary of what changed and why.

## Unreleased

### Language

- **CoDev speaks Vietnamese** — the desktop and web app, the shared UI
components, the console, and the stats site are now fully translated into
Vietnamese alongside English and Spanish. The app follows your system
language automatically (`vi` in your browser or OS picks it up) and "Tiếng
Việt" appears in every language picker; docs links for Vietnamese users
point at the published Vietnamese docs. Terminology follows the convention
Vietnamese developers expect: UI chrome is translated, while terms like
session, prompt, token, commit, branch, and agent stay in English (the
glossary lives in `.codev/glossary/vi.md`).
([#307](https://github.com/quickbeard/codev-code/pull/307))

### Privacy

- **CoDev no longer asks for access to your Downloads, Photos, Desktop, or
Expand Down
5 changes: 3 additions & 2 deletions DIVERGENCES.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,8 +266,9 @@ from the squash message first, or use a plain merge.
| `sdks/vscode/package.json`, `sdks/vscode/src/extension.ts`, `sdks/vscode/README.md` | `name` `opencode` → `codev-code` (the extension ID is `publisher.name`); `displayName` "opencode" → "CoDev Code"; `publisher` `sst-dev` → `CoDev` (PLACEHOLDER — no such Marketplace publisher is registered; the mixed-case `CoDev` also isn't a valid Marketplace publisher id, which must be lowercase — fine for the sideloaded `.vsix` the fork ships, but replace with the real fork-owned lowercase publisher id before any `vsce publish`, and keep it in sync with the CLI auto-install target); `description` "opencode for VS Code" → "CoDev Code for VS Code"; `icon` `images/icon.png` is a fork-local generated PNG of the CoDev "c" badge (brand red `#ee0033` — the "dev" half of the wordmark, see `packages/tui/src/logo.ts` — on `#0a0a0a`), with `images/icon.svg` as its source of record — replacing the earlier symlink to the shared `packages/identity` bracket mark; the two per-theme command `icon` objects (`{light,dark}` → `images/button-{dark,light}.svg`) collapse to a single `"images/button.svg"` string and `extension.ts`'s terminal `iconPath` object collapses to a single `Uri`, both pointing at a fork-local transparent brand-red `#ee0033` "c" glyph that reads on light+dark themes (replacing the two `packages/identity` bracket-mark symlinks, now deleted); `repository.url` → `github.com/quickbeard/codev-code`; command IDs `opencode.*` → `codev.*` across `contributes.commands`/`menus`/`keybindings` and the `registerCommand` calls; command/keybinding titles "Open opencode…"/"Run opencode"/"opencode: Insert At-Mentioned" → "Open CoDev Code…"/"Run CoDev Code"/"CoDev Code: Insert At-Mentioned" (keys unchanged); `TERMINAL_NAME` `opencode` → `codev` and the terminal launches `codev --port <port>`; scripts gain `"vsix"` (`vsce package --no-dependencies --skip-license -o dist/`) with `@vscode/vsce` added as a pinned devDependency; README body rebranded and trimmed to the details-panel essentials — CoDev Code intro linking the fork repo, `npm install -g codev-code` prerequisite, and feature prose only; upstream's Support and Development/Making-Changes sections are dropped (they're contributor-facing, not end-user extension description) | The fork ships this extension as an installable `.vsix`, so every user-visible surface — palette titles, command IDs, terminal name, launched binary, details-panel README — must say codev, and the launched binary must be the fork's `codev` CLI. Distinct `codev.*` IDs also avoid clashing with upstream's extension if both are installed. `--no-dependencies` because the bundle is self-contained (no runtime deps, bun-managed lockfile); `--skip-license` because `sdks/vscode` has no LICENSE file and vsce aborts without it. | Keep our name + displayName + publisher + repository + description, command IDs + titles, `TERMINAL_NAME`, the `codev --port` launch line, the `vsix` script + `@vscode/vsce` devDependency, and the trimmed README (branding + dropped Support/Development sections); take upstream's extension-logic and manifest-structure changes. |
| `packages/opencode/src/ide/index.ts` | IDE auto-install target `sst-dev.opencode` → `CoDev.codev-code` (`publisher.name` from `sdks/vscode/package.json`) | The CLI must install the fork's extension ID (`publisher.name` from `sdks/vscode/package.json`), not upstream's Marketplace listing. `Ide.install` currently has no runtime caller (only its test imports the module), and the install only resolves once the extension is actually published — until then the local `.vsix` is the supported install path. | Keep our extension ID (and re-sync it if the registered publisher id changes); take upstream's install/detection logic changes. |
| Env-var rename, repo-wide (~230 files): both flag registries (`packages/core/src/flag/flag.ts`, `packages/opencode/src/effect/runtime-flags.ts`), every `process.env`/`Bun.env` read+set site across `packages/*`, `sdks/vscode`, `infra/enterprise.ts`, `nix/opencode.nix`, `turbo.json`, `.github/workflows/test.yml`, test preloads/fixtures/snapshots, docs (en + es), `SECURITY.md`, specs | `OPENCODE_*` env vars → `CODEV_*` (hard cutover, no legacy fallback), including the bundler define identifiers (`CODEV_VERSION`, `CODEV_CHANNEL`, `CODEV_MODELS_DEV`, `CODEV_LIBC`, `CODEV_CLI_NAME`, `CODEV_WORKER_PATH`), `VITE_OPENCODE_*` → `VITE_CODEV_*`, `import.meta.env.OPENCODE_CHANNEL` → `CODEV_CHANNEL`, the VSCode extension's `OPENCODE_CALLER`/`_EXTENSION_OPENCODE_PORT`, and the bare `OPENCODE=1` marker → `CODEV=1` | The fork's env surface uses the fork's name (reverses the earlier "internal names stay upstream" decision for env vars). NOT renamed: `__OPENCODE_PHOTON_WASM_PATH` and `__OPENCODE__` globals (not env vars; the photon name is baked into `patches/@silvia-odwyer%2Fphoton-node@0.3.4.patch`), `patches/install-korean-ime-fix.sh` script-local shell vars, upstream secret names in workflow-deletion notes, lowercase identifiers (`@opencode-ai/*`, URLs, provider id) | Keep our names; rename any new upstream `OPENCODE_*` env var (or define/`VITE_` name) to `CODEV_*` when syncing. |
| Locale dicts in `packages/{ui,app}/src/i18n/`, `packages/desktop/src/renderer/i18n/`, `packages/console/app/src/i18n/`, `packages/stats/app/src/i18n/`, `packages/web/src/content/i18n/`; every translated docs dir in `packages/web/src/content/docs/` | Deleted every locale except `en`/`es` (incl. `packages/app/src/i18n/parity.test.ts`) | Fork ships English + Spanish only. | Keep deleted (`git rm` the upstream side of modify/delete conflicts); also delete any locale files upstream adds. |
| `packages/app/src/context/language.tsx`, `packages/app/src/entry.tsx`, `packages/desktop/src/renderer/i18n/index.ts`, `packages/console/app/src/i18n/index.ts`, `packages/console/app/src/lib/language.ts`, `packages/console/app/src/component/footer.tsx`, `packages/stats/app/src/i18n.ts`, `packages/enterprise/src/app.tsx`, `packages/app/e2e/regression/session-timeline-locale-projection.spec.ts` | Locale registries trimmed to `en` + `es`: `Locale` types, LOCALES/LABEL/TAG/DOCS maps, dict loaders, accept-language matchers; enterprise/entry `zh` special cases now use `es`; console footer always links Discord (Feishu was zh-only); e2e locale-projection spec tests `es` ("Explorado") instead of de/ar | Fork ships English + Spanish only. | Keep our trimmed lists; take upstream's structural changes and re-trim any new locale entries. |
| Locale dicts in `packages/{ui,app}/src/i18n/`, `packages/desktop/src/renderer/i18n/`, `packages/console/app/src/i18n/`, `packages/stats/app/src/i18n/`, `packages/web/src/content/i18n/`; every translated docs dir in `packages/web/src/content/docs/` | Deleted every locale except `en`/`es` (incl. `packages/app/src/i18n/parity.test.ts`) | Fork ships English + Spanish, plus fork-authored Vietnamese (`vi.ts` dicts are new fork files — see the Vietnamese row below); every other upstream locale stays deleted. | Keep deleted (`git rm` the upstream side of modify/delete conflicts); also delete any locale files upstream adds — except a `vi` locale, which the fork owns (prefer our translations and reconcile key-by-key). |
| `packages/app/src/context/language.tsx`, `packages/app/src/entry.tsx`, `packages/desktop/src/renderer/i18n/index.ts`, `packages/console/app/src/i18n/index.ts`, `packages/console/app/src/lib/language.ts`, `packages/console/app/src/component/footer.tsx`, `packages/stats/app/src/i18n.ts`, `packages/enterprise/src/app.tsx`, `packages/app/e2e/regression/session-timeline-locale-projection.spec.ts` | Locale registries trimmed to `en` + `es`, then extended with fork-added `vi`: `Locale` types, LOCALES/LABEL/TAG/DOCS maps (console maps `vi` to the published `/docs/vi` segment), dict loaders, accept-language matchers; enterprise/entry `zh` special cases now use `es` (both also match `vi`); console footer always links Discord (Feishu was zh-only); e2e locale-projection spec tests `es` ("Explorado") instead of de/ar | Fork ships English + Spanish + fork-authored Vietnamese. | Keep our locale lists (including `vi`); take upstream's structural changes, re-trim any new upstream locale entries, and re-apply the `vi` entries alongside them. |
| `packages/app/src/i18n/{en,es}.ts` (`language.vi` label), `script/translate-app.ts`, `script/translate-app.test.ts` | Added Vietnamese (`vi`) as a fork-only third locale: autonym label `"language.vi": "Tiếng Việt"` inserted into the en/es app dicts' language list; `vi`/"Vietnamese" registered in the translate script's locale tables (desktop surface included — only `th`/`tr` are excluded); `glossaryFile` repointed `.opencode/glossary/` → `.codev/glossary/` (test expectations updated) so glossaries actually load from the fork's renamed config dir. The dictionaries themselves — `packages/{app,ui}/src/i18n/vi.ts`, `packages/desktop/src/renderer/i18n/vi.ts`, `packages/console/app/src/i18n/vi.ts`, `packages/stats/app/src/i18n/vi.ts` — and `.codev/glossary/vi.md` are fork-only new files (not tracked per policy). | Vietnamese docs are published on the fork's docs site (`…/codev/vi/docs`), and the product UI should match. Upstream has no `vi` locale, so the new files never collide with upstream content. | Keep our `vi` entries and the `.codev` glossary path; take upstream's surrounding script/dict changes. If upstream ever adds a `vi` locale, prefer our translations and reconcile key-by-key. |
| `packages/tui/src/component/prompt/autocomplete.tsx`, `packages/opencode/src/cli/cmd/run/footer.prompt.tsx`, `packages/core/src/filesystem/search.ts`, `packages/tui/test/fixture/tui-sdk.ts`, `packages/opencode/test/cli/run/footer.view.test.tsx`, `packages/core/test/filesystem/search.test.ts` | `@` mention autocomplete lists file/directory results before references/agents/MCP resources; TUI and run-CLI menus cap the leading files so up to 3 non-file matches stay in the first visible page. TUI and run-CLI file resources track menu visibility so opening on an empty query fetches (previously nothing refetched until the query changed); core's ripgrep-fallback `find` returns its first entries for an empty query to mirror fff. Tests: tui-sdk fixture's `FetchHandler` gains an optional `method` arg, `renderFooter` accepts `findFiles`/`agents` overrides + a mention-menu ordering test, core search tests cover the fallback's empty query (fork-only test file: `packages/tui/test/prompt-autocomplete.test.tsx`). The legacy web prompt originally carried the same reorder (fork-only `prompt-input/at-options.ts`); it was deleted with the legacy prompt input (see the removal row below), and the V2 composer's files-first ordering is tracked in its own `session-ui` row (PR #289). | Files are the dominant `@` use case; they should rank first and appear on a bare `@`. | Keep our ordering/reserve logic, resource source tracking, empty-query branch, fixture/test additions; take upstream's surrounding changes. |
| `packages/slack/.gitignore`, `sdks/vscode/.gitignore` | Added the missing trailing newline (content unchanged) | Upstream's copies end without a newline; POSIX text files should end with one, and tools that concatenate or append to these files misbehave without it. | Whitespace-only: keep the trailing newline; take upstream's entries. |
| `packages/tui/src/config/keybind.ts`, `packages/tui/src/component/prompt/index.tsx`, `packages/tui/src/app.tsx` | Permission-mode indicator + shift+tab cycle. keybind.ts: added `permission_mode_cycle: keybind("shift+tab", ...)` + its `permission.mode` CommandMap entry; `agent_cycle_reverse` default `"shift+tab"` → `"none"` (tab still cycles agents forward; previous-agent stays rebindable). prompt/index.tsx: the footer badge next to the agent name always renders in prompt mode — gray `manual` (`theme.textMuted`) in normal mode, red `auto` (`theme.error`) in auto mode (upstream only showed gray `auto`); the footer's shortcut hints gain a `shift+tab modes` text (via a `modeShortcut` `useCommandShortcut("permission.mode")` memo) left of `tab agents`, in the same `Show` gate (see the hints-and-usage row above for when that gate renders). app.tsx: `"permission.mode"` added to `appBindingCommands` so the binding is gathered. | The permission mode is always visible (manual/auto) with auto in red as a danger signal, and shift+tab toggles it without opening the palette. | Keep the `permission_mode_cycle` definition + mapping, the `agent_cycle_reverse` `"none"` default, the badge `Show` block, the `modeShortcut` memo + hint text, and the `appBindingCommands` entry; take upstream's surrounding changes. If upstream reworks the auto badge or rebinds shift+tab, re-apply on top and re-audit the shift+tab conflict. |
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { expect, test } from "@playwright/test"
import { assistantMessage, setupTimeline, toolPart, userMessage } from "../performance/timeline-stability/fixture"

for (const profile of [{ locale: "es", label: "Explorado" }] as const) {
for (const profile of [
{ locale: "es", label: "Explorado" },
{ locale: "vi", label: "Đã khám phá" },
] as const) {
test(`projects translated context status in ${profile.locale}`, async ({ page }) => {
const ids = [`prt_locale_${profile.locale}_01_read`, `prt_locale_${profile.locale}_02_glob`]
await setupTimeline(page, {
Expand Down
8 changes: 6 additions & 2 deletions packages/app/src/context/language.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Persist, persisted } from "@/utils/persist"
import { dict as en } from "@/i18n/en"
import { dict as uiEn } from "@opencode-ai/ui/i18n/en"

export type Locale = "en" | "es"
export type Locale = "en" | "es" | "vi"

type RawDictionary = typeof en & typeof uiEn
type Dictionary = i18n.Flatten<RawDictionary>
Expand All @@ -16,16 +16,18 @@ function cookie(locale: Locale) {
return `oc_locale=${encodeURIComponent(locale)}; Path=/; Max-Age=31536000; SameSite=Lax`
}

const LOCALES: readonly Locale[] = ["en", "es"]
const LOCALES: readonly Locale[] = ["en", "es", "vi"]

const INTL: Record<Locale, string> = {
en: "en",
es: "es",
vi: "vi",
}

const LABEL_KEY: Record<Locale, keyof Dictionary> = {
en: "language.en",
es: "language.es",
vi: "language.vi",
}

const base = i18n.flatten({ ...en, ...uiEn })
Expand All @@ -36,6 +38,7 @@ const merge = (app: Promise<Source>, ui: Promise<Source>) =>

const loaders: Record<Exclude<Locale, "en">, () => Promise<Dictionary>> = {
es: () => merge(import("@/i18n/es"), import("@opencode-ai/ui/i18n/es")),
vi: () => merge(import("@/i18n/vi"), import("@opencode-ai/ui/i18n/vi")),
}

function loadDict(locale: Locale) {
Expand All @@ -56,6 +59,7 @@ export function loadLocaleDict(locale: Locale) {
const localeMatchers: Array<{ locale: Locale; match: (language: string) => boolean }> = [
{ locale: "en", match: (language) => language.startsWith("en") },
{ locale: "es", match: (language) => language.startsWith("es") },
{ locale: "vi", match: (language) => language.startsWith("vi") },
]

function detectLocale(): Locale {
Expand Down
6 changes: 5 additions & 1 deletion packages/app/src/entry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { AppBaseProviders, AppInterface } from "@/app"
import { type Platform, PlatformProvider } from "@/context/platform"
import { dict as en } from "@/i18n/en"
import { dict as es } from "@/i18n/es"
import { dict as vi } from "@/i18n/vi"
import { handleNotificationClick } from "@/utils/notification-click"
import { authFromToken } from "@/utils/server"
import pkg from "../package.json"
Expand All @@ -19,14 +20,17 @@ const getLocale = () => {
for (const language of languages) {
if (!language) continue
if (language.toLowerCase().startsWith("es")) return "es" as const
if (language.toLowerCase().startsWith("vi")) return "vi" as const
}
return "en" as const
}

const getRootNotFoundError = () => {
const key = "error.dev.rootNotFound" as const
const locale = getLocale()
return locale === "es" ? (es[key] ?? en[key]) : en[key]
if (locale === "es") return es[key] ?? en[key]
if (locale === "vi") return vi[key] ?? en[key]
return en[key]
}

const getStorage = (key: string) => {
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,7 @@ export const dict = {
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.vi": "Tiếng Việt",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/i18n/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -590,6 +590,7 @@ export const dict = {
"language.ko": "한국어",
"language.de": "Deutsch",
"language.es": "Español",
"language.vi": "Tiếng Việt",
"language.fr": "Français",
"language.da": "Dansk",
"language.ja": "日本語",
Expand Down
Loading
Loading