From 6199799056a8ed21cf2b6007f5d259dfbcfdd642 Mon Sep 17 00:00:00 2001 From: Saul Dominguez Date: Wed, 26 Aug 2026 16:48:01 +0200 Subject: [PATCH] fix(react): stop the i18n gate flagging key caps and icon glyphs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five of the 133 tracked strings are not product copy at all, and would sit in the baseline forever. `Enter` is a key cap: it reads what is printed on the key, and the sentence around it is already translated separately (`F0AiChatTextArea` pairs it with `ai.clarifyingQuestion.navHint.select`). That generalises, so it is a rule rather than a marker: text directly inside ``, ``, `
`, `` or `` is machine or device text.
Only the immediate parent counts — a `` inside a sentence must not
silence the sentence, which the tests pin down.

The other three are one-offs and get inline markers: the "AD" in
AudioDescriptionToggleIcons is a glyph inside the icon artwork, like the CC
badge, and "XRay" names the dev-only inspector overlay.

Baseline 133 -> 128.

Co-Authored-By: Claude Opus 5 
---
 .../__tests__/check-untranslated-copy.test.ts | 25 +++++++++++++++
 .../react/.scripts/check-untranslated-copy.ts | 31 ++++++++++++++++++-
 .../.scripts/untranslated-copy-debt.json      |  8 +----
 .../AudioDescriptionToggleIcons.tsx           |  2 ++
 packages/react/src/lib/xray.tsx               |  1 +
 5 files changed, 59 insertions(+), 8 deletions(-)

diff --git a/packages/react/.scripts/__tests__/check-untranslated-copy.test.ts b/packages/react/.scripts/__tests__/check-untranslated-copy.test.ts
index 73e908d731..5df6e9c7b4 100644
--- a/packages/react/.scripts/__tests__/check-untranslated-copy.test.ts
+++ b/packages/react/.scripts/__tests__/check-untranslated-copy.test.ts
@@ -203,6 +203,31 @@ describe("findInSource", () => {
     ).toEqual([])
   })
 
+  it("ignores a key cap, but not the sentence it sits in", () => {
+    // The pattern in F0AiChatTextArea: the cap reads what is printed on the
+    // key, the surrounding sentence is translated separately.
+    expect(
+      scanTsx(
+        "const A = () => 

Enter to select, or press Esc

" + ) + ).toEqual([' = "to select, or press"']) + }) + + it.each(["code", "pre", "samp", "var"])( + "ignores machine text inside <%s>", + (tag) => { + expect( + scanTsx(`const A = () => <${tag}>Delete the row`) + ).toEqual([]) + } + ) + + it("still flags prose in an element that merely contains a kbd", () => { + expect( + scanTsx("const A = () =>
Press this keyEnter
") + ).toEqual([' = "Press this key"']) + }) + it("does not look for JSX text in a .ts file", () => { // A .ts file parsed as TSX turns `` casts and comparisons into elements, // and their contents into "JSX text". diff --git a/packages/react/.scripts/check-untranslated-copy.ts b/packages/react/.scripts/check-untranslated-copy.ts index a08aed2c48..04d72eb158 100644 --- a/packages/react/.scripts/check-untranslated-copy.ts +++ b/packages/react/.scripts/check-untranslated-copy.ts @@ -216,6 +216,17 @@ const STYLE_CONTAINERS = new Set([ "whileInView", ]) +/** + * Elements whose text is machine or device text rather than product copy, so + * their children are skipped. + * + * `` is the one that actually comes up: a key cap reads what is printed on + * the key, and the sentence around it is translated separately — + * `Enter {i18n.…navHint.select}`. The rest hold code, sample output + * and variable names, which are never translated either. + */ +const NON_COPY_ELEMENTS = new Set(["kbd", "code", "pre", "samp", "var"]) + export type FindingKind = | "jsx-text" | "jsx-attribute" @@ -404,6 +415,19 @@ export function findInSource(filePath: string, source: string): Finding[] { } } + /** + * Is this text a direct child of a ``, ``, … element? Only the + * immediate parent counts: a `` inside a translated sentence must not + * silence the sentence, and prose nested under a `
` would be a bug in
+   * the markup rather than something to hide.
+   */
+  const inNonCopyElement = (node: ts.Node): boolean => {
+    const parent = node.parent
+    if (!parent || !ts.isJsxElement(parent)) return false
+    const tag = parent.openingElement.tagName.getText(sourceFile)
+    return NON_COPY_ELEMENTS.has(tag)
+  }
+
   const visit = (node: ts.Node): void => {
     if (ts.isJsxAttribute(node) && node.initializer) {
       const attrName = node.name.getText(sourceFile)
@@ -443,7 +467,12 @@ export function findInSource(filePath: string, source: string): Finding[] {
       }
     }
 
-    if (isTsx && ts.isJsxText(node) && readsAsCopy(node.text)) {
+    if (
+      isTsx &&
+      ts.isJsxText(node) &&
+      !inNonCopyElement(node) &&
+      readsAsCopy(node.text)
+    ) {
       record(node, "", node.text, "jsx-text")
     }
 
diff --git a/packages/react/.scripts/untranslated-copy-debt.json b/packages/react/.scripts/untranslated-copy-debt.json
index d9204d6adf..e4d2f19349 100644
--- a/packages/react/.scripts/untranslated-copy-debt.json
+++ b/packages/react/.scripts/untranslated-copy-debt.json
@@ -1,6 +1,6 @@
 {
   "note": "Untranslated user-visible copy in packages/react/src — string literals that should come from the i18n layer (src/lib/providers/i18n) instead. Enforced by .scripts/check-untranslated-copy.ts. This list may only shrink: translate a string and remove it from here (or run \"--update\"), never add one.",
-  "total": 133,
+  "total": 128,
   "files": {
     "src/components/F0Card/components/CardMetadata.tsx": [
       "Unsupported property type:"
@@ -12,10 +12,6 @@
       "Increase"
     ],
     "src/components/F0Slider/F0SliderSkeleton.tsx": ["Loading slider"],
-    "src/components/F0VideoPlayer/components/AudioDescriptionToggleIcons.tsx": [
-      "AD",
-      "AD"
-    ],
     "src/components/OneChip/index.tsx": ["Close"],
     "src/components/RichText/F0NotesTextEditor/F0NotesTextEditor.tsx": [
       "Add paragraph"
@@ -103,7 +99,6 @@
     "src/kits/ai/F0AiChat/components/markdownRenderers/components/Image.tsx": [
       "Download"
     ],
-    "src/kits/ai/F0AiChatTextArea/F0AiChatTextArea.tsx": ["Enter", "Esc"],
     "src/kits/Charts/CategoryBarChart/index.tsx": ["Category bar chart"],
     "src/kits/surveys/SurveyAnsweringForm/SurveyAnsweringForm.tsx": ["Value"],
     "src/kits/surveys/SurveyFormBuilder/lib.ts": ["New option 1"],
@@ -111,7 +106,6 @@
       "Emojis"
     ],
     "src/lib/F0GridStack/components/grid-stack-provider.tsx": ["No content"],
-    "src/lib/xray.tsx": ["XRay"],
     "src/patterns/F0AnalyticsDashboard/components/DashboardGrid/DashboardGrid.tsx": [
       "Drag to reorder"
     ],
diff --git a/packages/react/src/components/F0VideoPlayer/components/AudioDescriptionToggleIcons.tsx b/packages/react/src/components/F0VideoPlayer/components/AudioDescriptionToggleIcons.tsx
index 4f6a039414..f8a3e396cd 100644
--- a/packages/react/src/components/F0VideoPlayer/components/AudioDescriptionToggleIcons.tsx
+++ b/packages/react/src/components/F0VideoPlayer/components/AudioDescriptionToggleIcons.tsx
@@ -42,6 +42,7 @@ export const AudioDescriptionLineIcon = forwardRef<
       vectorEffect="non-scaling-stroke"
     />
     
+      {/* i18n-exempt: glyph in the icon artwork, like the CC badge */}
       AD
     
   
@@ -67,6 +68,7 @@ export const AudioDescriptionFilledIcon = forwardRef<
       
         
         
+          {/* i18n-exempt: glyph in the icon artwork, like the CC badge */}
           AD
         
       
diff --git a/packages/react/src/lib/xray.tsx b/packages/react/src/lib/xray.tsx
index e866c169ad..d4875bc483 100644
--- a/packages/react/src/lib/xray.tsx
+++ b/packages/react/src/lib/xray.tsx
@@ -66,6 +66,7 @@ export const XRayProvider: React.FC<{ children: ReactNode }> = ({
         typeof document !== "undefined" &&
         createPortal(
           
+ {/* i18n-exempt: name of the dev-only inspector overlay */}
XRay
{componentTypes.map((type) => (