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');