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');
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', () => {
+1 -1
View File
@@ -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);
+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>();
/** Text in a data-i18n-skip subtree (user text): must never be translated. */
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) {
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([]);
+1 -1
View File
@@ -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');