diff --git a/src/web/public/tile-grid.js b/src/web/public/tile-grid.js index 8fa08206..bd9fc979 100644 --- a/src/web/public/tile-grid.js +++ b/src/web/public/tile-grid.js @@ -647,8 +647,8 @@ Object.assign(CodemanApp.prototype, { * 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 * per-device `showTileGridButton` setting is on: with it off (the default) - * the chord is inert and reaches the terminal like any unbound key. (The - * applied default pending the owner's answer; one line to change.) The focus, + * the chord is inert and reaches the terminal like any unbound key (owner + * decision 6 in docs/tile-grid-plan.md). The focus, * zoom and remove chords apply only while the grid is open, however it was * opened. Registry-aware (rebinds and disables in App Settings, Shortcuts). * The capture handler (app.js) dispatches it; every xterm key handler returns @@ -1096,7 +1096,13 @@ Object.assign(CodemanApp.prototype, { } entry.overlay.hidden = false; 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; entry.overlayBtn.hidden = !attachable; entry.overlayHint.hidden = attachable; @@ -1223,7 +1229,11 @@ Object.assign(CodemanApp.prototype, { if (entry.dot.className !== dotClass) entry.dot.className = dotClass; const since = row?.since?.at ? this._mobileOverviewStampText?.(row.since.at, 'for') : ''; 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 // 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. @@ -1359,7 +1369,9 @@ Object.assign(CodemanApp.prototype, { const on = id === zoomed; const label = on ? 'Restore the grid' : 'Zoom this tile'; 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.setAttribute('aria-label', label); } diff --git a/test/tile-grid-chrome.test.ts b/test/tile-grid-chrome.test.ts index e86e3822..650d98fc 100644 --- a/test/tile-grid-chrome.test.ts +++ b/test/tile-grid-chrome.test.ts @@ -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', () => { function startRename(app: GridApp, id: string) { headerOf(id).children[1].dispatch('dblclick', { stopPropagation: vi.fn() });