Skip to content

feat(viewer): add Chinese locale#673

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

feat(viewer): add Chinese locale#673
RayShark wants to merge 15 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

Release Notes

  • New Features

    • Added internationalization support to the Viewer UI with built-in English, German, and Chinese language options via the VIEWER_LANGUAGE environment variable.
    • Missing translations gracefully fall back to English.
  • Documentation

    • Updated guides with instructions for contributing new language translations to the Viewer.
  • Tests

    • Added comprehensive i18n functionality and locale handling test coverage.

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

📝 Walkthrough

Walkthrough

This PR implements viewer UI internationalization by adding a locale subsystem that loads and normalizes language settings, injects locale bundles into the rendered HTML, provides client-side translation helpers, and includes English, German, and Chinese locale data files. The feature is configurable via the VIEWER_LANGUAGE environment variable and documented in the README, contributing guide, and changelog.

Changes

Viewer i18n System

Layer / File(s) Summary
Locale loading and normalization
src/auth.ts, src/viewer/locales.ts, test/viewer-i18n.test.ts
VIEWER_LOCALE_PLACEHOLDER constant is added. New locale subsystem discovers locale JSON files at runtime, normalizes language inputs to 2–3 letter primary subtags, validates language codes, caches locale loads, and resolves the viewer language from the VIEWER_LANGUAGE environment variable with fallback to English. Bundle building constructs a locale object with active messages and English fallback.
Server-side document injection
src/viewer/document.ts, test/viewer-i18n.test.ts, test/viewer-security.test.ts
renderViewerDocument() resolves the viewer language, builds the locale bundle, JSON-encodes it with < escaping to prevent script injection, and replaces VIEWER_LOCALE_PLACEHOLDER in the template. Tests verify placeholder removal, JSON/HTML escaping, inclusion of an HTML-safe t() implementation in the rendered HTML, and an allowlist that excludes dangerous attributes. Security test confirms the locale bundle is injected into the nonce-bearing script tag containing viewer code.
Client-side i18n runtime and UI wiring
src/viewer/index.html (lines 995–1086, 946–962, 1120)
Defines window.t(key, vars) for HTML-escaped interpolation, window.tRaw(key, vars) for unescaped text, and window.applyI18n(root) to apply data-i18n and data-i18n-attr bindings with a fixed allowlist for safe attributes. Exposes convenience accessors and adds te() for enum label translation. Header UI localizes tab buttons and theme toggle.
Viewer UI localization
src/viewer/index.html (dashboard/graph/memories/timeline/activity/sessions/lessons/actions/crystals/audit/profile/replay/status sections)
All viewer pages localize user-facing strings: dashboard hero/metrics/gauges/memory cards, graph sidebar/tooltips/legend, memories table/empty states/delete modal, timeline toolbar/filters/pagination, activity heatmap/feed, sessions list/detail/summarize, lessons/actions/crystals/audit/profile search/empty states, replay controls, and websocket connection status.
Locale data files and structural validation
src/viewer/locales/en.json, src/viewer/locales/de.json, src/viewer/locales/zh.json, test/viewer-i18n.test.ts
English baseline and German/Chinese translations define all UI strings with templated placeholders ({n}, {tokens}, {page}, etc.). Tests enforce structural parity: every top-level key and nested leaf path exists in each target locale, and placeholder marker sets match exactly. Simulated t() tests verify message retrieval, fallback-to-English behavior, missing-key fallback, and placeholder interpolation.
Build configuration and documentation
package.json, .env.example, README.md, CHANGELOG.md, CONTRIBUTING.md
Build script creates dist/viewer/locales and copies src/viewer/locales/*.json into distribution. VIEWER_LANGUAGE environment variable is documented in .env.example and README with supported locales and fallback behavior. Contributing guide instructs translators to copy/translate the English locale JSON, run npm test for structural validation, and open a PR. Changelog documents the feature and issue reference.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

A rabbit hops through languages now,
English, German, Chinese—take a bow! 🐰
t() translates each UI view,
From dashboard cards to memories too,
With fallback grace when strings are few. 🌍

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(viewer): add Chinese locale' is partially related—it highlights adding the Chinese locale but doesn't convey that this completes a comprehensive i18n infrastructure built in prior commits.
Linked Issues check ✅ Passed All coding requirements from #483 are met: i18n infrastructure is implemented [#541], Chinese locale file added [#673], structural parity tests enforced, locale input normalization included, and JSON contribution workflow documented.
Out of Scope Changes check ✅ Passed All changes are in-scope: locale files, i18n infrastructure, documentation updates, build configuration, tests, and test security hardening all directly support the #483 feature request.

✏️ 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

### Added

- Viewer i18n: `VIEWER_LANGUAGE` env switches viewer UI between shipped locales (`en`, `de`, `zh`). New locales ship via PR with JSON files under `src/viewer/locales/`. Closes #483.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Align changelog updates with the documented release policy.

Line 11 adds a CHANGELOG entry in a feature PR, while the repo contribution policy says CHANGELOG edits should land in release PRs only. Please move this note to the release PR flow (or update the policy if practice has changed).

🤖 Prompt for 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.

In `@CHANGELOG.md` at 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.

Comment thread src/viewer/locales.ts
Comment on lines +99 to +103
export function buildLocaleBundle(lang: string): LocaleBundle {
const normalized = normalizeLanguageTag(lang);
const messages = loadLocale(normalized);
const fallback = normalized === FALLBACK_LANG ? {} : loadLocale(FALLBACK_LANG);
return { lang: normalized, messages, fallback };
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate canonical lang in buildLocaleBundle() before returning.

buildLocaleBundle() currently returns lang from normalizeLanguageTag() without VALID_LANG enforcement, so invalid inputs can leak into bundle.lang (e.g., "123"), even though message loading falls back safely. Please hard-fallback lang to "en" when the normalized tag fails validation (Line 100 onward).

Suggested patch
 export function buildLocaleBundle(lang: string): LocaleBundle {
-  const normalized = normalizeLanguageTag(lang);
+  const candidate = normalizeLanguageTag(lang);
+  const normalized = VALID_LANG.test(candidate) ? candidate : FALLBACK_LANG;
   const messages = loadLocale(normalized);
   const fallback = normalized === FALLBACK_LANG ? {} : loadLocale(FALLBACK_LANG);
   return { lang: normalized, messages, fallback };
 }
🤖 Prompt for 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.

In `@src/viewer/locales.ts` around lines 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.

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