From 66da91f78bef4c4992576711052f59a3545ca562 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Wed, 7 Oct 2026 02:07:27 +0200 Subject: [PATCH] feat(i18n): the tab's exited-agent badge in Chinese (zh-CN) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Owner request: translate the EXITED badge. The badge carried data-i18n-skip on purpose, because its in-place update compared the DOM with the English label: a translated badge would never have matched, and every incremental tab pass would have written English back for the translator to redo. The tab's accessible name (which carries the exit, the badge being aria-hidden) was set unconditionally on every pass, the same trap once it is translated. Now the badge is left to the translator. applyPaneExitBadge remembers the last English badge text (data-label) and accessible name (data-aria-source), both also seeded by the full render, and compares with those, never the DOM. The tab strip's incremental path updates tabs in place (no row-HTML comparison), so nothing else re-renders on a translated badge. i18n.js: "exited" -> 已退出, patterns for "exited (N)" -> 已退出(N) and "exited (signal N)" -> 已退出(信号 N), and for the accessible name " session, agent exited ..." -> " 会话,智能体已退出 ...", the session name passed through untranslated. The header strip, the session sidebar and the vertical rail all host the same tab markup, so this covers all three. English reads exactly as before. Tests: session-pane-exit-ui pins the new markup, the remembered English and that a translated badge and accessible name survive an unchanged pass; i18n-exit-run-help runs every paneExitLabel form (and the accessible name, with names that are dictionary words) through the real translator in both languages. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/web/public/app.js | 30 ++++++++---- src/web/public/i18n.js | 13 +++++ test/i18n-exit-run-help.test.ts | 80 +++++++++++++++++++++++++++++++ test/session-pane-exit-ui.test.ts | 29 +++++++++-- 4 files changed, 139 insertions(+), 13 deletions(-) create mode 100644 test/i18n-exit-run-help.test.ts diff --git a/src/web/public/app.js b/src/web/public/app.js index d7246a4f..0381fa77 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -381,8 +381,17 @@ function applyPaneExitBadge(tab, paneExit) { tab.classList.toggle('tab-agent-exited', !!label); // The tab's aria-label overrides its contents for the accessible name, and the // badge is aria-hidden like its siblings, so the exit has to ride the label. + // Compared with the last English label set (data-aria-source, seeded by the + // full render too), never the attribute: in zh-CN the translator rewrites it, + // and writing English back on every pass would have it translate again. const name = tab.querySelector('.tab-name')?.dataset?.fullName; - if (name) tab.setAttribute('aria-label', paneExitAriaLabel(name, label)); + if (name) { + const aria = paneExitAriaLabel(name, label); + if (tab.dataset.ariaSource !== aria) { + tab.dataset.ariaSource = aria; + tab.setAttribute('aria-label', aria); + } + } if (!label) { existing?.remove(); return; @@ -391,16 +400,19 @@ function applyPaneExitBadge(tab, paneExit) { const badge = document.createElement('span'); badge.className = 'tab-exited-badge'; badge.setAttribute('aria-hidden', 'true'); - // Generated status text, like the status pills: it carries data-i18n-skip - // rather than a dictionary entry. Without it the translator would rewrite - // the badge and the next render pass would rewrite it back, because the - // comparison below is against the English string. - badge.setAttribute('data-i18n-skip', ''); + // Translated like any other text (i18n.js has "exited" and its exit-code + // forms). The comparison below is with the last English label (data-label), + // never the DOM, which holds the translation in zh-CN: a DOM compare would + // write the English back on every pass for the translator to redo. + badge.dataset.label = label; badge.textContent = label; tab.querySelector('.tab-name')?.insertAdjacentElement('afterend', badge); return; } - if (existing.textContent !== label) existing.textContent = label; + if (existing.dataset.label !== label) { + existing.dataset.label = label; + existing.textContent = label; + } } const DEFAULT_SHORTCUTS = [ @@ -6022,7 +6034,7 @@ class CodemanApp { const inlineSessionActions = this.shouldInlineSessionActions(); const tabActionsHtml = `⚙⧉×`; - rowHtml.set(id, `