Skip to content

feat(viewer): add Chinese locale#673

Open
RayShark wants to merge 16 commits into
rohitg00:mainfrom
RayShark:feat/viewer-zh-cn-locale
Open

feat(viewer): add Chinese locale#673
RayShark wants to merge 16 commits into
rohitg00:mainfrom
RayShark:feat/viewer-zh-cn-locale

Conversation

@RayShark
Copy link
Copy Markdown

@RayShark RayShark commented May 27, 2026

Summary

  • add a Simplified Chinese viewer locale as zh.json for the VIEWER_LANGUAGE i18n framework from feat(viewer): add VIEWER_LANGUAGE env + EN/DE locales #541
  • document en, de, and zh as built-in viewer locales and extend locale parity tests to all bundled non-English locales
  • canonicalize regional language inputs such as zh-CN to zh, localize runtime WebSocket status text, and add locale helper docstrings for the docstring-coverage check

Related

Note: the Chinese locale file is zh.json, not cn.json, because #541 normalizes regional tags like zh-CN / zh_CN to the primary language subtag used for locale filenames.

Testing

  • npm run build
  • env HOME=/tmp/agentmemory-i18n-test-home npm test
  • git diff --check origin/main...HEAD

Summary by CodeRabbit

  • New Features

    • Viewer UI now supports multiple languages including English, German, and Chinese. Configure your preferred language using the VIEWER_LANGUAGE environment variable.
  • Documentation

    • Added translation contribution guide with instructions for community contributors to localize the Viewer to additional languages.

Review Change Stack

ChristianWalterMedia and others added 15 commits May 19, 2026 13:47
Inventory of visible English strings in the viewer (~237 keys across
nav, dashboard, memories, sessions, timeline, lessons, actions,
crystals, audit, activity, profile, replay, graph, status, types,
table, buttons, loading, modal). Source of truth for upcoming i18n
work; no runtime behavior change yet.

Signed-off-by: Christian Walter <chris.walter@mail.de>
Adds src/viewer/locales.ts with:
- resolveViewerLanguage() — reads VIEWER_LANGUAGE env, normalizes
  de-DE/de_DE → de, lowercases, defaults to en
- loadLocale(lang) — reads src/viewer/locales/<lang>.json with
  process-level cache, returns {} when missing (no throw)
- buildLocaleBundle(lang) — returns { lang, messages, fallback } with
  en fallback for non-en languages, empty fallback for en itself

Tests cover env normalization, missing-file handling, and bundle shape.
No wiring into the rendered viewer yet — that comes in the next commit.

Signed-off-by: Christian Walter <chris.walter@mail.de>
Adds VIEWER_LOCALE_PLACEHOLDER and wires buildLocaleBundle() through
renderViewerDocument(). The bundle JSON is injected as
window.__AM_LOCALE__ inside the existing nonced <script> tag — no new
script element, no external fetch, the CSP nonce model is unchanged.

< is escaped to < in the payload so a translation containing
'</script' cannot break out of the inline script.

The viewer template gets a single seeding line that holds the
placeholder; the runtime t() helper arrives in the next commit.

Signed-off-by: Christian Walter <chris.walter@mail.de>
Adds a 30-line IIFE in the viewer's inline script:
- t(key, vars) — dot-path lookup against window.__AM_LOCALE__.messages
  with en fallback per key and {placeholder} interpolation
- applyI18n(root) — replaces textContent on [data-i18n] elements and
  attributes on [data-i18n-attr="attr:key,attr:key"] elements
- DOMContentLoaded hook runs applyI18n(document) once and sets the
  <html lang> attribute

Markup tagging and dynamic-string conversion come in the next two
commits.

Signed-off-by: Christian Walter <chris.walter@mail.de>
Tags visible static elements with data-i18n="<namespace>.<key>" matching
the keys in src/viewer/locales/en.json. The original English text stays
in place as a no-JS fallback; the DOMContentLoaded pass added in the
previous commit overwrites textContent (and selected attributes via
data-i18n-attr) with the resolved locale string.

Dynamic strings built inside the inline <script> render functions are
not touched here — those go through t() calls in the next commit.

No visible change in the en case: t("nav.dashboard") returns
"Dashboard", identical to the literal already in markup.

Signed-off-by: Christian Walter <chris.walter@mail.de>
Converts template-literal strings inside the viewer's inline script
(renderDashboard, renderMemories, renderSessions, renderTimeline,
renderLessons, renderActions, renderCrystals, renderAudit,
renderActivity, renderProfile, renderReplay, empty-state generators,
table headers, type badges, status labels, loading placeholders) to call
t(key) instead of embedding English literals.

API enum values (m.type, node.type, s.status, etc.) are untouched —
only their display form is mapped via t(). Filters, search, and
persistence keep operating on the raw enum strings.

Adds var t = window.t at module scope so that render functions can call
t() as a plain identifier in VM sandbox contexts (required by the
viewer-session-id unit tests).

Signed-off-by: Christian Walter <chris.walter@mail.de>
Mirrors the structure of src/viewer/locales/en.json with German values
for ~237 UI strings: nav, dashboard cards, gauges, first-run hero,
memories empty state, sessions table, status labels (AKTIV/
ABGESCHLOSSEN/…), type and graph-node-type display names, replay
detail labels, modal text, and per-tab content for timeline, lessons,
actions, crystals, audit, activity, profile, replay, graph.

Adds a structural-parity test that fails if a future en.json change
adds a top-level key without a corresponding de.json entry.

Signed-off-by: Christian Walter <chris.walter@mail.de>
Extends the build script so src/viewer/locales/*.json is copied to
dist/viewer/locales/ during build. Without this, an npm-installed
agentmemory would not find any locale files at runtime and would
silently fall back to the empty-locale path.

Signed-off-by: Christian Walter <chris.walter@mail.de>
- README gains a 'Viewer language' subsection showing the env switch
  and pointing contributors at CONTRIBUTING.md.
- .env.example documents VIEWER_LANGUAGE next to the other UI flags.
- CONTRIBUTING.md gets a 'Contributing a translation' how-to.
- CHANGELOG.md records the change under Unreleased.

Signed-off-by: Christian Walter <chris.walter@mail.de>
Final i18n coverage pass. Adds six new keys covering compound or
suffix strings that the per-render conversion missed:
- dashboard.edges_count — '{n} edges' suffix on Graph Nodes card
- dashboard.token_savings_sub — Token Savings compound sub-label
- timeline.no_obs_filter_suffix — empty-state filter branch
- timeline.no_obs_session_suffix — empty-state session branch
- actions.three_ways_intro — Actions empty-state intro line
- audit.empty_body — Audit empty-state second paragraph

en.json / de.json both updated; structural parity test still green.

Signed-off-by: Christian Walter <chris.walter@mail.de>
Addresses CodeRabbit review on rohitg00#541.

t() now HTML-escapes the resolved translation string before
interpolation. Translations arrive via community PRs; escaping at the
boundary stops a malicious translation from injecting markup at the
~200 sites that concatenate t() output into innerHTML. Interpolation
vars stay raw so the hardcoded HTML fragments used in memories.title_intro
(e.g. <code>memory_remember</code>) still render.

A new tRaw() returns the unescaped value, for the small number of
callers that assign to textContent or setAttribute on safe attributes
— textContent treats entities literally, so escape would otherwise
show "&amp;" as four characters in the DOM.

data-i18n-attr now enforces a SAFE_I18N_ATTRS allowlist
(placeholder, title, alt, aria-label/labelledby/describedby/
roledescription). Any other attribute name is silently skipped, so a
contributor cannot turn the mechanism into an href:/src:/onclick:
injection vector by adding a data-i18n-attr to an element.

loadLocale() validates lang against /^[a-z]{2,3}$/i before touching
the filesystem. resolveViewerLanguage() already normalizes inputs
down to the primary subtag, so callers in this codebase are unchanged;
this is a defence-in-depth guard for the exported boundary.

Tests:
- new: rejects path-traversal sequences (../etc/passwd, ..\\windows, en/../en)
- new: rejects non-language inputs ("", "123", "en-US", single letter, 4+ letters)
- new: verifies the IIFE in index.html ships escI18n() wired into t()
- new: verifies SAFE_I18N_ATTRS allowlist exists and excludes href/src/on*
- new: structural parity now covers every nested leaf path, not just top level
- new: placeholder-marker parity between en.json and de.json

Signed-off-by: Christian Walter <chris.walter@mail.de>
Addresses CodeRabbit review on rohitg00#541.

Both README.md (Viewer language section + the env table at the bottom)
and .env.example referenced 'Drop src/viewer/locales/<lang>.json',
which is misleading for npm/global installs that ship dist/ without a
src/ tree. Rewords to explicitly call out the PR-against-repo path
(source checkout required) so users do not assume a runtime drop-in
mechanism exists.

Signed-off-by: Christian Walter <chris.walter@mail.de>
Four follow-up findings from the review on rohitg00#541.

1. SAFE_I18N_ATTRS no longer includes IDREF ARIA attributes.
   aria-labelledby and aria-describedby must reference element IDs,
   not free text — the first caller using data-i18n-attr on either
   would have silently broken the accessible name/description wiring.
   Removed both; kept aria-label and aria-roledescription.

2. Aliased tRaw beside t for the VM sandbox.
   The viewer unit tests run render functions inside a VM where
   window !== globalThis. There was already a 'var t = window.t;'
   module-scope alias for the same reason; the new tRaw() calls
   added in the previous commit would have thrown ReferenceError
   in that environment. Added 'var tRaw = window.tRaw;'.

3. loadLocale() now normalizes lang before validate/cache/file.
   VALID_LANG was case-insensitive, so loadLocale("EN") passed
   validation but then tried to read EN.json (which does not exist)
   and cached the failure under the uppercase key. Now lowercases
   and trims before everything, so loadLocale("EN"), "  en  ",
   and "En" all resolve to the same en.json bundle and the same
   cache slot. VALID_LANG simplified to /^[a-z]{2,3}$/ (no /i).

4. Placeholder-parity test no longer hides empty-string regressions.
   Previously 'if (!deVal) continue;' skipped both missing keys and
   intentional empty-string translations. Switched to a typeof check
   so empty strings are still validated for placeholder parity.

Signed-off-by: Christian Walter <chris.walter@mail.de>
Signed-off-by: rayshark <13261091606@163.com>
Signed-off-by: rayshark <13261091606@163.com>
@vercel
Copy link
Copy Markdown

vercel Bot commented May 27, 2026

@RayShark is attempting to deploy a commit to the rohitg00's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d482a342-4e1e-44c3-b22a-efeaa475284f

📥 Commits

Reviewing files that changed from the base of the PR and between c56b6ba and 01dc152.

📒 Files selected for processing (2)
  • src/viewer/locales.ts
  • test/viewer-i18n.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/viewer/locales.ts
  • test/viewer-i18n.test.ts

📝 Walkthrough

Walkthrough

This PR implements comprehensive internationalization for the viewer UI, adding language resolution, locale file discovery, client-side translation runtime, and localized strings across all dashboard pages (dashboard, graph, memories, timeline, sessions, lessons, actions, crystals, audit, profile, replay), plus German and Chinese translations with structural parity testing.

Changes

Viewer i18n System

Layer / File(s) Summary
Locale loading and normalization
src/viewer/locales.ts
Language normalization to primary subtag, filesystem directory discovery, locale JSON file loading with caching, environment resolution, and locale bundle assembly with English fallback.
Server-side locale placeholder and injection
src/auth.ts, src/viewer/document.ts
Locale placeholder constant and renderViewerDocument() locale bundle injection with script-breakout escaping.
Client-side translation runtime
src/viewer/index.html (lines 995–1086)
Window translation functions t() (escaped), tRaw() (unescaped), applyI18n() (DOM binding), safe-attribute allowlist, and te() (enum labels).
Header and theme UI localization
src/viewer/index.html (lines 946–962, 1120)
Tab button labels and theme toggle button text.
Dashboard UI localization
src/viewer/index.html (lines 1357–1700)
Loading indicator, first-run hero, metric cards, gauges, memory cards, and function metrics.
Graph sidebar and tooltip localization
src/viewer/index.html (lines 1735–2091)
Search placeholder, legend, filter labels, node tooltips, and empty states.
Memories and timeline localization
src/viewer/index.html (lines 2347–2699)
Table headers, empty states, delete modal, timeline toolbar, type filters, and pagination.
Activity and sessions localization
src/viewer/index.html (lines 2719–3023)
Heatmap, activity feed, sessions list, session detail, and summarize progress.
Lessons, actions, crystals, audit, profile, replay, and status localization
src/viewer/index.html (lines 3043–4098)
All remaining page UI and websocket status text.
Locale data files
src/viewer/locales/en.json, src/viewer/locales/de.json, src/viewer/locales/zh.json
English baseline and German/Chinese translations with full UI coverage and templated placeholders.
i18n tests and security verification
test/viewer-i18n.test.ts, test/viewer-security.test.ts
Language resolution, locale loading, bundle assembly, document injection, structural parity, interpolation behavior, and script-injection prevention.
Build and documentation
package.json, .env.example, README.md, CONTRIBUTING.md
Build script locale-asset copying, environment variable documentation, contribution guide for translators, and configuration examples.

Sequence Diagram

sequenceDiagram
  participant Server as Server
  participant ViewerDoc as renderViewerDocument()
  participant LocaleModule as Locale Module
  participant HTML as Viewer HTML
  participant Runtime as Client-side Runtime
  participant Browser as Browser DOM

  Server->>ViewerDoc: Render viewer document
  ViewerDoc->>LocaleModule: resolveViewerLanguage()
  LocaleModule-->>ViewerDoc: Resolved language (env or "en")
  ViewerDoc->>LocaleModule: buildLocaleBundle(lang)
  LocaleModule-->>ViewerDoc: {lang, messages, fallback}
  ViewerDoc->>ViewerDoc: Serialize JSON, escape <
  ViewerDoc->>HTML: Replace __AGENTMEMORY_LOCALE__ placeholder
  HTML->>Browser: Load HTML + embedded locale
  Runtime->>Runtime: Initialize window.__AM_LOCALE__
  Runtime->>Runtime: Define window.t(), tRaw(), applyI18n()
  Runtime->>Browser: Apply data-i18n bindings to DOM
  Browser-->>Runtime: Translated UI rendered
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • rohitg00

Poem

🐰 A wandering rabbit speaks:

Across the lands of code, where strings once fixed did reign,
A translator's toolkit blooms, in English, German, chain.
From Chinese halls to dashboard bright, the words now flow as free—
"t('hello.world')" whispers soft, what shall our viewers see?

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feat(viewer): add Chinese locale' is partially related to the changeset but not the primary change. While Chinese locale is included, the PR implements comprehensive i18n infrastructure (locales.ts, locale bundling, English/German locales, tests) with Chinese as one outcome. Consider a more accurate title like 'feat(viewer): implement i18n framework with Chinese locale' to reflect all major changes, or clarify if Chinese locale is the primary focus.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed The pull request fully satisfies all coding requirements from #483 and #541: implements i18n framework with JSON-based locale files, adds English/German/Chinese locales, provides mechanism for community contributions, includes comprehensive tests, and satisfies docstring coverage checks.
Out of Scope Changes check ✅ Passed All changes directly support the i18n framework and Chinese locale objectives. No out-of-scope changes detected; all modifications align with stated PR objectives from #483 and #541.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@CHANGELOG.md`:
- Line 11: The CHANGELOG entry "Viewer i18n: `VIEWER_LANGUAGE` env switches
viewer UI..." was added in a feature PR but per the repo contribution policy
changelog edits must land in release PRs; remove this line from the current PR
(revert the CHANGELOG.md addition) and instead add the entry to the upcoming
release PR's changelog section (or update the contribution policy if you intend
to change practice), ensuring the same text is used when moved.

In `@src/viewer/locales.ts`:
- Around line 99-103: buildLocaleBundle currently returns the normalized tag
from normalizeLanguageTag(lang) even when it doesn't match VALID_LANG; change it
to validate the normalized value against VALID_LANG and hard-fallback the
returned bundle.lang to FALLBACK_LANG (e.g., "en") when validation fails.
Specifically, in buildLocaleBundle, call normalizeLanguageTag(lang) ->
normalized, then if (!VALID_LANG.test(normalized)) set normalized =
FALLBACK_LANG before calling loadLocale and constructing the returned
LocaleBundle ({ lang: normalized, messages, fallback }), keeping loadLocale
fallback behavior unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 204aff83-65b5-4e6c-92d0-a4b8903e114f

📥 Commits

Reviewing files that changed from the base of the PR and between 6939d4a and c56b6ba.

📒 Files selected for processing (14)
  • .env.example
  • CHANGELOG.md
  • CONTRIBUTING.md
  • README.md
  • package.json
  • src/auth.ts
  • src/viewer/document.ts
  • src/viewer/index.html
  • src/viewer/locales.ts
  • src/viewer/locales/de.json
  • src/viewer/locales/en.json
  • src/viewer/locales/zh.json
  • test/viewer-i18n.test.ts
  • test/viewer-security.test.ts

Comment thread CHANGELOG.md Outdated
Comment thread src/viewer/locales.ts
Signed-off-by: rayshark <13261091606@163.com>
@RayShark
Copy link
Copy Markdown
Author

Addressed the current review items:

  • Removed the feature-PR CHANGELOG entry so release notes can stay in the release PR.
  • Hardened buildLocaleBundle() so invalid normalized language values hard-fallback to en before loading locale files.
  • Added regression coverage for the invalid-language fallback.

Verification:

  • npm test -- test/viewer-i18n.test.ts test/viewer-security.test.ts
  • npm run build
  • HOME=$(mktemp -d) npm test
  • git diff --check

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature request: i18n / Chinese localization for viewer UI

2 participants