Claude Skill

i18n-parity

MANDATORY for every coding agent and contributor touching localized content — keep all five localization surfaces (dashboard UI keys, wiki page, mirrored READMEs, locale-aware formatting, language switchers) in parity across every supported language. Use automatically (without be

LLM Mart · 0 points · 8 views 0 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download hoangsonww-claude-code-agent-monitor-.agents_skills_i18n-parity-83d4df5.zip · 14 KB
Part of hoangsonww/claude-code-agent-monitor — 86 skills

Install

skills CLI npx skills add https://github.com/hoangsonww/Claude-Code-Agent-Monitor/tree/master/.agents/skills/i18n-parity
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install hoangsonww-claude-code-agent-monitor@llmmart
Git git clone https://github.com/hoangsonww/Claude-Code-Agent-Monitor.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole hoangsonww/claude-code-agent-monitor collection as a plugin from our marketplace. Git is the plain clone.

Skill manifest

Mirror. The canonical copy of this skill — and the i18n-audit.sh script it tells you to run — live at .claude/skills/i18n-parity/. Keep the two in sync; edit the canonical copy first.

i18n Parity

This repository is localized on five independent surfaces, each with its own mechanism. A change that lands on one and not the others leaves the product half-translated: the English falls through, and the gap is invisible to anyone working in English. This skill states the invariant, maps every surface to the exact files, and gives one command that proves parity.

The invariant: English is the source of truth on every surface, and no change is done until every supported language carries it in the same PR. A fallback to English is a safety net, never a completed translation.

Supported languages are declared in one place — supportedLngs in client/src/i18n/index.ts. Today: en, zh, vi, ko, es. Everything below derives from that list; the audit script reads it rather than hard-coding.

The five surfaces

# Surface English source of truth Translations live in Automated gate
1 Dashboard UI client/src/i18n/locales/en/*.json client/src/i18n/locales/<xx>/*.json (same 15 namespaces) client/src/i18n/__tests__/i18n.test.ts — key, type, and interpolation-token parity
2 Wiki page English text in the wiki/index.html DOM wiki/script.js (T, ATTRIBUTE_TRANSLATIONS, META, languageLabels, the two language ladders) + wiki/i18n-content.js (window.__WIKI_CONTENT_I18N, both the body bundles and plain) client/tests/wiki-i18n.test.ts — live-DOM prose coverage, inline-tag preservation, block-length budgets, asset-version sync
3 Mirrored READMEs README.md README-CN.md (zh), README-VN.md (vi), README-KO.md (ko), README-ES.md (es) partial — scripts/i18n-audit.sh (existence, heading count, cross-links) and server/__tests__/plugins-marketplace.test.js (documented counts); prose parity is review-only
4 Language switchers — client/src/components/Sidebar.tsx, client/src/lib/paletteCommands.ts, the two .lang-select-menu blocks in wiki/index.html, nav.json languageNames / languageShort scripts/i18n-audit.sh
5 Locale-aware formatting — client/src/lib/format.ts (SupportedLanguage union, getCurrentLanguage() whitelist, getCurrentLocale() BCP-47 map) client/src/lib/__tests__/format.test.ts

What is deliberately NOT localized

Verified against the tree — do not go looking for translation hooks in these, and do not add them without being asked:

  • The root landing page index.html has no i18n layer. Do not add data-lang markup; put localized long-form content in the wiki instead. Its one language-aware element is the Languages (en/zh/…) stat label, which just enumerates the codes.
  • client/index.html is an English shell: <html lang="en">, og:locale=en_US, and English <title>/meta. The React app never reassigns document.documentElement.lang when the user switches language — a known gap, not something a translation PR is expected to fix.
  • The CLI (bin/ccam.js), the MCP server (mcp/), the Express server (server/), the desktop shell (desktop/), the VS Code extension, and the statusline contain no i18n wiring at all. Their output is English.
  • client/src/lib/event-summary.ts and event-grouping.ts build tool-event headlines and bullets from English template literals (Last message: …, 3 lines stdout, 2 matches). Their MODULE_GUIDE boilerplate claims strings belong in i18n JSON, but neither file imports i18next. They sit outside the key system today; do not "fix" them as part of a localization change.
  • Number and date formatting is only locale-aware where the format.ts helpers are used. Most components call toLocaleString() / toLocaleString(undefined, …) directly, which follows the browser locale rather than the chosen UI language. In new code prefer getCurrentLocale() from format.ts; leave existing call sites alone unless asked.

Workflow A — you changed content (the common case)

Find what you touched in the left column and ship everything in the right column in the same PR.

You changed You must also do
Added/renamed a UI string or i18n key Add the key to en and every other locale in the same namespace file. Same key path, same value type, same {{interpolation}} tokens.
Added a new namespace (new *.json) Create it for every locale, then register the imports, the resources entry per language, and the ns array in client/src/i18n/index.ts.
Added user-visible wiki text in wiki/index.html Follow .claude/rules/wiki-i18n.md: scannable layer (the PLAIN selector set — .logo-sub, .section-label, .nav-section, .nav-empty, .stat-label, .t-label, h2/h3/h4, th, .hero-desc, plus .nav-link / .hero-badge trailing text nodes) → T in wiki/script.js; body prose (the HTML_SEL set — p, li, td, th, captions, .callout-body > strong, .route-desc, footer) → wiki/i18n-content.js keyed by whitespace-normalized innerHTML; new alt/aria-label/title/placeholder → ATTRIBUTE_TRANSLATIONS. Then bump CACHE_NAME in wiki/sw.js and the matching ?v= query strings.
Edited a section of README.md Mirror the same edit at the corresponding section of README-CN.md, README-VN.md, README-KO.md, and README-ES.md. All four, every time.
Changed behavior that the README/wiki document (env var, event type, route, CLI command, feature) Run the update-project-docs (.claude/skills/update-project-docs/SKILL.md) skill — it owns the change→docs mapping — then come back here for the translation propagation it triggers.
Changed a documented count (plugins, skills, namespaces, languages) The count is repeated across all five READMEs, ARCHITECTURE.md, docs/*.md, index.html, wiki/index.html, wiki/i18n-content.js, and asserted in server/__tests__/plugins-marketplace.test.js. Grep the old number repo-wide; update every hit.

Workflow B — adding a new language

This is a large, exact, mechanical change. Read references/new-language-checklist.md and work through it top to bottom — it lists every file, in dependency order, with the exact edit for each.

The three things contributors most often ship incomplete, stated up front:

  1. The README mirror must be complete. README-<XX>.md is a full mirror of README.md — every section, every table row, every code block, every mermaid diagram, in the same order. Do not summarize, do not drop "less important" sections, do not stop halfway. Diff the heading list against README.md before you open the PR — the audit script compares heading counts, which catches a truncated mirror but not a reordered or silently condensed one.
  2. Every app key must be translated. All 15 namespaces × every key. The parity test fails on a missing key, but it passes on a key you copied over in English — so read your diff for untranslated leftovers.
  3. The wiki must be translated completely, not just the headings. That means a full <xx> bundle in wiki/i18n-content.js (thousands of body strings), a full <xx> block in T, META, and every entry of ATTRIBUTE_TRANSLATIONS in wiki/script.js. A locale that only fills T renders a page with translated headings over English paragraphs, which is worse than English.

What stays in English (all surfaces)

Never translate: code inside <code>/backticks, commands, file and directory paths, URLs, env-var names, HTTP methods and status codes, CLI flags, code identifiers, numbers with units, brand and product names (Claude Code, MCP, Codex), Claude Code hook event names (PreToolUse, Stop, …), and Claude Code tool names (Bash, Agent, Read, Edit). Translate only the prose around them. A block that is entirely code or identifiers needs no wiki entry — it correctly falls back to English.

Two different things are spelled Agent. The Claude Code tool named Agent (alongside Bash, Read, Edit) is an identifier and stays literal in every locale, everywhere it appears — hook-event tables, tool lists, event names. The UI noun for an agent (common:agent / common:subagent) is product vocabulary with a per-locale contract, and only that noun has the Spanish exception below. Never carry the exception into the tool name.

The UI noun's contract, asserted by client/src/i18n/__tests__/i18n.test.ts: zh, vi, and ko keep Agent / Subagent literal; es renders them agente / subagente. A new locale must decide this explicitly and add its row to that test.

Terminology, per-locale conventions, and the shared glossary live in references/translation-style.md. Read it before translating; drifting terminology across pages is the most common review comment on localization PRs.

Two traps worth knowing before you edit the wiki

  1. client/tests/wiki-i18n.test.ts parses wiki/script.js by exact source markers. It slices the file between literal strings — " const T = " … "\n\n const PLAIN", " const ATTRIBUTE_TRANSLATIONS = " … "\n const ATTR", " const META = " … "\n const trH" — and evals what it finds. Renaming, reordering, or re-indenting those declarations breaks the test with a confusing error. Add locales inside the existing objects; leave the declaration lines alone.
  2. The wiki's chosen language lives in localStorage["wiki-lang"], not a URL parameter. There is no ?lang= support. First visit falls back to a navigator.language prefix ladder in wiki/script.js. To preview a locale, use the switcher, or run localStorage.setItem("wiki-lang", "<xx>") in the console and reload.

This skill is mirrored for every agent

The canonical copy — and both scripts — live at .claude/skills/i18n-parity/. It is mirrored, with links rewritten to repo-root-relative paths and an agents/openai.yaml interface added, to:

  • .agents/skills/i18n-parity/ (the shared/OpenAI skill tree)
  • .codex/skills/i18n-parity/ (Codex)

Editing this skill means regenerating the mirrors — edit the canonical copy, then run:

bash .claude/skills/i18n-parity/scripts/sync-agent-mirrors.sh

i18n-audit.sh runs that script in --check mode, so a stale mirror is a reported gap rather than silent drift. The mirrors carry no scripts: they point back at the canonical ones by repo-root path.

Verify (do not skip)

# 1. Cross-surface parity: locale sets, namespace files, key parity, switcher
#    entries, wiki bundles, README mirrors and cross-links, agent-skill mirrors.
bash .claude/skills/i18n-parity/scripts/i18n-audit.sh

# 2. UI key/type/interpolation parity + locale formatting
npm run test:client

# 3. Wiki live-DOM coverage, inline tags, metadata, cache versions
cd client && npx vitest run tests/wiki-i18n.test.ts && cd ..

# 4. Documented counts asserted against the source tree
npm run test:server

# 5. The static wiki files and locale JSON are Prettier-managed
npm run format

i18n-audit.sh exits non-zero and names the exact file and locale for every gap. Every check is a structural one — a named thing is present or it is not — with a single exception: the wiki/i18n-content.js bundle-size check is a stub detector (it flags a locale holding under 60% of the largest bundle's entries). Exact per-string wiki coverage is asserted by client/tests/wiki-i18n.test.ts, which walks the live DOM.

State explicitly which surfaces you updated and which you intentionally skipped (with the reason), per the repo's verification policy in CLAUDE.md.

Tips

  • Write the English first and get it right, on all surfaces, before translating anything. Every other locale is derived from it; re-translating because the English moved is the biggest waste in this workflow.
  • To find where a string already lives: grep -rn "<neighbouring English text>" client/src/i18n/locales/en wiki/i18n-content.js wiki/script.js.
  • When adding a language, one locale per subagent is fine for the wiki body bundle (it is large), but keep the README mirror with a single author so the section order and terminology stay coherent.
  • Wiki edits are cache-first: forgetting the CACHE_NAME / ?v= bump means returning visitors never see the translation you just shipped.
Files (claude-code-agent-monitor)
  • agents
    • openai.yaml 404 B
      interface:
        display_name: "i18n Parity"
        short_description: "Keep every localization surface in parity across all supported languages."
        default_prompt: "Use $i18n-parity to propagate a content change to every supported language, or to add a new language across the dashboard keys, the wiki, the mirrored READMEs, the switchers, and locale-aware formatting."
      policy:
        allow_implicit_invocation: true
      
  • references
    • new-language-checklist.md 10.3 KB
      # Adding a New Language — Complete Checklist
      
      Work top to bottom; later steps depend on earlier ones. Throughout, `<xx>` is
      the new ISO 639-1 code (e.g. `ja`), `<XX>` the README suffix you choose
      (`README-JP.md`), and `<xx-YY>` the BCP-47 tag used for date/number formatting
      (`ja-JP`).
      
      Pick the README suffix once and use it everywhere; the existing set is
      `zh → CN`, `vi → VN`, `ko → KO`, `es → ES`.
      
      Run the audit after each phase — it tells you exactly what is still missing:
      
      ```bash
      bash .claude/skills/i18n-parity/scripts/i18n-audit.sh
      ```
      
      ---
      
      ## Phase 1 — Register the locale
      
      - [ ] `client/src/i18n/index.ts`
        - [ ] Add `import <ns>_<xx> from "./locales/<xx>/<ns>.json";` for **all 15
              namespaces** (`common`, `nav`, `dashboard`, `sessions`, `activity`,
              `analytics`, `workflows`, `settings`, `kanban`, `errors`, `updates`,
              `ccConfig`, `run`, `alerts`, `splash`) — copy the `es` import block.
        - [ ] Add the `<xx>: { ... }` entry to `resources`, listing all 15 namespaces.
        - [ ] Add `"<xx>"` to `supportedLngs`.
        - [ ] Update the file's header comment, which names the supported languages.
      - [ ] `client/src/lib/format.ts`
        - [ ] Add `| "<xx>"` to the `SupportedLanguage` union.
        - [ ] Add the `language === "<xx>"` branch in `getCurrentLanguage()`.
        - [ ] Add `if (language === "<xx>") return "<xx-YY>";` to the locale map.
      - [ ] `client/src/components/Sidebar.tsx`
        - [ ] Add `"<xx>"` to `SUPPORTED_LANGUAGES`.
        - [ ] Add the `base === "<xx>"` case to `normalizeLanguage()` so regional tags
              (`<xx>-YY`) resolve instead of falling back to `en`.
      - [ ] `client/src/lib/paletteCommands.ts` — add `"<xx>"` to `LANGUAGES` so the
            command palette can switch to it.
      
      Nothing else in the client needs touching: `client/index.html` is an English
      shell (`<html lang="en">`, `og:locale`) and the app does not reassign
      `document.documentElement.lang` on switch.
      
      ## Phase 2 — Translate the dashboard UI
      
      - [ ] Create `client/src/i18n/locales/<xx>/` with **all 15 `*.json` files**,
            copied from `en/` and then translated.
      - [ ] Every key, at every nesting level, must exist with the **same key path**,
            the **same value type**, and the **same `{{interpolation}}` tokens** as
            `en`. `client/src/i18n/__tests__/i18n.test.ts` enforces this.
      - [ ] Plural keys use the i18next v4 suffixes `_one` / `_other`. Key parity is
            absolute, so the new locale needs **both** forms for every plural key even
            if the language has no plural inflection — give both the same string, the
            way `zh`/`vi`/`ko` already do. (The stray `_plural` keys in `kanban.json`
            and `sessions.json` are legacy and no longer resolved; mirror them for
            parity, but do not create new ones.)
      - [ ] Add `"<xx>"` to **`languageNames` and `languageShort` in every locale's
            `nav.json`** — `en`, `zh`, `vi`, `ko`, `es`, and the new one. This is the
            switcher label; a missing entry renders the raw key in that language.
      - [ ] Read the diff for strings you copied but never translated. The parity test
            passes on English left in a `<xx>` file — only review catches it.
      
      ## Phase 3 — Translate the wiki (`wiki/`)
      
      The wiki is the largest surface. English lives in the `wiki/index.html` DOM and
      is swapped at runtime; see `.claude/rules/wiki-i18n.md`
      for the mechanism and the length/markup constraints.
      
      - [ ] `wiki/i18n-content.js`
        - [ ] Update the file's header comment, which names the bundled languages.
        - [ ] Add a top-level `<xx>: { ... }` bundle: **every body-content string**
              (`p`, `li`, `td`, `th`, `.screenshot-caption`, `.callout-body > strong`,
              `.route-desc`, footer). Keys are the element's `innerHTML` with every
              whitespace run collapsed to one space and the ends trimmed; values keep
              the identical set of inline tags (`<code>`, `<strong>`, `<a>`, `<span>`).
        - [ ] Add `plain.<xx>` with the heading/label entries.
        - [ ] Mirror the `Object.assign(...)` appendix blocks at the end of the file.
      - [ ] `wiki/script.js` — **eight** separate edits; missing any one leaves the
            locale half-wired. Add them *inside* the existing objects: the wiki test
            slices this file on literal markers (`"  const T = "`, `"  const
            ATTRIBUTE_TRANSLATIONS = "`, `"  const META = "`), so do not rename,
            reorder, or re-indent those declaration lines.
        - [ ] Add the `<xx>` block to `T` — the scannable layer matched by the `PLAIN`
              selector (`.logo-sub`, `.section-label`, `.nav-section`, `.nav-empty`,
              `.stat-label`, `.t-label`, `.main-content h2/h3/h4/th`, `.hero-desc`)
              plus the `TEXTNODE_SEL` trailing text nodes (`.nav-link`, `.hero-badge`)
              and UI chrome (`Search docs...`, `No results found`).
        - [ ] Add `<xx>: CONTENT.<xx> || {}` to the `H` map.
        - [ ] Add an `<xx>` value to **every entry** of `ATTRIBUTE_TRANSLATIONS`
              (`alt`, `aria-label`, `title`, `placeholder`) — currently 52 entries.
        - [ ] Add the `<xx>` block to `META` (title, description, socialTitle,
              socialDescription, twitterDescription, socialImageAlt).
        - [ ] Add `"<xx>"` to the `["zh", "vi", "ko", "es"]` array that merges
              `CONTENT.plain` into `T`.
        - [ ] Add `<xx>: "<native name>"` to `languageLabels` — without it the
              switcher trigger reads "English" while the page is translated.
        - [ ] Add the `<xx>` branch to the `document.documentElement.lang` ladder
              inside `apply()`, mapping to the BCP-47 tag.
        - [ ] Add the `<xx>` branch to the first-visit `navigator.language` ladder
              (`n.indexOf("<xx>") === 0`), or the locale is only ever reachable by
              picking it manually.
        - [ ] Update the file's header comment, which names the wiki's languages.
      - [ ] If you introduced a **new content container/class**, add its selector to
            `HTML_SEL` (body prose) or `PLAIN` (scannable labels) so the engine
            translates it at all.
      - [ ] `wiki/index.html`
        - [ ] Add `<button type="button" class="lang-option" data-lang="<xx>" role="option" aria-selected="false">Native name</button>`
              to **both** `.lang-select-menu` blocks (desktop header and mobile drawer).
        - [ ] Update the prose that names the shipped languages ("five languages — …").
      - [ ] **Bust the cache** (mandatory — the service worker is cache-first):
            bump `i18n-content.js?v=N` and `script.js?v=N` in `wiki/index.html`, bump
            the **same** values in `PRECACHE` in `wiki/sw.js`, and bump `CACHE_NAME`.
            A `?v=` mismatch between the two files means the precached asset is never
            served.
      
      ## Phase 4 — Mirror the README
      
      - [ ] Create `README-<XX>.md` as a **complete mirror of `README.md`**: every
            section, every table (same rows, same column count), every code block,
            every mermaid diagram, every badge, in the same order. Nothing summarized,
            nothing dropped.
      - [ ] Keep untranslated: code, commands, paths, URLs, env-var names, CLI flags,
            identifiers, mermaid node IDs, brand and product names, hook event names.
            Translate mermaid **labels**, not node IDs.
      - [ ] Update the localized-docs cross-link line (around line 73–74) in **all**
            of `README.md`, `README-CN.md`, `README-VN.md`, `README-KO.md`,
            `README-ES.md`, and the new file, so every README links to every other.
      - [ ] `server/__tests__/plugins-marketplace.test.js` — add a `COUNTED_DOCS`
            entry for `README-<XX>.md` with regexes matching how the plugin / skill
            counts are phrased in the new language.
      
      ## Phase 5 — Update the docs that enumerate languages
      
      Grep first — this list is the current state, not a guarantee:
      
      ```bash
      grep -rn 'en/zh/vi/ko/es\|five languages\|"en", "zh", "vi", "ko", "es"' \
        --include='*.md' --include='*.ts' --include='*.tsx' --include='*.js' --include='*.html' \
        . --exclude-dir=node_modules --exclude-dir=dist
      ```
      
      - [ ] `docs/I18N.md` — supported-language line, `supportedLngs`, the locale map
            (`<xx>` → `<xx-YY>`), the ERD `code`/`locale` strings, the classDiagram
            `supportedLngs`, the `stateDiagram-v2` transitions, the format flowchart
            branch, the test matrix, and the operational checklist.
      - [ ] `docs/README.md` — the "Internationalization Support (en/zh/…)" heading
            and the supported-codes paragraph.
      - [ ] `ARCHITECTURE.md` — the localization-stack paragraph, the command-palette
            actions row, and the `splash` namespace note.
      - [ ] `client/README.md` — the palette actions row.
      - [ ] `index.html` — the `Languages (en/zh/vi/ko/es)` stat label. The landing
            page itself stays English-only.
      - [ ] `.claude/rules/wiki-i18n.md` and `.claude/rules/i18n-parity.md` — the
            locale lists.
      - [ ] `.claude/skills/update-project-docs/SKILL.md` and
            `references/doc-map.md` — the README lists.
      - [ ] `.github/CONTRIBUTING.md` — the localization section, if it names locales.
      
      ## Phase 6 — Extend the tests
      
      - [ ] `client/src/i18n/__tests__/i18n.test.ts` — add `"<xx>"` to the parity loop
            and to the two `["en", "zh", "vi", "ko", "es"]` coverage loops; add a
            nav-keys test and a non-explicit-tag test (`<xx>-YY` resolves to `<xx>`)
            mirroring the existing per-language cases.
      - [ ] `client/src/lib/__tests__/format.test.ts` — add the `<xx>` → `<xx-YY>`
            formatting case.
      - [ ] `client/src/components/__tests__/Sidebar.test.tsx` — update the
            "all five languages" expectation to the new count.
      - [ ] `client/tests/wiki-i18n.test.ts` — add `"<xx>"` to `LANGUAGES` so the wiki
            coverage assertions run against the new bundle.
      
      ## Phase 7 — Verify
      
      ```bash
      bash .claude/skills/i18n-parity/scripts/i18n-audit.sh   # must exit 0
      npm run verify                                          # headers, format, typecheck, server + client tests
      cd client && npx vitest run tests/wiki-i18n.test.ts     # wiki live-DOM coverage
      ```
      
      Then read the wiki top to bottom in the new locale. There is **no `?lang=` URL
      parameter** — the choice lives in `localStorage["wiki-lang"]`, so either pick it
      in the switcher or run this in the console and reload:
      
      ```js
      localStorage.setItem("wiki-lang", "<xx>");
      ```
      
      Any English paragraph under a translated heading is a missing
      `wiki/i18n-content.js` entry; an English switcher label with a translated page
      is a missing `languageLabels` entry.
      
      Finally, run the `update-project-docs` (`.claude/skills/update-project-docs/SKILL.md`)
      skill — a new language is a user-facing feature and belongs in the feature
      tables and the wiki's own feature list.
      
    • translation-style.md 6.3 KB
      # Translation Style and Glossary
      
      Terminology drift — the same concept rendered three ways on three pages — is the
      most common review comment on localization PRs. This file is the tie-breaker.
      
      ## Never translate
      
      | Category | Examples |
      |---|---|
      | Code and commands | anything inside `<code>` / backticks, `npm run dev`, `ccam sessions` |
      | Paths and URLs | `client/src/i18n/index.ts`, `~/.claude/settings.json`, any link target |
      | Env vars and CLI flags | `DASHBOARD_TOKEN_REPAIR`, `--list`, `PORT` |
      | HTTP | `GET`, `POST`, `404`, `WebSocket`, `SSE` |
      | Identifiers | function, class, table, column, and namespace names |
      | Claude Code vocabulary | hook events (`PreToolUse`, `PostToolUse`, `Stop`, `SessionStart`, `Notification`), tool names (`Bash`, `Agent`, `Read`, `Edit`) — the **tool** `Agent` is an identifier and is never translated in any locale |
      | Brand and product names | Claude Code, Codex, MCP, SQLite, Express, React, Vite, Prometheus, Grafana, Tabby |
      | Numbers with units | `~50,000 inserts/sec`, `200 KB / 63 KB gzip`, `< 5ms` |
      
      **Two different things are spelled `Agent`.** The Claude Code **tool** named
      `Agent` (alongside `Bash`, `Read`, `Edit`) is an identifier and stays literal in
      **every** locale, everywhere it appears — hook-event tables, tool lists, event
      names. The **UI noun** for an agent (`common:agent` / `common:subagent`) is
      product vocabulary with a per-locale contract, and only that noun has the
      Spanish exception below. Never carry the exception into the tool name.
      
      **As product vocabulary, `Agent` and `Subagent` are not ordinary English
      words** — but the repo does not treat them identically in every locale, and
      `client/src/i18n/__tests__/i18n.test.ts` pins the actual contract:
      
      | Locale | `common:agent` | `common:subagent` |
      |---|---|---|
      | `zh` / `vi` / `ko` | `Agent` (literal) | `Subagent` (literal) |
      | `es` | `agente` | `subagente` |
      
      Spanish localizing the pair is deliberate and asserted, not drift — Spanish has
      no comfortable way to carry the bare English noun through inflected prose. So
      keep `Agent` literal in `zh`, `vi`, and `ko` (`运行 Agent`, `Chạy Agent`,
      `Agent 실행`) and follow the Spanish convention in `es` (`Ejecutar agente`).
      **If you add a locale, decide this explicitly and add its row to the test** —
      do not leave it to whichever phrasing the first translated string happens to use.
      
      A wiki block whose content is *entirely* code, identifiers, or product names
      needs no `wiki/i18n-content.js` entry — it correctly falls back to English.
      
      ## Core glossary
      
      Established renderings. Match these; do not introduce a synonym.
      
      | English | `zh` | `vi` | `ko` | `es` |
      |---|---|---|---|---|
      | Dashboard | 仪表盘 | Tổng quan | 대시보드 | Panel |
      | Kanban Board | Kanban 看板 | Bảng Kanban | 칸반 보드 | Tablero Kanban |
      | Sessions | 会话 | Phiên | 세션 | Sesiones |
      | Activity Feed | 活动流 | Luồng hoạt động | 활동 피드 | Feed de Actividad |
      | Analytics | 分析 | Phân tích | 분석 | Analíticas |
      | Workflows | 工作流 | Quy trình | 워크플로 | Flujos |
      | Alerts | 警报 | Cảnh báo | 알림 | Alertas |
      | Agent Config | Agent 配置 | Cấu hình Agent | Agent 설정 | Configuración del agente |
      | Run Agent | 运行 Agent | Chạy Agent | Agent 실행 | Ejecutar agente |
      | Settings | 设置 | Cài đặt | 설정 | Configuración |
      | Live | 在线 | Trực tiếp | 실시간 | En vivo |
      | Disconnected | 已断开 | Mất kết nối | 연결 끊김 | Desconectado |
      
      ### Session status vocabulary
      
      These five values appear in filters, badges, charts, the Kanban columns, and
      every state-machine diagram in the docs. Keep them identical everywhere.
      
      | English | `zh` | `vi` | `ko` | `es` |
      |---|---|---|---|---|
      | Active | 活跃 | Đang hoạt động | 활성 | Activas |
      | Waiting | 等待中 | Đang chờ | 대기 중 | En espera |
      | Completed | 已完成 | Hoàn tất | 완료됨 | Completadas |
      | Error | 错误 | Lỗi | 오류 | Error |
      | Abandoned | 已废弃 | Bị bỏ dở | 중단됨 | Abandonadas |
      
      When adding a new locale, extend both tables in this file with its column before
      you start translating — deciding the vocabulary once up front is what keeps 15
      namespaces and a 7,000-line wiki bundle coherent.
      
      ## Mechanics
      
      - **Interpolation tokens are literal.** `{{count}}`, `{{name}}`, `%{value}` must
        survive verbatim; only their surrounding words are translated. Reordering
        around a token is fine and often necessary.
      - **Plurals use the i18next v4 JSON suffixes `_one` / `_other`** (the client is
        on i18next 26). A handful of legacy `_plural` keys survive in `kanban.json`
        and `sessions.json`; v4 no longer resolves them — do not copy that pattern
        into new keys. Because the parity test requires an **identical key set in
        every locale**, `zh`, `vi`, and `ko` must still carry both `_one` and
        `_other` even though they have no plural inflection: give both the same
        string. `es` gets genuinely different forms, as English does.
      - **Inline markup is preserved exactly.** A `wiki/i18n-content.js` value must
        contain the same set of `<code>`, `<strong>`, `<a>`, and `<span>` tags as its
        English key, with the same attributes. Dropping a tag silently breaks the
        page's styling and links in that locale only.
      - **Length matters in the UI.** Sidebar labels, buttons, table headers, and
        Kanban column titles live in fixed-width space. After translating, check a
        narrow viewport; prefer the shorter natural phrasing over a literal one.
      - **Wiki blocks have length budgets** (feature cards ~450–550 chars, screenshot
        captions ~150–300). A translation that doubles the English length breaks the
        carousel layout. See `.claude/rules/wiki-i18n.md`.
      - **Tone matches the English**: direct, technical, second person, no marketing
        filler. Documentation register, not advertising.
      
      ## README mirrors
      
      - Mirror **structure first**: same headings in the same order, same tables with
        the same rows and column counts, same code blocks, same mermaid diagrams.
      - In mermaid, translate **labels only** — node IDs, arrows, and directives stay
        as written, or the diagram stops parsing.
      - Keep badges, links, and anchors pointing at the same targets as `README.md`.
      - Numbers quoted in prose (plugin counts, skill counts, namespace counts) must
        match the English exactly; they are asserted in
        `server/__tests__/plugins-marketplace.test.js`.
      
  • SKILL.md 13.1 KB
    ---
    name: i18n-parity
    description: MANDATORY for every coding agent and contributor touching localized content — keep all five localization surfaces (dashboard UI keys, wiki page, mirrored READMEs, locale-aware formatting, language switchers) in parity across every supported language. Use automatically (without being asked) whenever you add or change user-visible UI copy, add an i18n key, edit README.md, edit wiki/index.html, or change docs that the READMEs and wiki mirror — and use the full new-language checklist whenever adding a language (a new README-XX.md, a new client/src/i18n/locales/<xx>/ directory, or a new lang-option in the wiki).
    ---
    
    > **Mirror.** The canonical copy of this skill — and the `i18n-audit.sh`
    > script it tells you to run — live at `.claude/skills/i18n-parity/`.
    > Keep the two in sync; edit the canonical copy first.
    
    # i18n Parity
    
    This repository is localized on **five independent surfaces**, each with its own
    mechanism. A change that lands on one and not the others leaves the product
    half-translated: the English falls through, and the gap is invisible to anyone
    working in English. This skill states the invariant, maps every surface to the
    exact files, and gives one command that proves parity.
    
    **The invariant: English is the source of truth on every surface, and no change
    is done until every supported language carries it in the same PR.** A fallback
    to English is a safety net, never a completed translation.
    
    Supported languages are declared in one place — `supportedLngs` in
    `client/src/i18n/index.ts`. Today:
    **`en`, `zh`, `vi`, `ko`, `es`**. Everything below derives from that list; the
    audit script reads it rather than hard-coding.
    
    ## The five surfaces
    
    | # | Surface | English source of truth | Translations live in | Automated gate |
    |---|---|---|---|---|
    | 1 | **Dashboard UI** | `client/src/i18n/locales/en/*.json` | `client/src/i18n/locales/<xx>/*.json` (same 15 namespaces) | `client/src/i18n/__tests__/i18n.test.ts` — key, type, and interpolation-token parity |
    | 2 | **Wiki page** | English text in the `wiki/index.html` DOM | `wiki/script.js` (`T`, `ATTRIBUTE_TRANSLATIONS`, `META`, `languageLabels`, the two language ladders) + `wiki/i18n-content.js` (`window.__WIKI_CONTENT_I18N`, both the body bundles and `plain`) | `client/tests/wiki-i18n.test.ts` — live-DOM prose coverage, inline-tag preservation, block-length budgets, asset-version sync |
    | 3 | **Mirrored READMEs** | `README.md` | `README-CN.md` (zh), `README-VN.md` (vi), `README-KO.md` (ko), `README-ES.md` (es) | partial — `scripts/i18n-audit.sh` (existence, heading count, cross-links) and `server/__tests__/plugins-marketplace.test.js` (documented counts); prose parity is review-only |
    | 4 | **Language switchers** | — | `client/src/components/Sidebar.tsx`, `client/src/lib/paletteCommands.ts`, the two `.lang-select-menu` blocks in `wiki/index.html`, `nav.json` `languageNames` / `languageShort` | `scripts/i18n-audit.sh` |
    | 5 | **Locale-aware formatting** | — | `client/src/lib/format.ts` (`SupportedLanguage` union, `getCurrentLanguage()` whitelist, `getCurrentLocale()` BCP-47 map) | `client/src/lib/__tests__/format.test.ts` |
    
    ### What is deliberately NOT localized
    
    Verified against the tree — do not go looking for translation hooks in these,
    and do not add them without being asked:
    
    - **The root landing page `index.html`** has no i18n layer. Do not add
      `data-lang` markup; put localized long-form content in the wiki instead. Its
      one language-aware element is the `Languages (en/zh/…)` stat label, which just
      enumerates the codes.
    - **`client/index.html`** is an English shell: `<html lang="en">`,
      `og:locale=en_US`, and English `<title>`/meta. The React app never reassigns
      `document.documentElement.lang` when the user switches language — a known gap,
      not something a translation PR is expected to fix.
    - **The CLI (`bin/ccam.js`), the MCP server (`mcp/`), the Express server
      (`server/`), the desktop shell (`desktop/`), the VS Code extension, and the
      statusline** contain no i18n wiring at all. Their output is English.
    - **`client/src/lib/event-summary.ts` and `event-grouping.ts`** build tool-event
      headlines and bullets from English template literals (`Last message: …`,
      `3 lines stdout`, `2 matches`). Their MODULE_GUIDE boilerplate claims strings
      belong in i18n JSON, but neither file imports i18next. They sit outside the
      key system today; do not "fix" them as part of a localization change.
    - **Number and date formatting is only locale-aware where the `format.ts`
      helpers are used.** Most components call `toLocaleString()` / 
      `toLocaleString(undefined, …)` directly, which follows the *browser* locale
      rather than the chosen UI language. In new code prefer `getCurrentLocale()`
      from `format.ts`; leave existing call sites alone unless asked.
    
    ## Workflow A — you changed content (the common case)
    
    Find what you touched in the left column and ship everything in the right column
    **in the same PR**.
    
    | You changed | You must also do |
    |---|---|
    | Added/renamed a UI string or i18n key | Add the key to `en` **and every other locale** in the same namespace file. Same key path, same value type, same `{{interpolation}}` tokens. |
    | Added a new namespace (new `*.json`) | Create it for every locale, then register the imports, the `resources` entry per language, and the `ns` array in `client/src/i18n/index.ts`. |
    | Added user-visible wiki text in `wiki/index.html` | Follow `.claude/rules/wiki-i18n.md`: scannable layer (the `PLAIN` selector set — `.logo-sub`, `.section-label`, `.nav-section`, `.nav-empty`, `.stat-label`, `.t-label`, `h2`/`h3`/`h4`, `th`, `.hero-desc`, plus `.nav-link` / `.hero-badge` trailing text nodes) → `T` in `wiki/script.js`; body prose (the `HTML_SEL` set — `p`, `li`, `td`, `th`, captions, `.callout-body > strong`, `.route-desc`, footer) → `wiki/i18n-content.js` keyed by whitespace-normalized `innerHTML`; new `alt`/`aria-label`/`title`/`placeholder` → `ATTRIBUTE_TRANSLATIONS`. Then bump `CACHE_NAME` in `wiki/sw.js` and the matching `?v=` query strings. |
    | Edited a section of `README.md` | Mirror the **same** edit at the corresponding section of `README-CN.md`, `README-VN.md`, `README-KO.md`, and `README-ES.md`. All four, every time. |
    | Changed behavior that the README/wiki document (env var, event type, route, CLI command, feature) | Run the `update-project-docs` (`.claude/skills/update-project-docs/SKILL.md`) skill — it owns the change→docs mapping — then come back here for the translation propagation it triggers. |
    | Changed a documented count (plugins, skills, namespaces, languages) | The count is repeated across all five READMEs, `ARCHITECTURE.md`, `docs/*.md`, `index.html`, `wiki/index.html`, `wiki/i18n-content.js`, and asserted in `server/__tests__/plugins-marketplace.test.js`. Grep the old number repo-wide; update every hit. |
    
    ## Workflow B — adding a new language
    
    This is a large, exact, mechanical change. **Read
    [`references/new-language-checklist.md`](references/new-language-checklist.md)
    and work through it top to bottom** — it lists every file, in dependency order,
    with the exact edit for each.
    
    The three things contributors most often ship incomplete, stated up front:
    
    1. **The README mirror must be complete.** `README-<XX>.md` is a full mirror of
       `README.md` — every section, every table row, every code block, every mermaid
       diagram, in the same order. Do not summarize, do not drop "less important"
       sections, do not stop halfway. Diff the heading list against `README.md`
       before you open the PR — the audit script compares heading counts, which
       catches a truncated mirror but not a reordered or silently condensed one.
    2. **Every app key must be translated.** All 15 namespaces × every key. The
       parity test fails on a missing key, but it *passes* on a key you copied over
       in English — so read your diff for untranslated leftovers.
    3. **The wiki must be translated completely**, not just the headings. That means
       a full `<xx>` bundle in `wiki/i18n-content.js` (thousands of body strings), a
       full `<xx>` block in `T`, `META`, and every entry of `ATTRIBUTE_TRANSLATIONS`
       in `wiki/script.js`. A locale that only fills `T` renders a page with
       translated headings over English paragraphs, which is worse than English.
    
    ## What stays in English (all surfaces)
    
    Never translate: code inside `<code>`/backticks, commands, file and directory
    paths, URLs, env-var names, HTTP methods and status codes, CLI flags, code
    identifiers, numbers with units, brand and product names (`Claude Code`, `MCP`,
    `Codex`), Claude Code hook event names (`PreToolUse`, `Stop`, …), and Claude
    Code tool names (`Bash`, `Agent`, `Read`, `Edit`). Translate only the prose
    around them. A block that is *entirely* code or identifiers needs no wiki
    entry — it correctly falls back to English.
    
    **Two different things are spelled `Agent`.** The Claude Code **tool** named
    `Agent` (alongside `Bash`, `Read`, `Edit`) is an identifier and stays literal in
    **every** locale, everywhere it appears — hook-event tables, tool lists, event
    names. The **UI noun** for an agent (`common:agent` / `common:subagent`) is
    product vocabulary with a per-locale contract, and only that noun has the
    Spanish exception below. Never carry the exception into the tool name.
    
    The UI noun's contract, asserted by `client/src/i18n/__tests__/i18n.test.ts`:
    `zh`, `vi`, and `ko` keep `Agent` / `Subagent` literal; `es` renders them
    `agente` / `subagente`. A new locale must decide this explicitly and add its
    row to that test.
    
    Terminology, per-locale conventions, and the shared glossary live in
    [`references/translation-style.md`](references/translation-style.md). Read it
    before translating; drifting terminology across pages is the most common
    review comment on localization PRs.
    
    ## Two traps worth knowing before you edit the wiki
    
    1. **`client/tests/wiki-i18n.test.ts` parses `wiki/script.js` by exact source
       markers.** It slices the file between literal strings — `"  const T = "` …
       `"\n\n  const PLAIN"`, `"  const ATTRIBUTE_TRANSLATIONS = "` …
       `"\n  const ATTR"`, `"  const META = "` … `"\n  const trH"` — and `eval`s
       what it finds. Renaming, reordering, or re-indenting those declarations
       breaks the test with a confusing error. Add locales *inside* the existing
       objects; leave the declaration lines alone.
    2. **The wiki's chosen language lives in `localStorage["wiki-lang"]`, not a URL
       parameter.** There is no `?lang=` support. First visit falls back to a
       `navigator.language` prefix ladder in `wiki/script.js`. To preview a locale,
       use the switcher, or run
       `localStorage.setItem("wiki-lang", "<xx>")` in the console and reload.
    
    ## This skill is mirrored for every agent
    
    The canonical copy — and both scripts — live at `.claude/skills/i18n-parity/`.
    It is mirrored, with links rewritten to repo-root-relative paths and an
    `agents/openai.yaml` interface added, to:
    
    - `.agents/skills/i18n-parity/` (the shared/OpenAI skill tree)
    - `.codex/skills/i18n-parity/` (Codex)
    
    **Editing this skill means regenerating the mirrors** — edit the canonical copy,
    then run:
    
    ```bash
    bash .claude/skills/i18n-parity/scripts/sync-agent-mirrors.sh
    ```
    
    `i18n-audit.sh` runs that script in `--check` mode, so a stale mirror is a
    reported gap rather than silent drift. The mirrors carry no scripts: they point
    back at the canonical ones by repo-root path.
    
    ## Verify (do not skip)
    
    ```bash
    # 1. Cross-surface parity: locale sets, namespace files, key parity, switcher
    #    entries, wiki bundles, README mirrors and cross-links, agent-skill mirrors.
    bash .claude/skills/i18n-parity/scripts/i18n-audit.sh
    
    # 2. UI key/type/interpolation parity + locale formatting
    npm run test:client
    
    # 3. Wiki live-DOM coverage, inline tags, metadata, cache versions
    cd client && npx vitest run tests/wiki-i18n.test.ts && cd ..
    
    # 4. Documented counts asserted against the source tree
    npm run test:server
    
    # 5. The static wiki files and locale JSON are Prettier-managed
    npm run format
    ```
    
    `i18n-audit.sh` exits non-zero and names the exact file and locale for every
    gap. Every check is a structural one — a named thing is present or it is not —
    with a single exception: the `wiki/i18n-content.js` bundle-size check is a
    **stub detector** (it flags a locale holding under 60% of the largest bundle's
    entries). Exact per-string wiki coverage is asserted by
    `client/tests/wiki-i18n.test.ts`, which walks the live DOM.
    
    State explicitly which surfaces you updated and which you intentionally skipped
    (with the reason), per the repo's verification policy in `CLAUDE.md`.
    
    ## Tips
    
    - **Write the English first and get it right**, on all surfaces, before
      translating anything. Every other locale is derived from it; re-translating
      because the English moved is the biggest waste in this workflow.
    - To find where a string already lives: `grep -rn "<neighbouring English text>"
      client/src/i18n/locales/en wiki/i18n-content.js wiki/script.js`.
    - When adding a language, one locale per subagent is fine for the *wiki body*
      bundle (it is large), but keep the README mirror with a single author so the
      section order and terminology stay coherent.
    - Wiki edits are cache-first: forgetting the `CACHE_NAME` / `?v=` bump means
      returning visitors never see the translation you just shipped.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related