diff --git a/src/web/public/i18n.js b/src/web/public/i18n.js index 8a31d88a..b08a557e 100644 --- a/src/web/public/i18n.js +++ b/src/web/public/i18n.js @@ -1142,6 +1142,23 @@ return !element || Boolean(element.closest(SKIP_SELECTOR)); } + // xterm's DOM renderer rewrites its rows (`.xterm-rows > div`) on every frame + // a pane changes: thousands of mutation records a second with a grid of tiles, + // each paying a closest() over the whole skip list. All rows of one terminal + // share that parent, so its own shouldSkip() verdict is kept once it says + // skip; a skip verdict cannot lapse, since xterm keeps `.xterm-rows` inside + // its `.xterm`. A rows container that is not skipped is never kept: its rows + // go through the full check below like any other node. + const skippedRows = new WeakSet(); + function isSkippedRow(node) { + const rows = node.parentNode; + if (!rows?.classList?.contains('xterm-rows')) return false; + if (skippedRows.has(rows)) return true; + if (!shouldSkip(rows)) return false; + skippedRows.add(rows); + return true; + } + function shouldSkipText(node) { const element = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement; return shouldSkip(node) || Boolean(element?.closest(USER_TEXT_SELECTOR)); @@ -1243,7 +1260,7 @@ // check per record instead of one per text node and attribute matters // for xterm's DOM renderer, which replaces rows every frame (the split // pane, every tile of the grid). - if (shouldSkip(mutation.target)) continue; + if (isSkippedRow(mutation.target) || shouldSkip(mutation.target)) continue; if (mutation.type === 'characterData') translateNode(mutation.target); if (mutation.type === 'attributes') translateAttributes(mutation.target); for (const added of mutation.addedNodes) translateNode(added); diff --git a/test/i18n-branding.test.ts b/test/i18n-branding.test.ts index 0b77acec..8638d214 100644 --- a/test/i18n-branding.test.ts +++ b/test/i18n-branding.test.ts @@ -132,6 +132,59 @@ describe('custom display name and browser localization', () => { dom.window.close(); }); + it('decides an xterm row by its rows container, with the verdict closest() gives', async () => { + // A terminal (rows inside .xterm) and a stray `.xterm-rows` outside any + // skipped surface, which the full check translates. + const dom = makeDom( + '