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) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer
2026-10-07 01:20:22 +02:00
parent c848e7cf27
commit dfb9f32e23
4 changed files with 20 additions and 5 deletions
+6 -2
View File
@@ -529,8 +529,12 @@ Object.assign(CodemanApp.prototype, {
const item = document.createElement('button'); const item = document.createElement('button');
item.type = 'button'; item.type = 'button';
item.setAttribute('role', 'menuitem'); item.setAttribute('role', 'menuitem');
item.setAttribute('data-i18n-skip', ''); // Only the name is user text: skipping the whole button would keep its
item.textContent = c.label; // 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; item.disabled = full;
if (full) item.title = limit.full; if (full) item.title = limit.full;
item.addEventListener('click', () => { item.addEventListener('click', () => {
+1 -1
View File
@@ -67,7 +67,7 @@ describe('every way in stops at the cap', () => {
const app = fullGrid(); const app = fullGrid();
app.openTileAddMenu({ stopPropagation: vi.fn(), preventDefault: vi.fn(), currentTarget: null }, 's-1'); 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 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); expect(items).toHaveLength(2);
for (const item of items) { for (const item of items) {
expect(item.disabled).toBe(true); expect(item.disabled).toBe(true);
+12 -1
View File
@@ -72,13 +72,20 @@ const leftover = (text: string) => text.replace(ALLOWED, '').match(/[A-Za-z]+/g)
const seen = new Map<string, string>(); const seen = new Map<string, string>();
/** Text in a data-i18n-skip subtree (user text): must never be translated. */ /** Text in a data-i18n-skip subtree (user text): must never be translated. */
const userText = new Set<string>(); const userText = new Set<string>();
/**
* 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) { function harvest(root: FakeEl | null | undefined, where: string) {
const walk = (el: FakeEl, inSkip: boolean) => { const walk = (el: FakeEl, inSkip: boolean) => {
const skip = inSkip || 'data-i18n-skip' in el.attrs; const skip = inSkip || 'data-i18n-skip' in el.attrs;
const add = (value: unknown, kind: string) => { const add = (value: unknown, kind: string) => {
if (typeof value !== 'string' || !/[A-Za-z]/.test(value)) return; 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})`); else if (!seen.has(value.trim())) seen.set(value.trim(), `${where} (${kind})`);
}; };
add(el.textContent, 'text'); 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([]); 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', () => { it('each one reads exactly as before in English', () => {
const changed = [...seen.keys()].filter((s) => en.api.t(s) !== s); const changed = [...seen.keys()].filter((s) => en.api.t(s) !== s);
expect(changed).toEqual([]); expect(changed).toEqual([]);
+1 -1
View File
@@ -212,7 +212,7 @@ describe("a tile's + menu", () => {
app.markIdleAlertSeen.mockClear(); app.markIdleAlertSeen.mockClear();
addButtonOf('s-a').dispatch('click', { stopPropagation: vi.fn(), currentTarget: null }); addButtonOf('s-a').dispatch('click', { stopPropagation: vi.fn(), currentTarget: null });
const items = addMenu()!.children.filter((i) => i.className !== 'tile-add-new'); 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'); items[1].dispatch('click');
expect(app._tileGrid.ids).toEqual(['s-a', 's-b', 's-c']); expect(app._tileGrid.ids).toEqual(['s-a', 's-b', 's-c']);
expect(app.activeSessionId).toBe('s-c'); expect(app.activeSessionId).toBe('s-c');