From dfb9f32e23f743ecc66f9745f93e534131a60db5 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Wed, 7 Oct 2026 01:20:22 +0200 Subject: [PATCH] fix(tiles): a tile's + menu items get their disabled reason in Chinese Found live in zh-CN: the + menu's session items stayed titled "The grid holds at most 6 tiles". Each item carried data-i18n-skip on the whole button to keep the session name as typed, and the translator skips an element's attributes along with its text. Only the name is skipped now (a child span, as the picker does), so the title is translated. The zh-CN coverage test classified a title inside a skipped subtree as user text, which is how this got past it; such a label is now a failure of its own ("no UI label sits inside a skipped subtree"). Co-Authored-By: Claude Opus 5.5 (1M context) --- src/web/public/tile-grid.js | 8 ++++++-- test/tile-grid-cap.test.ts | 2 +- test/tile-grid-i18n.test.ts | 13 ++++++++++++- test/tile-grid-picker.test.ts | 2 +- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/web/public/tile-grid.js b/src/web/public/tile-grid.js index bd9fc979..3d4e1289 100644 --- a/src/web/public/tile-grid.js +++ b/src/web/public/tile-grid.js @@ -529,8 +529,12 @@ Object.assign(CodemanApp.prototype, { const item = document.createElement('button'); item.type = 'button'; item.setAttribute('role', 'menuitem'); - item.setAttribute('data-i18n-skip', ''); - item.textContent = c.label; + // Only the name is user text: skipping the whole button would keep its + // title (why it is disabled) out of the translator too. + const name = document.createElement('span'); + name.setAttribute('data-i18n-skip', ''); + name.textContent = c.label; + item.appendChild(name); item.disabled = full; if (full) item.title = limit.full; item.addEventListener('click', () => { diff --git a/test/tile-grid-cap.test.ts b/test/tile-grid-cap.test.ts index ced5c79e..d0e8f85d 100644 --- a/test/tile-grid-cap.test.ts +++ b/test/tile-grid-cap.test.ts @@ -67,7 +67,7 @@ describe('every way in stops at the cap', () => { const app = fullGrid(); app.openTileAddMenu({ stopPropagation: vi.fn(), preventDefault: vi.fn(), currentTarget: null }, 's-1'); const menu = body.children.find((c) => c.className.includes('tile-add-menu'))!; - const items = menu.children.filter((c) => c.textContent === 's-7' || c.textContent === 's-8'); + const items = menu.children.filter((c) => ['s-7', 's-8'].includes(c.children[0]?.textContent ?? '')); expect(items).toHaveLength(2); for (const item of items) { expect(item.disabled).toBe(true); diff --git a/test/tile-grid-i18n.test.ts b/test/tile-grid-i18n.test.ts index 437edf96..08258393 100644 --- a/test/tile-grid-i18n.test.ts +++ b/test/tile-grid-i18n.test.ts @@ -72,13 +72,20 @@ const leftover = (text: string) => text.replace(ALLOWED, '').match(/[A-Za-z]+/g) const seen = new Map(); /** Text in a data-i18n-skip subtree (user text): must never be translated. */ const userText = new Set(); +/** + * A title or accessible name inside a skipped subtree: the translator skips + * the element's attributes along with its text, so a UI label there stays + * English. Only the user text itself may be skipped. + */ +const labelsInSkip: string[] = []; function harvest(root: FakeEl | null | undefined, where: string) { const walk = (el: FakeEl, inSkip: boolean) => { const skip = inSkip || 'data-i18n-skip' in el.attrs; const add = (value: unknown, kind: string) => { if (typeof value !== 'string' || !/[A-Za-z]/.test(value)) return; - if (skip) userText.add(value.trim()); + if (skip && kind === 'text') userText.add(value.trim()); + else if (skip) labelsInSkip.push(`${where} (${kind}): ${value}`); else if (!seen.has(value.trim())) seen.set(value.trim(), `${where} (${kind})`); }; add(el.textContent, 'text'); @@ -298,6 +305,10 @@ describe('every tile grid string the code puts on screen translates to zh-CN', ( expect(bad).toEqual([]); }); + it('no UI label sits inside a skipped subtree (where the translator cannot reach it)', () => { + expect(labelsInSkip).toEqual([]); + }); + it('each one reads exactly as before in English', () => { const changed = [...seen.keys()].filter((s) => en.api.t(s) !== s); expect(changed).toEqual([]); diff --git a/test/tile-grid-picker.test.ts b/test/tile-grid-picker.test.ts index c58eb6db..63235bcc 100644 --- a/test/tile-grid-picker.test.ts +++ b/test/tile-grid-picker.test.ts @@ -212,7 +212,7 @@ describe("a tile's + menu", () => { app.markIdleAlertSeen.mockClear(); addButtonOf('s-a').dispatch('click', { stopPropagation: vi.fn(), currentTarget: null }); const items = addMenu()!.children.filter((i) => i.className !== 'tile-add-new'); - expect(items.map((i) => i.textContent)).toEqual(['s-other', 's-c']); + expect(items.map((i) => i.children[0].textContent)).toEqual(['s-other', 's-c']); items[1].dispatch('click'); expect(app._tileGrid.ids).toEqual(['s-a', 's-b', 's-c']); expect(app.activeSessionId).toBe('s-c');