From c6dcc4c3d86d9b82c0fc5b3ee8b6d6d691dece66 Mon Sep 17 00:00:00 2001 From: Andrew Polk Date: Tue, 25 Aug 2026 10:55:29 -0700 Subject: [PATCH 1/2] Send the Comic Book readmes' missing sections to Crowdin (BL-16750) Three whole sections of the Paper Comic Book and Digital Comic Book template readmes had no {i18n="..."} annotations, so 26 strings had never been sent to Crowdin and showed in English whatever UI language the user chose: ## Limitations of Bloom's current comic book support ## Tips on using the Canvas Toolbox ## A note on White on Black Text ...along with all of their bullets and paragraphs. The sections either side of them are annotated, so these were almost certainly added later and the annotations forgotten. The text is identical in both readmes, so it gets shared comic.template.* ids rather than per-readme ones, per the note at the top of each file: Crowdin translates each string once and both readmes pick it up. Also annotated the four "n/a" cells in the Leveled Reader table, which sit in raw HTML whose header cells already carried inline i18n attributes; all four hold the same text, so they share one id. Regenerating the English XLIFFs adds 27 trans-units. Nothing was lost -- `--extract --preserve` keeps a unit that is no longer in the HTML, so the "Obsolete as of 6.3" full-bleed unit survives, moved to the end of the file. Nothing was catching this. The extractor only picks up blocks carrying an i18n attribute; text without one is skipped at every stage, and since the extractor writes the same XLIFF either way, regenerating it produces no diff. So this also brings in check-readme-i18n.js, which found the bug, and wires it into build-prod with --strict so it cannot come back. Verified both ways: it fails (exit 1) on a heading with the annotation removed, and passes on all ten readmes as they now stand. Co-Authored-By: Claude Opus 5 (1M context) --- .../Digital Comic Book/ReadMe-en.xlf | 65 ++++++ .../localization/Leveled Reader/ReadMe-en.xlf | 5 + .../Paper Comic Book/ReadMe-en.xlf | 79 ++++++- src/BloomBrowserUI/package.json | 4 +- .../scripts/check-readme-i18n.js | 211 ++++++++++++++++++ .../Digital Comic Book/ReadMe-en.md | 26 +-- .../Leveled Reader/ReadMe-en.md | 8 +- .../Paper Comic Book/ReadMe-en.md | 26 +-- 8 files changed, 386 insertions(+), 38 deletions(-) create mode 100644 src/BloomBrowserUI/scripts/check-readme-i18n.js diff --git a/DistFiles/localization/Digital Comic Book/ReadMe-en.xlf b/DistFiles/localization/Digital Comic Book/ReadMe-en.xlf index b92668c06401..6d5a670cb692 100644 --- a/DistFiles/localization/Digital Comic Book/ReadMe-en.xlf +++ b/DistFiles/localization/Digital Comic Book/ReadMe-en.xlf @@ -17,6 +17,61 @@ ID: comic.template.digital.use + + Limitations of Bloom's current comic book support + + ID: comic.template.limitations.header + + + As with other text, Bloom's Talking Book Tool allows recording audio for comic bubbles. The checkbox "Show Playback Order buttons" in the Talking Book Tool enables you to define the playback order of the bubbles. + + ID: comic.template.limitations.audio + + + If you change the paper-size or layout of a book, you may have to adjust the locations of bubbles. + + ID: comic.template.limitations.bubblelocations + + + The text in bubbles is currently limited to a rectangle, rather than conforming to the actual outlines of the bubble. You can use <enter> to manually break lines. Your comic will look more professional if you take the time to resize bubbles to be as tight as possible. + + ID: comic.template.limitations.rectangle + + + Comic books can show only one language on an image at the same time. + + ID: comic.template.limitations.onelanguage + + + If a comic book contains multiple languages (as sources), users can switch between languages in Bloom Reader, the web, etc. + + ID: comic.template.limitations.switchlanguages + + + Tips on using the Canvas Toolbox + + ID: comic.template.tips.canvas.header + + + To move a bubble, click on it once, then drag anywhere in the bubble. + + ID: comic.template.tips.canvas.move + + + To edit the text in a bubble, click on it once a second time. + + ID: comic.template.tips.canvas.edit + + + To resize a bubble, grab the resize handle on either side. + + ID: comic.template.tips.canvas.resize + + + In the rare case that you need to change the curve of a bubble tail, drag the circle that is in the middle of the tail. It will become a solid color to indicate that you have left "auto mode". To return to "auto mode", double click that circle. + + ID: comic.template.tips.canvas.tail + Tips on "lettering" @@ -27,6 +82,16 @@ ID: comic.template.tips.lettering + + A note on White on Black Text + + ID: comic.template.whiteonblack.header + + + You can use "Change Layout" to divide the screen and add a text box on the side. The background will be black, so you'll need to select the style "WhiteText". + + ID: comic.template.whiteonblack + \ No newline at end of file diff --git a/DistFiles/localization/Leveled Reader/ReadMe-en.xlf b/DistFiles/localization/Leveled Reader/ReadMe-en.xlf index f4103656b707..a27ef2a8b349 100644 --- a/DistFiles/localization/Leveled Reader/ReadMe-en.xlf +++ b/DistFiles/localization/Leveled Reader/ReadMe-en.xlf @@ -57,6 +57,11 @@ ID: decodable.stage + + n/a + + ID: leveled.decodable.notapplicable + \ No newline at end of file diff --git a/DistFiles/localization/Paper Comic Book/ReadMe-en.xlf b/DistFiles/localization/Paper Comic Book/ReadMe-en.xlf index 0f5b12bf31be..5f55217fabc6 100644 --- a/DistFiles/localization/Paper Comic Book/ReadMe-en.xlf +++ b/DistFiles/localization/Paper Comic Book/ReadMe-en.xlf @@ -22,17 +22,66 @@ ID: comic.template.paper.full.bleed.header - - This template tells Bloom that this book is a candidate for "full bleed" printing by a printshop. This feature is available with a Bloom subscription. If this feature is enabled, then in the Edit tab, you will see semi-opaque borders. The outer one is the "bleed"; this is the area that will probably be cut off by a printshop. The inner border is an area that might be cut off, so you should avoid putting things in there that would look really bad if they were cut off. To actually get a PDF that is ready for this commercial printing, please see the "PDF Options" under the Publish Tab. - - ID: comic.template.paper.full.bleed - Obsolete as of 6.3 - This template works best with "full bleed" printing by a printshop. This feature is available with a Bloom subscription. If this feature is enabled in book settings, then in the Edit tab, you will see semi-opaque borders. The outer one is the "bleed"; this is the area that will probably be cut off by a printshop. The inner border is an area that might be cut off, so you should avoid putting things in there that would look really bad if they were cut off. To actually get a PDF that is ready for this commercial printing, please see the "PDF Options" under the Publish Tab. ID: comic.template.paper.full.bleed.v2 + + Limitations of Bloom's current comic book support + + ID: comic.template.limitations.header + + + As with other text, Bloom's Talking Book Tool allows recording audio for comic bubbles. The checkbox "Show Playback Order buttons" in the Talking Book Tool enables you to define the playback order of the bubbles. + + ID: comic.template.limitations.audio + + + If you change the paper-size or layout of a book, you may have to adjust the locations of bubbles. + + ID: comic.template.limitations.bubblelocations + + + The text in bubbles is currently limited to a rectangle, rather than conforming to the actual outlines of the bubble. You can use <enter> to manually break lines. Your comic will look more professional if you take the time to resize bubbles to be as tight as possible. + + ID: comic.template.limitations.rectangle + + + Comic books can show only one language on an image at the same time. + + ID: comic.template.limitations.onelanguage + + + If a comic book contains multiple languages (as sources), users can switch between languages in Bloom Reader, the web, etc. + + ID: comic.template.limitations.switchlanguages + + + Tips on using the Canvas Toolbox + + ID: comic.template.tips.canvas.header + + + To move a bubble, click on it once, then drag anywhere in the bubble. + + ID: comic.template.tips.canvas.move + + + To edit the text in a bubble, click on it once a second time. + + ID: comic.template.tips.canvas.edit + + + To resize a bubble, grab the resize handle on either side. + + ID: comic.template.tips.canvas.resize + + + In the rare case that you need to change the curve of a bubble tail, drag the circle that is in the middle of the tail. It will become a solid color to indicate that you have left "auto mode". To return to "auto mode", double click that circle. + + ID: comic.template.tips.canvas.tail + Tips on "lettering" @@ -43,6 +92,22 @@ ID: comic.template.tips.lettering + + A note on White on Black Text + + ID: comic.template.whiteonblack.header + + + You can use "Change Layout" to divide the screen and add a text box on the side. The background will be black, so you'll need to select the style "WhiteText". + + ID: comic.template.whiteonblack + + + This template tells Bloom that this book is a candidate for "full bleed" printing by a printshop. This feature is available with a Bloom subscription. If this feature is enabled, then in the Edit tab, you will see semi-opaque borders. The outer one is the "bleed"; this is the area that will probably be cut off by a printshop. The inner border is an area that might be cut off, so you should avoid putting things in there that would look really bad if they were cut off. To actually get a PDF that is ready for this commercial printing, please see the "PDF Options" under the Publish Tab. + + ID: comic.template.paper.full.bleed + Obsolete as of 6.3 + - + \ No newline at end of file diff --git a/src/BloomBrowserUI/package.json b/src/BloomBrowserUI/package.json index 403fec8ccd10..439782affe72 100644 --- a/src/BloomBrowserUI/package.json +++ b/src/BloomBrowserUI/package.json @@ -31,13 +31,15 @@ "build": "node scripts/build.js", "build:clean": "node scripts/clean.js", "// 'build-prod': 'production build: clean, pageSizes, then build:ui and build:content, then l10n'": " ", - "build-prod": "pnpm run build:clean && pnpm --dir ../content run build:pageSizes && npm-run-all build:ui build:content && npm-run-all --parallel build:l10n:translate build:l10n:create", + "build-prod": "pnpm run build:clean && pnpm --dir ../content run build:pageSizes && npm-run-all build:ui build:content && npm-run-all --parallel build:l10n:translate build:l10n:create && pnpm run check:readme-i18n:strict", "// 'build:l10n': creates/updates xliff files and translates html files.": " ", "// 'build:l10n': is needed when markdown/html content changes or when testing l10n.": " ", "// 'build:l10n': should be run after build. (build-prod includes this functionality.)": " ", "build:l10n": "node scripts/l10n-build.js", "build:l10n:translate": "node scripts/l10n-build.js translate", "build:l10n:create": "node scripts/l10n-build.js create", + "check:readme-i18n": "node scripts/check-readme-i18n.js", + "check:readme-i18n:strict": "node scripts/check-readme-i18n.js --strict", "build:content": "pnpm run check-that-node-modules-exists-in-content-dir && pnpm --dir ../content run build", "// 'watch': rebuilds bundles when source files change (for entrypoints not yet working with vite dev)": " ", "watch": "vite build --watch", diff --git a/src/BloomBrowserUI/scripts/check-readme-i18n.js b/src/BloomBrowserUI/scripts/check-readme-i18n.js new file mode 100644 index 000000000000..cf6bfa8125cf --- /dev/null +++ b/src/BloomBrowserUI/scripts/check-readme-i18n.js @@ -0,0 +1,211 @@ +#!/usr/bin/env node +/** + * Checks that the readmes of the templates we ship are fully internationalized. + * + * Template readmes are localized as whole files: each `ReadMe-en.htm` is turned into a + * `ReadMe-en.xlf` by `HtmlXliff.exe --extract` (see l10n-build.js), that XLIFF goes to Crowdin, + * and the translations come back and are injected to produce `ReadMe-.htm`. + * + * The weak point is that the extractor only picks up blocks that carry an `i18n` attribute, + * which comes from an `{i18n="some.id"}` annotation on the block in the markdown source. Forget + * that annotation and the text is silently skipped at every stage: it never reaches the XLIFF, + * never reaches Crowdin, never gets translated, and regenerating the XLIFF produces no diff -- + * so nothing reports it. It just stays English forever, in every language. This script is what + * reports it. + * + * Three checks, all mechanical: + * 1. Text in ReadMe-en.htm that no `i18n` attribute covers -> it will never be translated. + * 2. A block with an `i18n` id and real text, but no matching trans-unit in ReadMe-en.xlf + * -> extraction dropped it. + * 3. A trans-unit in ReadMe-en.xlf with no matching block in the HTML, and not marked obsolete + * -> a stale unit still being offered to translators. + * + * Usage: node scripts/check-readme-i18n.js [--strict] + * --strict exit non-zero if anything is reported (for CI); otherwise warn only. + */ + +const { glob } = require("glob"); +const fs = require("fs"); +const path = require("path"); +const { JSDOM } = require("jsdom"); + +// Elements whose text is not prose for a translator. CODE/PRE/KBD/SAMP/VAR matter here +// because the readmes deliberately show literal markdown and file names in backticks. +const kNonProseTags = new Set([ + "SCRIPT", + "STYLE", + "HEAD", + "TITLE", + "CODE", + "PRE", + "KBD", + "SAMP", + "VAR", +]); + +/** + * Whether a piece of text is something a translator would actually translate. Numbers, symbols + * and lone punctuation are not -- the Leveled Reader readme, for instance, has a table of stage + * and level numbers, and flagging every cell of it would bury the real findings. + */ +function isTranslatableText(text) { + return /\p{L}/u.test(text); +} + +/** + * The English XLIFF that l10n-build.js generates for a given readme. + */ +function xliffPathForReadme(htmPath) { + const templateName = path.basename(path.dirname(htmPath)); + return path.join( + "../../DistFiles/localization", + templateName, + "ReadMe-en.xlf", + ); +} + +/** + * Ids of the trans-units in an English readme XLIFF, split by whether the unit is marked + * obsolete. We deliberately keep obsolete units rather than deleting them (see + * DistFiles/localization/README.md), so they must not be reported as stale. + */ +function readXliffUnitIds(xliffPath) { + const live = new Set(); + const obsolete = new Set(); + if (!fs.existsSync(xliffPath)) return { live, obsolete, missing: true }; + const xml = fs.readFileSync(xliffPath, "utf8"); + const unitRegex = /]*\bid="([^"]*)"[\s\S]*?<\/trans-unit>/g; + let match; + while ((match = unitRegex.exec(xml)) !== null) { + if (/\s*Obsolete\b/i.test(match[0])) obsolete.add(match[1]); + else live.add(match[1]); + } + return { live, obsolete, missing: false }; +} + +/** + * Every text node in the body that a translator would need, paired with the nearest ancestor + * carrying an `i18n` attribute (null when nothing covers it). + */ +function collectTextNodes(document) { + const results = []; + const walk = (node) => { + for (const child of node.childNodes) { + if (child.nodeType === 3) { + const text = child.textContent.trim(); + if (text && isTranslatableText(text)) + results.push({ text, node }); + continue; + } + if (child.nodeType !== 1) continue; + if (kNonProseTags.has(child.tagName)) continue; + walk(child); + } + }; + walk(document.body); + return results.map((r) => { + let el = r.node; + while (el && el.nodeType === 1) { + if (el.hasAttribute && el.hasAttribute("i18n")) + return { text: r.text, id: el.getAttribute("i18n") }; + el = el.parentNode; + } + return { text: r.text, id: null }; + }); +} + +function shorten(text) { + const oneLine = text.replace(/\s+/g, " "); + return oneLine.length > 70 ? oneLine.slice(0, 70) + "..." : oneLine; +} + +function checkReadme(htmPath) { + const problems = []; + const html = fs.readFileSync(htmPath, "utf8"); + const { document } = new JSDOM(html).window; + const texts = collectTextNodes(document); + + const xliffPath = xliffPathForReadme(htmPath); + const { live, obsolete, missing } = readXliffUnitIds(xliffPath); + if (missing) { + problems.push( + `no English XLIFF at ${xliffPath} -- nothing in this readme is being translated`, + ); + return problems; + } + + // 1. Text that no i18n attribute covers. + for (const t of texts) { + if (t.id === null) + problems.push( + `text has no i18n id, so it will never be translated: "${shorten(t.text)}"\n` + + ` Fix: add an {i18n="some.unique.id"} annotation to that block in the ReadMe-en.md source.`, + ); + } + + // 2. Ids present in the HTML with real text, but no trans-unit for them. + const idsWithText = new Set(texts.filter((t) => t.id).map((t) => t.id)); + for (const id of idsWithText) { + if (!live.has(id) && !obsolete.has(id)) + problems.push( + `i18n id "${id}" has text in the readme but no trans-unit in ReadMe-en.xlf -- extraction dropped it`, + ); + } + + // 3. Live trans-units that no longer correspond to anything in the HTML. + const allHtmlIds = new Set( + Array.from(document.querySelectorAll("[i18n]")).map((e) => + e.getAttribute("i18n"), + ), + ); + for (const id of live) { + if (!allHtmlIds.has(id)) + problems.push( + `trans-unit "${id}" is in ReadMe-en.xlf but not in the readme -- either restore it ` + + `or mark it obsolete with a Obsolete as of X.Y`, + ); + } + + return problems; +} + +function main() { + const strict = process.argv.includes("--strict"); + const readmes = glob + .sync("../../output/browser/templates/**/ReadMe-en.htm") + .sort(); + + if (readmes.length === 0) { + console.error( + "No ReadMe-en.htm found under output/browser/templates. Run the content/markdown " + + "build first (this checks the generated readmes, which is what the extractor sees).", + ); + process.exit(strict ? 1 : 0); + } + + console.log(`Checking i18n of ${readmes.length} template readmes`); + let total = 0; + for (const htmPath of readmes) { + const problems = checkReadme(htmPath); + const name = path.basename(path.dirname(htmPath)); + if (problems.length === 0) { + console.log(` ok ${name}`); + continue; + } + total += problems.length; + console.log(` !! ${name}`); + for (const p of problems) console.log(` - ${p}`); + } + + if (total === 0) { + console.log("\nAll template readmes are fully internationalized."); + return; + } + console.log( + `\n${total} problem(s) found. These strings will never reach Crowdin, so they stay ` + + `English in every language.`, + ); + if (strict) process.exit(1); +} + +main(); diff --git a/src/content/templates/template books/Digital Comic Book/ReadMe-en.md b/src/content/templates/template books/Digital Comic Book/ReadMe-en.md index dc60438c4ab6..72542c88b998 100644 --- a/src/content/templates/template books/Digital Comic Book/ReadMe-en.md +++ b/src/content/templates/template books/Digital Comic Book/ReadMe-en.md @@ -10,26 +10,26 @@ Note: The Canvas Tool requires that you have a valid 1 4 1 5 1 6 - 2 n/a - 3 n/a - 4 n/a - 5 n/a + 2 n/a + 3 n/a + 4 n/a + 5 n/a diff --git a/src/content/templates/template books/Paper Comic Book/ReadMe-en.md b/src/content/templates/template books/Paper Comic Book/ReadMe-en.md index b15fa7f3a3dc..ec65689571fb 100644 --- a/src/content/templates/template books/Paper Comic Book/ReadMe-en.md +++ b/src/content/templates/template books/Paper Comic Book/ReadMe-en.md @@ -14,26 +14,26 @@ Use this template as a starting point for comics designed for print. The size wi This template works best with "[full bleed](https://docs.bloomlibrary.org/full-bleed)" printing by a printshop. This feature is available with a Bloom subscription. If this feature is enabled in book settings, then in the Edit tab, you will see semi-opaque borders. The outer one is the "bleed"; this is the area that will probably be cut off by a printshop. The inner border is an area that *might* be cut off, so you should avoid putting things in there that would look really bad if they were cut off. To actually get a PDF that is ready for this commercial printing, please see the "PDF Options" under the Publish Tab. {i18n="comic.template.paper.full.bleed.v2"} -## Limitations of Bloom's current comic book support +## Limitations of Bloom's current comic book support {i18n="comic.template.limitations.header"} -- As with other text, Bloom's Talking Book Tool allows recording audio for comic bubbles. The checkbox "Show Playback Order buttons" in the Talking Book Tool enables you to define the playback order of the bubbles. -- If you change the paper-size or layout of a book, you may have to adjust the locations of bubbles. -- The text in bubbles is currently limited to a rectangle, rather than conforming to the actual outlines of the bubble. You can use <enter> to manually break lines. Your comic will look more professional if you take the time to resize bubbles to be as tight as possible. -- Comic books can show only one language on an image at the same time. -- If a comic book contains multiple languages (as sources), users can switch between languages in Bloom Reader, the web, etc. +- As with other text, Bloom's Talking Book Tool allows recording audio for comic bubbles. The checkbox "Show Playback Order buttons" in the Talking Book Tool enables you to define the playback order of the bubbles.{i18n="comic.template.limitations.audio"} +- If you change the paper-size or layout of a book, you may have to adjust the locations of bubbles.{i18n="comic.template.limitations.bubblelocations"} +- The text in bubbles is currently limited to a rectangle, rather than conforming to the actual outlines of the bubble. You can use <enter> to manually break lines. Your comic will look more professional if you take the time to resize bubbles to be as tight as possible.{i18n="comic.template.limitations.rectangle"} +- Comic books can show only one language on an image at the same time.{i18n="comic.template.limitations.onelanguage"} +- If a comic book contains multiple languages (as sources), users can switch between languages in Bloom Reader, the web, etc.{i18n="comic.template.limitations.switchlanguages"} -## Tips on using the Canvas Toolbox +## Tips on using the Canvas Toolbox {i18n="comic.template.tips.canvas.header"} -- To move a bubble, click on it once, then drag anywhere in the bubble. -- To edit the text in a bubble, click on it once a second time. -- To resize a bubble, grab the resize handle on either side. -- In the rare case that you need to change the curve of a bubble tail, drag the circle that is in the middle of the tail. It will become a solid color to indicate that you have left "auto mode". To return to "auto mode", double click that circle. +- To move a bubble, click on it once, then drag anywhere in the bubble.{i18n="comic.template.tips.canvas.move"} +- To edit the text in a bubble, click on it once a second time.{i18n="comic.template.tips.canvas.edit"} +- To resize a bubble, grab the resize handle on either side.{i18n="comic.template.tips.canvas.resize"} +- In the rare case that you need to change the curve of a bubble tail, drag the circle that is in the middle of the tail. It will become a solid color to indicate that you have left "auto mode". To return to "auto mode", double click that circle.{i18n="comic.template.tips.canvas.tail"} ## Tips on "lettering" {i18n="comic.template.tips.lettering.header"} *"Lettering"* is the term used for adding bubbles and text to comics. Take some time to learn what professional *letterers* say about making good-looking comics. Bloom takes care of some of these things for you, but there are many things that require training your eye. See https://blambot.com/pages/lettering-tips. Note, you do *not* need to only use upper case letters. It does look cool because we are used to seeing comics done this way, but you should do whatever is best for your audience. If you do choose upper case *and* have a script that is widely supported, consider using a special comic book font. See [here](https://blambot.com/collections/all-fonts/dialogue) and [here](https://jasonthibault.com/comic-book-fonts/). {i18n="comic.template.tips.lettering"} -## A note on White on Black Text +## A note on White on Black Text {i18n="comic.template.whiteonblack.header"} -You can use "Change Layout" to divide the screen and add a text box on the side. The background will be black, so you'll need to select the style "WhiteText". +You can use "Change Layout" to divide the screen and add a text box on the side. The background will be black, so you'll need to select the style "WhiteText".{i18n="comic.template.whiteonblack"} From 6f817b43d28030f145d290148f2502bb36397e63 Mon Sep 17 00:00:00 2001 From: Andrew Polk Date: Tue, 25 Aug 2026 10:59:18 -0700 Subject: [PATCH 2/2] Also catch one i18n id shared by blocks whose text differs Sharing an id between blocks that say the same thing is deliberate and useful -- this PR does exactly that for the Leveled Reader's four "n/a" cells, so the string is translated once. But the XLIFF holds one source per id, so if the texts ever differ, one block silently displays the other's translation. That is now the check's third rule, guarding the technique the rest of this PR relies on. Verified: changing one of those four cells to "not applicable" makes the check fail with both texts named; restoring it passes. Co-Authored-By: Claude Opus 5 (1M context) --- .../scripts/check-readme-i18n.js | 31 +++++++++++++++++-- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/BloomBrowserUI/scripts/check-readme-i18n.js b/src/BloomBrowserUI/scripts/check-readme-i18n.js index cf6bfa8125cf..18340e028c57 100644 --- a/src/BloomBrowserUI/scripts/check-readme-i18n.js +++ b/src/BloomBrowserUI/scripts/check-readme-i18n.js @@ -13,11 +13,13 @@ * so nothing reports it. It just stays English forever, in every language. This script is what * reports it. * - * Three checks, all mechanical: + * Four checks, all mechanical: * 1. Text in ReadMe-en.htm that no `i18n` attribute covers -> it will never be translated. * 2. A block with an `i18n` id and real text, but no matching trans-unit in ReadMe-en.xlf * -> extraction dropped it. - * 3. A trans-unit in ReadMe-en.xlf with no matching block in the HTML, and not marked obsolete + * 3. One id on two blocks whose text differs -> one of them will show the other's + * translation (sharing an id for identical text is fine, and is used deliberately). + * 4. A trans-unit in ReadMe-en.xlf with no matching block in the HTML, and not marked obsolete * -> a stale unit still being offered to translators. * * Usage: node scripts/check-readme-i18n.js [--strict] @@ -152,7 +154,30 @@ function checkReadme(htmPath) { ); } - // 3. Live trans-units that no longer correspond to anything in the HTML. + // 3. One id used by two blocks whose text differs. Sharing an id between blocks that say + // the same thing is deliberate and useful -- the Leveled Reader's "n/a" cells do it, so the + // string is translated once -- but the XLIFF holds one source per id, so if the texts differ + // one block silently ends up displaying the other's translation. + const textsById = new Map(); + for (const el of document.querySelectorAll("[i18n]")) { + const text = el.textContent.replace(/\s+/g, " ").trim(); + if (!text) continue; + const id = el.getAttribute("i18n"); + if (!textsById.has(id)) textsById.set(id, new Set()); + textsById.get(id).add(text); + } + for (const [id, texts] of textsById) { + if (texts.size > 1) + problems.push( + `i18n id "${id}" is used by ${texts.size} blocks with different text, so all but ` + + `one will show the wrong translation -- give them separate ids: ` + + Array.from(texts) + .map((t) => `"${shorten(t)}"`) + .join(" vs "), + ); + } + + // 4. Live trans-units that no longer correspond to anything in the HTML. const allHtmlIds = new Set( Array.from(document.querySelectorAll("[i18n]")).map((e) => e.getAttribute("i18n"),