From 7e0f43a5b3da22c857292b85c149562fb368a1ee Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 28 Jul 2026 08:12:41 +0200 Subject: [PATCH] fix(ui): detect composed accessibility copy --- scripts/lib/control-ui-i18n-raw-copy.ts | 41 +++++++++++++++++++------ test/scripts/control-ui-i18n.test.ts | 4 ++- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/scripts/lib/control-ui-i18n-raw-copy.ts b/scripts/lib/control-ui-i18n-raw-copy.ts index 9f915b9b9d20..cd6e635788f1 100644 --- a/scripts/lib/control-ui-i18n-raw-copy.ts +++ b/scripts/lib/control-ui-i18n-raw-copy.ts @@ -78,6 +78,28 @@ function pushRawCopySegments( } } +function collectStaticStringSegments(node: ts.Expression): string[] { + if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) { + return [node.text]; + } + if (ts.isTemplateExpression(node)) { + return [node.head.text, ...node.templateSpans.map((span) => span.literal.text)]; + } + if (ts.isParenthesizedExpression(node)) { + return collectStaticStringSegments(node.expression); + } + if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) { + return [...collectStaticStringSegments(node.left), ...collectStaticStringSegments(node.right)]; + } + if (ts.isConditionalExpression(node)) { + return [ + ...collectStaticStringSegments(node.whenTrue), + ...collectStaticStringSegments(node.whenFalse), + ]; + } + return []; +} + async function walkSourceFiles(dir: string): Promise { const entries = await readdir(dir, { withFileTypes: true }); const files: string[] = []; @@ -154,16 +176,17 @@ export function collectControlUiRawCopyFromSource(params: { nameArg && valueArg && (ts.isStringLiteral(nameArg) || ts.isNoSubstitutionTemplateLiteral(nameArg)) && - RAW_COPY_ATTRIBUTE_NAMES.has(nameArg.text) && - (ts.isStringLiteral(valueArg) || ts.isNoSubstitutionTemplateLiteral(valueArg)) + RAW_COPY_ATTRIBUTE_NAMES.has(nameArg.text) ) { - pushRawCopyFinding(findings, { - kind: "html-attribute", - line: toLine(valueArg.getStart(sourceFile)), - name: nameArg.text, - path: repoPath, - text: valueArg.text, - }); + for (const text of collectStaticStringSegments(valueArg)) { + pushRawCopyFinding(findings, { + kind: "html-attribute", + line: toLine(valueArg.getStart(sourceFile)), + name: nameArg.text, + path: repoPath, + text, + }); + } } } if (ts.isTaggedTemplateExpression(node) && node.tag.getText(sourceFile) === "html") { diff --git a/test/scripts/control-ui-i18n.test.ts b/test/scripts/control-ui-i18n.test.ts index a29d32411fb4..286a7677d9e1 100644 --- a/test/scripts/control-ui-i18n.test.ts +++ b/test/scripts/control-ui-i18n.test.ts @@ -214,7 +214,7 @@ describe("control-ui-i18n process runner", () => { it("finds raw text and attributes split by template interpolation", () => { const source = - 'const jsx = `; const image = html`Preview`; menu.setAttribute("aria-label", "Selection actions");'; + 'const jsx = `; const image = html`Preview`; menu.setAttribute("aria-label", "Selection actions"); reply.setAttribute("aria-label", `Reply to ${name}`); file.setAttribute("title", "Open " + fileName);'; const sourceFile = ts.createSourceFile( "ui/src/pages/example.ts", source, @@ -235,6 +235,8 @@ describe("control-ui-i18n process runner", () => { { kind: "html-attribute", text: "Delete" }, { kind: "html-text", text: "Delete" }, { kind: "html-attribute", text: "Selection actions" }, + { kind: "html-attribute", text: "Reply to" }, + { kind: "html-attribute", text: "Open" }, ]); });