feat(viewer): add Chinese locale#673
Conversation
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 "&" 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>
|
@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. |
📝 WalkthroughWalkthroughThis 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 ChangesViewer i18n System
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add 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. Comment |
There was a problem hiding this comment.
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
📒 Files selected for processing (14)
.env.exampleCHANGELOG.mdCONTRIBUTING.mdREADME.mdpackage.jsonsrc/auth.tssrc/viewer/document.tssrc/viewer/index.htmlsrc/viewer/locales.tssrc/viewer/locales/de.jsonsrc/viewer/locales/en.jsonsrc/viewer/locales/zh.jsontest/viewer-i18n.test.tstest/viewer-security.test.ts
|
|
||
| ### 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. |
There was a problem hiding this comment.
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.
| 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 }; |
There was a problem hiding this comment.
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.
Summary
zh.jsonfor theVIEWER_LANGUAGEi18n framework from feat(viewer): add VIEWER_LANGUAGE env + EN/DE locales #541en,de, andzhas built-in viewer locales and extend locale parity tests to all bundled non-English localeszh-CNtozh, localize runtime WebSocket status text, and add locale helper docstrings for the docstring-coverage checkRelated
mainso this stack is mergeable against the upstream branchNote: the Chinese locale file is
zh.json, notcn.json, because #541 normalizes regional tags likezh-CN/zh_CNto the primary language subtag used for locale filenames.Testing
npm run buildenv HOME=/tmp/agentmemory-i18n-test-home npm testgit diff --check origin/main...HEADSummary by CodeRabbit
Release Notes
New Features
VIEWER_LANGUAGEenvironment variable.Documentation
Tests