fix(tiles): a translated tile label is not rewritten on every refresh

Three refreshes compared the DOM with the English source: the tile
header's tooltip, the Attach overlay's text and the zoom button's title.
With App Settings language set to 简体中文 the i18n observer writes the
translation into the DOM, so the comparison never matched again and every
chrome refresh (each session:updated, several a second with busy tiles)
wrote the English back for the observer to translate once more. Each now
remembers the last English value on the tile entry and compares with that.
English mode behaves exactly as before.

Also: tileShortcutFor's comment still called the inert chord a default
pending the owner's answer; it is owner decision 6.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-07 01:09:35 +02:00
parent be04d3e5e0
commit 6c5b4a7a25
2 changed files with 55 additions and 5 deletions
+17 -5
View File
@@ -647,8 +647,8 @@ Object.assign(CodemanApp.prototype, {
* The tile chord `e` asks for, if it applies right now, else null. The * The tile chord `e` asks for, if it applies right now, else null. The
* toggle applies while the grid is open, or where one could open AND the * toggle applies while the grid is open, or where one could open AND the
* per-device `showTileGridButton` setting is on: with it off (the default) * per-device `showTileGridButton` setting is on: with it off (the default)
* the chord is inert and reaches the terminal like any unbound key. (The * the chord is inert and reaches the terminal like any unbound key (owner
* applied default pending the owner's answer; one line to change.) The focus, * decision 6 in docs/tile-grid-plan.md). The focus,
* zoom and remove chords apply only while the grid is open, however it was * zoom and remove chords apply only while the grid is open, however it was
* opened. Registry-aware (rebinds and disables in App Settings, Shortcuts). * opened. Registry-aware (rebinds and disables in App Settings, Shortcuts).
* The capture handler (app.js) dispatches it; every xterm key handler returns * The capture handler (app.js) dispatches it; every xterm key handler returns
@@ -1096,7 +1096,13 @@ Object.assign(CodemanApp.prototype, {
} }
entry.overlay.hidden = false; entry.overlay.hidden = false;
const text = busy ? 'Attaching\u2026' : reason.text; const text = busy ? 'Attaching\u2026' : reason.text;
if (entry.overlayText.textContent !== text) entry.overlayText.textContent = text; // Compared with the last English text set, never the DOM: with the zh-CN
// translator on, the DOM holds the translation and would never match, so
// every refresh would rewrite English for it to translate again.
if (entry.overlayLabel !== text) {
entry.overlayLabel = text;
entry.overlayText.textContent = text;
}
const attachable = busy || reason.attachable; const attachable = busy || reason.attachable;
entry.overlayBtn.hidden = !attachable; entry.overlayBtn.hidden = !attachable;
entry.overlayHint.hidden = attachable; entry.overlayHint.hidden = attachable;
@@ -1223,7 +1229,11 @@ Object.assign(CodemanApp.prototype, {
if (entry.dot.className !== dotClass) entry.dot.className = dotClass; if (entry.dot.className !== dotClass) entry.dot.className = dotClass;
const since = row?.since?.at ? this._mobileOverviewStampText?.(row.since.at, 'for') : ''; const since = row?.since?.at ? this._mobileOverviewStampText?.(row.since.at, 'for') : '';
const label = row ? [row.pill, since].filter(Boolean).join(' ') : ''; const label = row ? [row.pill, since].filter(Boolean).join(' ') : '';
if (entry.header.title !== label) entry.header.title = label; // Against the last English label, never the DOM (translated in zh-CN; see _renderTileOverlay).
if (entry.headerLabel !== label) {
entry.headerLabel = label;
entry.header.title = label;
}
// The input of a rename in progress has taken the name's place in the // The input of a rename in progress has taken the name's place in the
// header, so updating the detached name never touches what is being typed. // header, so updating the detached name never touches what is being typed.
// A rename still in flight shows as already done, as on the tab. // A rename still in flight shows as already done, as on the tab.
@@ -1359,7 +1369,9 @@ Object.assign(CodemanApp.prototype, {
const on = id === zoomed; const on = id === zoomed;
const label = on ? 'Restore the grid' : 'Zoom this tile'; const label = on ? 'Restore the grid' : 'Zoom this tile';
zoomBtn.setAttribute('aria-pressed', on ? 'true' : 'false'); zoomBtn.setAttribute('aria-pressed', on ? 'true' : 'false');
if (zoomBtn.title !== label) { // Against the last English label, never the DOM (translated in zh-CN).
if (entry.zoomLabel !== label) {
entry.zoomLabel = label;
zoomBtn.title = label; zoomBtn.title = label;
zoomBtn.setAttribute('aria-label', label); zoomBtn.setAttribute('aria-label', label);
} }
+38
View File
@@ -161,6 +161,44 @@ describe('header button size (owner feedback: the 12px glyphs read as tiny)', ()
}); });
}); });
describe('a translated label survives a refresh (zh-CN)', () => {
// The i18n observer writes the translation into the DOM. Comparing the DOM
// with the English source would never match again, so each refresh would
// rewrite English for the observer to translate again. The guards compare
// with the last English value set instead.
it('the header tooltip, the Attach overlay text and the zoom title stay translated until they change', () => {
const app = makeGridApp(['s-a', 's-b']);
let pill = 'idle';
app._sidebarRichRow = () => ({ state: pill, pill, since: { at: 1 } });
app._mobileOverviewStampText = () => '3m';
app.sessions.get('s-b').pid = null;
app.openTileGrid(['s-a', 's-b']);
const a = app._tileGrid.tiles.get('s-a');
const b = app._tileGrid.tiles.get('s-b');
expect(a.header.title).toBe('idle 3m');
expect(b.overlayText.textContent).toBe('Not attached');
app.zoomTile('s-a');
expect(a.zoomBtn.title).toBe('Restore the grid');
// What the translator does to them.
a.header.title = '空闲 3m';
b.overlayText.textContent = '未附加';
a.zoomBtn.title = '恢复平铺网格';
app._renderTileChrome();
app._applyTileLayout();
expect(a.header.title).toBe('空闲 3m');
expect(b.overlayText.textContent).toBe('未附加');
expect(a.zoomBtn.title).toBe('恢复平铺网格');
// A real change still writes the new English, for the translator to take.
pill = 'working';
app._renderTileChrome();
expect(a.header.title).toBe('working 3m');
app.zoomTile('s-a');
expect(a.zoomBtn.title).toBe('Zoom this tile');
});
});
describe('rename', () => { describe('rename', () => {
function startRename(app: GridApp, id: string) { function startRename(app: GridApp, id: string) {
headerOf(id).children[1].dispatch('dblclick', { stopPropagation: vi.fn() }); headerOf(id).children[1].dispatch('dblclick', { stopPropagation: vi.fn() });