From f4d4eb72ad5639689b8a45ccdbfa47258ed902ac Mon Sep 17 00:00:00 2001 From: Lucy Macartney Date: Mon, 19 Oct 2026 19:48:52 +0100 Subject: [PATCH 1/2] Rework translate skills into one flow with proofreading and review Add a proofread skill and status.js, make check-translation.js check page structure against the English, run the review in a fresh subagent inside /translate, and put the proofreading fixes in the locale PR. --- .agents/skills/proofread/SKILL.md | 68 ++++++ .agents/skills/review-translation/SKILL.md | 64 +++--- .agents/skills/translate/SKILL.md | 122 ++++++++--- .agents/skills/translate/check-translation.js | 135 +++++++++++- .agents/skills/translate/pages.md | 104 ++++----- .agents/skills/translate/status.js | 203 ++++++++++++++++++ .agents/skills/update-content/SKILL.md | 3 +- AGENTS.md | 14 +- 8 files changed, 584 insertions(+), 129 deletions(-) create mode 100644 .agents/skills/proofread/SKILL.md create mode 100644 .agents/skills/translate/status.js diff --git a/.agents/skills/proofread/SKILL.md b/.agents/skills/proofread/SKILL.md new file mode 100644 index 00000000000..e93ce16caf5 --- /dev/null +++ b/.agents/skills/proofread/SKILL.md @@ -0,0 +1,68 @@ +--- +name: proofread +description: + Fixes basic typos in English docs pages without asking (spelling, doubled + words, punctuation, stray characters, admonition syntax) and reports anything + else that looks wrong. Never changes meaning, code, links, or app names. Does + not commit or open a PR. Use before translating, after editing a page, or to + sweep a section for typos. +--- + +# Proofread + +Fix basic typos in English pages in `docs/`, and report the rest. The scope is +one page, one folder under `docs/`, or one sidebar category. With no scope, +proofread the pages this branch changes. Never proofread `adaptors/packages/` or +anything in `i18n/`. + +A fix here is safe to make without asking because it cannot change what a page +says. Anything that could goes to a person. + +## Fix without asking + +- A misspelled word, or a word written twice. +- A missing full stop, a doubled punctuation mark, or an unmatched bracket or + quote. +- British spellings and "adapter", which `STYLE.md` rules out, except in code or + names. +- A stray character, or an admonition written against `STYLE.md`: `::: tip` for + `:::tip`, a closing `:::note` for `:::`, or a missing blank line inside the + colons. Report an admonition with no closing `:::`, since where it ends + changes what it holds. + +Change only the characters that are wrong, so the diff shows just the fix. + +## Never touch + +- Code blocks, inline code, link targets, and numbers. A typo in link text is + fine to fix. +- The names of things in the app, even when they look misspelled. Check the + English in the Lightning code (see `AGENTS.md`) before you report one. +- Front matter. + +Never delete a line, even one that looks like a mistake. A line that seems out +of place in an example may be there on purpose. + +## Report the rest + +Anything else that looks wrong goes in the report, not the page: an unclear +sentence, a wrong fact, a broken rule in `STYLE.md`, or an app name in the wrong +form. + +## Finish + +Do not run Prettier; whoever commits the fixes formats them first. Report in the +chat, in this order, placing each item by its page URL (such as +`http://localhost:3000/documentation/build/triggers`) and section heading: + +1. **Needs a decision.** The sentence in full, what looks wrong, and the fix you + would make. +2. **Fixed.** Each sentence before and after, in full, with the changed words in + bold. +3. **Translations affected.** Translated pages of the pages you changed. They + show as stale in `translate/status.js` until the next `/translate`, which + usually only needs to update their hash. + +Do not commit or open a PR. When you were called by another skill, return the +report to it. To open a PR from a sweep, follow "Finish" in +`update-content/SKILL.md`, with only the proofreading fixes in it. diff --git a/.agents/skills/review-translation/SKILL.md b/.agents/skills/review-translation/SKILL.md index db0d76de616..56d898732b1 100644 --- a/.agents/skills/review-translation/SKILL.md +++ b/.agents/skills/review-translation/SKILL.md @@ -3,27 +3,35 @@ name: review-translation description: Checks translated docs pages against the English in a fresh session, fixes clear problems (changed meaning, literal phrasing, broken house style rules), - and reports the rest. Does not commit or open a PR. Use after /translate - pages, once the section is committed. + and reports the rest. Commits its fixes on their own; does not push or open a + PR. /translate runs it in a fresh subagent; run it yourself to review pages + already translated. disable-model-invocation: true --- # Review a translation -Run `/review-translation ` in a fresh session, not the one that -translated the pages: a translator is poor at spotting its own mistakes. Commit -the translation first, so the review's fixes show up as their own diff. The -scope is the same as for `/translate pages`: one page, one folder under `docs/`, -or one sidebar category. If no scope is given, ask. +Run `/review-translation ` in a fresh session or subagent, not the one +that translated the pages: a translator is poor at spotting its own mistakes. +`/translate` starts one for you when it finishes. The scope is the same as for +`/translate pages`: one page, one folder under `docs/`, or one sidebar category. +If no scope is given, review the translated pages this branch changes +(`git diff --name-only origin/main...HEAD -- i18n/`), and ask only if there are +none. The check takes the English paths, so drop the +`i18n//docusaurus-plugin-content-docs/current/` prefix and put `docs/` +in its place. + +The translation must be committed first, so the review's fixes show as their own +diff. If a page in scope has uncommitted changes, ask. The review checks against the rules the translator followed: "How to translate" in `.agents/skills/translate/SKILL.md`, the house style in `.agents/skills/translate/.md`, the rules for the locale in `translation-rules.yml`, and `glossary.yml`. -The review fixes clear problems in `machine` pages and reports the rest. It does -not commit, open a PR, or add rules. Never edit a `human-reviewed` page or a -fenced block; report the problem instead. +The review fixes clear problems in `machine` pages, commits them, and reports +the rest. It does not push, open a PR, or add rules. Never edit a +`human-reviewed` page or a fenced block; report the problem instead. ## 1. Lay out the pages @@ -40,12 +48,13 @@ reading for them: - Run the check: ```bash - node .agents/skills/translate/check-translation.js docs/.md... + node .agents/skills/translate/check-translation.js docs/... ``` - Each block it lists is a candidate, not a verdict: read it against the - English. It skips rules with their context in brackets, like "upgrade (a - plan)", so search for those by hand. + Each item it lists is a candidate, not a verdict: read it against the English. + It also checks code, links, headings, anchors, and front matter against the + English, so you don't need to count them. It skips rules with their context in + brackets, like "upgrade (a plan)", so search for those by hand. - Punctuation `.md` rules out, such as curly quotes. - English terms kept from `glossary.yml` written with a capital mid-sentence, if @@ -64,8 +73,9 @@ Check that the translation: forms that read as "usted". - **Keeps names of things in the app** exactly as in the English, and keeps links and inline code the same. -- **Is consistent across the scope.** The same English term gets the same - translation on every page. +- **Is consistent with the rest of the locale.** The same English term gets the + same translation on every page. Search all of `i18n//` for the other + translations of a term, not only the pages in scope. Do not flag wording you would only have written differently. If the English itself is wrong or unclear, note it under "Problems in the English" and leave @@ -76,25 +86,29 @@ the translation matching it. Fix a block when the problem and the fix are both clear. Change only the words that are wrong, so the diff shows just the fix. Leave the front matter alone. Then run `yarn prettier --write ` on the pages you changed, and run the -check again. +check again. Commit the fixes on their own, such as "Review es translation of +build". Do not push. ## 5. Report -Build the site and serve it (`yarn serve`), so each item can point to the page -as the reader sees it. Report in the chat. Place each item by the page URL, such -as `http://localhost:3000/es/documentation/build/triggers`, and the section -heading, not by file and line. Group the items like this: +Report in the chat. Do not build or serve the site first; the report should not +wait on it. Place each item by the page URL the check prints, such as +`http://localhost:3000/es/documentation/build/triggers`, and the section +heading, not by file and line. Group the items in this order: +- **Needs a decision.** Problems you were not sure how to fix, or where the fix + changes meaning. Give the English sentence and the current translation in + full, and the options as full sentences, with the one you recommend first. - **Fixed.** For each fix, give the English sentence, the translation before, and the translation after, all in full, with the changed words in bold. Then say why in one line. -- **Needs a decision.** Problems you were not sure how to fix, or where the fix - changes meaning. Quote the English and the current translation. -- **Problems in the English.** +- **Problems in the English.** The same format. Do not fix them; they are + follow-ups for a later English PR. - **Suggested rules.** A problem found on more than one page, or likely to come back in later sections, with the line you would add to `.md` or `translation-rules.yml`. Add it only if asked. -- **Page to spot-check.** The page with the most fixes, with its URL. +- **Page to spot-check.** The page with the most fixes, with its URL. To see it, + the user runs `yarn build` and `yarn serve`. ## Marking a page human-reviewed diff --git a/.agents/skills/translate/SKILL.md b/.agents/skills/translate/SKILL.md index 888028d10e2..03438bbb540 100644 --- a/.agents/skills/translate/SKILL.md +++ b/.agents/skills/translate/SKILL.md @@ -5,8 +5,8 @@ description: docusaurus.config.js, either a set of English pages or the interface text (navbar, footer, sidebar headings, homepage). Respects glossary.yml, translation-rules.yml, each locale's house style, review status, and - do-not-retranslate fences, and opens one PR per locale. Use when asked to - translate or refresh translations. + do-not-retranslate fences, runs a review in a fresh subagent, and opens one PR + per locale when asked. Use when asked to translate or refresh translations. disable-model-invocation: true --- @@ -28,25 +28,49 @@ need. `sidebars-main.js`, the navbar or footer in `docusaurus.config.js`, or any `` text in `src/` has changed. See `interface.md`. -If you are not told which task, work out which ones are needed from what has -changed in English, say so, and ask before starting. +If you are not told which task, run `status.js` (below) with no scope. It lists +the pages that need work and whether the interface text may have changed. Say +what it found and ask before starting. Never translate the generated adaptor pages, or articles and blog posts. ## Before you start -Check these five things. If any fails, stop and ask. +```bash +node .agents/skills/translate/status.js [docs/...] +``` + +It runs the checks before translating, then lists each page in scope as missing, +stale, up to date, or orphaned, with the diff to read, the hash to record, and +the page's local URL. For a sidebar category, pass its files or folder. If a +check fails, stop and ask. Do not enable a locale yourself; that changes what +gets deployed. If the branch is behind `main`, ask to merge `main` in first. If +your branch comes off another branch, merge `main` into that one, then that one +into yours. + +## Make the locale branch + +Make a branch from the current one, named after it plus the locale, such as +`fix-alerts-es`. Everything below is committed on it, and becomes one PR. + +## Fix the English first -- The locale is enabled in `docusaurus.config.js`. Do not enable it yourself; - that changes what gets deployed. -- `i18n/` is not in `.gitignore`. -- `glossary.yml` and `translation-rules.yml` are valid YAML. -- The locale has a house style guide, `.md`, in this folder. -- Your branch has everything on `main`. Run `git fetch origin main` and then - `git merge-base --is-ancestor origin/main HEAD`. If it fails, the English you - would translate is out of date, and the hashes you record will not match - `main`. Ask to merge `main` in first. If your branch comes off another branch, - merge `main` into that one, then that one into yours. +Settle the English before you translate it. A fix after translating makes the +translation stale. Follow `.agents/skills/proofread/SKILL.md` on the pages +`status.js` lists as missing or stale. Leave up-to-date pages alone; a fix there +would make their translation stale. It fixes basic typos without asking and +reports the rest. If it reports anything, stop and ask the user now, and apply +the fixes they agree to. + +If the English changed, run `yarn prettier --write` on those pages and commit +them on their own, before you translate. Do not push. The fixes change the +English hashes, so run `status.js` again and use the hashes it prints now. Then +translate from the fixed English, and list every fix in the report. + +Once the English is committed, leave it alone for the rest of the run. Problems +found later, such as by the review, are follow-ups for the PR description. When +the English is fixed in a later PR, the next `/translate` finds the page stale +and updates only the changed blocks. ## How to translate @@ -66,10 +90,25 @@ These apply to both tasks. - Keep the names of things in the app, like buttons, menus, tabs, and field labels, exactly as they are in the English. The app is English only, so a translated button name points the reader at a button that does not exist. +- Before choosing a word for a term, search the existing translations for it, so + the same English gets the same translation on every page. + +For more than three pages, split them across subagents, two or three pages each, +grouped by folder. Give each its pages, the paths to this file and `pages.md`, +and the status line for each page. Each writes its pages and runs the check. + +## Review + +When the pages are written and the check is clean, commit them on the locale +branch. Do not push. Then run `/review-translation` on the same scope in a new +subagent: read `.agents/skills/review-translation/SKILL.md` and pass it to a +fresh agent, not a fork. A translator is poor at spotting its own mistakes, so +the reviewer must not share your reasoning. It commits its fixes on their own, +so the translation and the review show as separate diffs. -## Before you open the PR +## Build -Build the site and make sure it passes: +Build once the review has committed its fixes, so the build covers them. ```bash yarn generate-adaptors @@ -78,14 +117,41 @@ yarn build Build the whole site, not just your locale. `yarn build --locale ` builds the locale at the site root, so every correct `/es/...` link shows up as -broken. - -Open one PR per locale, separate from the English PR. Commit each locale on its -own branch, created from the current one and named after it plus the locale, -such as `fix-alerts-es`. If the English is not on `main` yet, open the PR -against the English branch and say so in the description. Translated files do -not count toward the 20-file limit, because a locale's translations are reviewed -as a set. In the PR description, say which tool and model translated the text. If -you spot a problem in the English while translating, list it in the PR -description under "Problems in the English", with the file and line. Do not fix -it in this PR. +broken. If `yarn generate-adaptors` hits GitHub's rate limit and +`adaptors/packages/` already exists, build without it. If the build fails on a +sidebar id that no longer exists, run `yarn clear` and build again. + +## Report + +Report in the chat, in this order. Place each item by its page URL and section +heading, not by file and line. + +1. **Needs a decision.** Review items you need the user to decide, and OpenFn + feature names you translated that are not in `glossary.yml` (with the word + you chose). For each, give the English sentence, and the translation before + and after, in full. +2. **Fixed.** Fixes made to the English, then fixes made by the review. The same + format, with one line on why. +3. **Problems in the English.** What the review found in the English, left for a + later PR. +4. **What changed.** For each page, how many blocks changed and how many needed + new text. +5. **Page to spot-check.** The page the review fixed most, with its URL. +6. **Status.** The build result, the branch, the commits (English, translation, + review), and what is uncommitted. + +## Push and open the PR + +Push and open the PR only when the user says to. Open it against the branch the +locale branch was made from. The PR holds three commits: the English fixes, the +translation, and the review. Say in the description that it includes English +fixes, so an English reviewer looks at that commit. + +While only one locale is enabled, the English fixes go in the locale PR. Once +there are two, put them in their own PR first, so each locale PR does not carry +the same English commit. + +Translated files do not count toward the 20-file limit, because a locale's +translations are reviewed as a set. In the PR description, say which tool and +model translated the text, and list anything left from "Needs a decision" and +"Problems in the English". diff --git a/.agents/skills/translate/check-translation.js b/.agents/skills/translate/check-translation.js index 25766875917..3bf2ed94b79 100644 --- a/.agents/skills/translate/check-translation.js +++ b/.agents/skills/translate/check-translation.js @@ -1,12 +1,14 @@ #!/usr/bin/env node // Checks translated pages against the English they were translated from. // -// node .agents/skills/translate/check-translation.js es docs/get-started/*.md +// node .agents/skills/translate/check-translation.js es docs/get-started // // Lists blocks where a fixed glossary term appears fewer times in the // translation than in the English, which can mean it was translated. It also // lists blocks that may break a rule for the locale in translation-rules.yml, -// or use a word from the "Not" column in .md. +// or use a word from the "Not" column in .md. And it lists where the +// code, links, headings, anchors, callouts, images, or tables differ from the +// English, or the front matter is incomplete or out of date. // // A block is a run of lines between blank lines, after formatting with // Prettier, and a fenced code block is one block. Both sides go through @@ -115,6 +117,79 @@ function rulesFor(locale) { ]; } +// What a translation copies unchanged or keeps in step with the English: +// code, links, heading levels and anchors, callouts, images, and tables. +// Each difference is a candidate, like the rule checks. +const { writeMarkdownHeadingId } = require('@docusaurus/utils'); +const { url, frontMatter, walk } = require('./status.js'); + +const outsideCode = s => s.replace(/^(```|~~~)[\s\S]*?^\1/gm, ''); +// The site's anchors come from the rendered heading, so drop emphasis first: +// "See _Now_" is "see-now", not "see-_now_". +const anchors = text => + writeMarkdownHeadingId( + splitFrontMatter(text).body.replace(/^##.*$/gm, h => { + if (h.includes('{#')) return h; + const emphasis = /(^|[^\w`])(\*\*|\*|_)([^*_`]+)\2(?=[^\w`]|$)/g; + while (emphasis.test(h)) h = h.replace(emphasis, '$1$3'); + return h; + }) + ) + .split('\n') + .filter(l => /^##/.test(l)) + .map(l => (l.match(/\{#([^}]+)\}$/) || [])[1]); +const parts = { + 'code blocks': s => s.match(/^(```|~~~)[\s\S]*?^\1/gm) || [], + 'inline code': s => outsideCode(s).match(/`[^`\n]+`/g) || [], + links: s => [...outsideCode(s).matchAll(/\]\(\s*([^)\s]+)/g)].map(m => m[1]), + headings: s => outsideCode(s).match(/^#+(?= )/gm) || [], + callouts: s => outsideCode(s).match(/^:::\w*/gm) || [], + images: s => + [ + ...outsideCode(s).matchAll( + /!\[[^\]]*\]\(\s*([^)\s]+)|]*src="([^"]+)/g + ), + ].map(m => m[1] || m[2]), + 'table rows': s => outsideCode(s).match(/^\|/gm) || [], +}; +const sameList = (a, b) => + a.length === b.length && a.every((x, i) => x === b[i]); +const sorted = a => [...a].sort(); + +function structure(english, translation, current, previous) { + const en = splitFrontMatter(english).body; + const tr = splitFrontMatter(translation).body.replace(/\\\{#/g, '{#'); + const found = Object.entries(parts) + .filter(([name, get]) => + name === 'headings' || name === 'code blocks' + ? !sameList(get(en), get(tr)) + : !sameList(sorted(get(en)), sorted(get(tr))) + ) + .map(([name, get]) => { + const missing = get(en).filter(x => !get(tr).includes(x)); + const added = get(tr).filter(x => !get(en).includes(x)); + return `${name} differ from the English (${get(en).length} in English, ${get(tr).length} in translation)${missing.length ? `; missing: ${missing.slice(0, 5).join(' ')}` : ''}${added.length ? `; not in English: ${added.slice(0, 5).join(' ')}` : ''}`; + }); + const want = anchors(english); + // A heading left in English needs no {#id}: its own text gives the anchor. + const got = anchors(tr); + if (!sameList(want, got)) + found.push( + `heading anchors should be, in order: ${want.join(', ')}; translation has: ${got.join(', ')}` + ); + const fm = frontMatter(translation); + if (!fm.translation_review_status) + found.push('front matter has no translation_review_status'); + if (fm.translation_source_hash !== current) + found.push( + `translation_source_hash is not the current English (${current}): update the page, or the hash if the translation already matches` + ); + const fences = s => (s.match(//g) || []).length; + if (previous !== null && fences(translation) < fences(previous)) + found.push('a do-not-retranslate fence was removed since the last commit'); + return found; +} + function splitFrontMatter(text) { const m = text.match(/^---\n([\s\S]*?)\n---\n/); return m @@ -165,12 +240,20 @@ async function page(locale, enPath) { page: rel, error: 'No translation_source_hash in the front matter.', }; - let enBlocks; + // Git only stores the English once it is staged or committed, so an + // uncommitted English fix is read from disk. + let enBlocks, english; try { - const english = execFileSync('git', ['cat-file', '-p', hash], { - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - }); + const onDisk = fs.readFileSync(enPath, 'utf8'); + english = + execFileSync('git', ['hash-object', enPath], { + encoding: 'utf8', + }).trim() === hash + ? onDisk + : execFileSync('git', ['cat-file', '-p', hash], { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }); enBlocks = await blocks(english, enPath); } catch { return { @@ -191,8 +274,25 @@ async function page(locale, enPath) { }, ]; const ruleBreaks = rulesFor(locale); + const git = (...a) => { + try { + return execFileSync('git', a, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + } catch { + return null; + } + }; return { page: rel, + url: url(locale, rel, frontMatter(fs.readFileSync(enPath, 'utf8'))), + structure: structure( + english, + esText, + git('hash-object', enPath), + git('show', `HEAD:${esPath}`) + ), problems: pairs .map(r => ({ ...r, @@ -212,11 +312,23 @@ async function main() { console.error('Usage: check-translation.js ...'); process.exit(1); } + const bad = files.filter(f => !f.startsWith('docs') || !fs.existsSync(f)); + if (bad.length) { + console.error(`Not found: ${bad.join(', ')}. Pass a path under docs/.`); + process.exit(1); + } const flat = s => s.replace(/\s+/g, ' '); let found = 0; - for (const f of files) { + for (const f of files.flatMap(walk)) { const p = await page(locale, f); - if (p.error) console.log(`${p.page}: ${p.error}`); + if (p.error) { + found++; + console.log(`${p.page}: ${p.error}`); + } + for (const s of p.structure || []) { + found++; + console.log(`${p.page}: ${s}\n ${p.url}\n`); + } for (const r of p.problems || []) { found++; console.log( @@ -224,7 +336,10 @@ async function main() { ); } } - if (!found) console.log('No glossary terms missing and no rules broken.'); + if (!found) + console.log( + 'No glossary terms missing, no rules broken, and the structure matches.' + ); } main(); diff --git a/.agents/skills/translate/pages.md b/.agents/skills/translate/pages.md index 23f3cbb057f..2be0c490931 100644 --- a/.agents/skills/translate/pages.md +++ b/.agents/skills/translate/pages.md @@ -1,7 +1,7 @@ # Translate pages Read `SKILL.md` first. It has the checks to run before you start, the -translation rules, and how to build and open the PR. +translation rules, and how to review, build, report, and open the PR. Save each translation at the same path as the English page, under `i18n//docusaurus-plugin-content-docs/current/`. For example, @@ -10,9 +10,8 @@ Save each translation at the same path as the English page, under silently ignores a file anywhere else. Translate the English page exactly as it is on disk, so the hash you record -matches what you translated. Do not reformat it; English changes belong in their -own PR. After writing the translation, run `yarn prettier --write ` on -only the files you changed under `i18n/`. +matches what you translated. Do not reformat it; English fixes are made and +committed before you translate (see "Fix the English first" in `SKILL.md`). ## Front matter @@ -20,7 +19,7 @@ Copy the English page's front matter. Translate only `title` and `sidebar_label`. Then add: ```yaml -translation_source_hash: .md> +translation_source_hash: translation_review_status: machine ``` @@ -33,40 +32,32 @@ with it. ## Decide what to do with each page -- **No translation yet.** Translate the whole page. To retranslate a page from - scratch on purpose, delete it first. -- **The hash matches the current English.** Skip it, unless the page is - `machine` and this lists any commits: - - ```bash - git log --oneline $(git log -1 --format=%H -- )..HEAD -- glossary.yml translation-rules.yml .agents/skills/translate/.md - ``` - - Then see "When the rules change". - -- **The hash does not match, and the status is `machine` or missing.** See - "Updating a page". -- **The hash does not match, and the status is `human-reviewed`.** Leave it out - of the translation PR. Open a separate PR for the named reviewer: diff the - English they saw against the current English as in "Updating a page", and - translate only what changed. Set the new hash and leave the status as - `human-reviewed`; the reviewer merging it approves it. If the old English is - no longer in the repo, say so and offer a full retranslation. -- **The English page has moved or been deleted.** Move the translation to match - with `git mv`, or delete it. Docusaurus silently ignores a translation with no - English page. +`status.js` sorts the pages into these groups. + +- **Missing.** Translate the whole page. To retranslate a page from scratch on + purpose, delete it first. +- **Up to date.** Skip it. +- **Rules changed.** A `machine` page whose rules have changed since it was + committed. See "When the rules change". +- **Stale.** The English has changed and the page is `machine`. See "Updating a + page". +- **Stale and human-reviewed.** Leave it out of the translation PR. Open a + separate PR for the named reviewer: diff the English they saw against the + current English as in "Updating a page", and translate only what changed. Set + the new hash and leave the status as `human-reviewed`; the reviewer merging it + approves it. If the old English is no longer in the repo, say so and offer a + full retranslation. +- **Orphaned.** The English page has moved or been deleted. Move the translation + to match with `git mv`, or delete it. Docusaurus silently ignores a + translation with no English page. ## When the rules change Do not retranslate the page; a reviewer could no longer see what the rules changed. -1. See what changed in the rules since the page was last committed: - - ```bash - git diff $(git log -1 --format=%H -- ) -- glossary.yml translation-rules.yml .agents/skills/translate/.md - ``` - +1. See what changed in the rules since the page was last committed, with the + `git diff` command `status.js` printed for it. 2. Fix only the text that breaks a new or changed rule. Leave everything else, even wording you would now write differently. @@ -93,9 +84,8 @@ Edit the existing translation in place, block by block: If the recorded English is not in the repo (`git diff` fails), translate the whole page again, keeping fenced blocks, and say so in the PR. -Update `translation_source_hash` even if no translated text changed. In the PR -description, list for each page how many blocks changed and how many needed new -text. +Update `translation_source_hash` even if no translated text changed. In the +report, list for each page how many blocks changed and how many needed new text. ## Fenced blocks @@ -122,36 +112,28 @@ These are on top of the translation rules in `SKILL.md`. adds it. A relative link like `../deploy/portability.md` breaks the translated build, so fix it in the English first (see `STYLE.md`). - Give each translated heading its English anchor, as `{#anchor}` after the - heading, so existing links still work. To get the anchors, let Docusaurus - write them into a copy of the English page: - - ```bash - cp docs/.md /page.md - yarn docusaurus write-heading-ids . /page.md - ``` - - It keeps the underscores from `_italic_` text in the anchor, which the site - does not: `See _Now_` gives `see-_now_`, but the site uses `see-now`. Drop - them. + heading, so existing links still work. A heading left in English needs none. + The check lists the anchors the headings should have when they don't match. ## Check each page +After writing a page, run `yarn prettier --write` on it, then: + ```bash -node .agents/skills/translate/check-translation.js docs/.md... +node .agents/skills/translate/check-translation.js docs/... ``` -This lists blocks where a fixed glossary term appears fewer times in the -translation than in the English, or that may break a rule in -`translation-rules.yml` or `.md`. Each is a candidate. If a term was -translated, put the English back. If Spanish just uses it fewer times, for -example by dropping a repeated subject, leave it. - -Then check by hand that: +It takes pages or folders, and lists: -- code blocks are identical to the English -- the counts of headings, code blocks, callouts, images, and tables match -- the front matter is complete -- every fenced block survived +- Blocks where a fixed glossary term appears fewer times in the translation than + in the English. If a term was translated, put the English back. If the + translation just uses it fewer times, for example by dropping a repeated + subject, leave it. +- Blocks that may break a rule in `translation-rules.yml` or `.md`. +- Code blocks, inline code, link targets, image files, headings, anchors, + callouts, or table rows that differ from the English. +- Front matter that is incomplete, a hash that is not the current English, or a + fenced block removed since the last commit. -Then build as `SKILL.md` describes, commit, and run `/review-translation` in a -fresh session. +Each is a candidate, not a verdict. Fix the real ones and run it again. Then go +back to "Review" in `SKILL.md`. diff --git a/.agents/skills/translate/status.js b/.agents/skills/translate/status.js new file mode 100644 index 00000000000..c5bffc26f61 --- /dev/null +++ b/.agents/skills/translate/status.js @@ -0,0 +1,203 @@ +#!/usr/bin/env node +// Runs the checks before translating, then says what each page needs. +// +// node .agents/skills/translate/status.js es # every page +// node .agents/skills/translate/status.js es docs/build # a folder +// node .agents/skills/translate/status.js es docs/build/triggers.md +// +// Each page is one of: missing, up to date, rules changed (a `machine` page +// whose rules have changed since it was committed), stale (the English has +// changed), or orphaned (a translation with no English page). It prints the +// English hash to record and the page's local URL. +const fs = require('fs'); +const path = require('path'); +const { execFileSync } = require('child_process'); +const yaml = require('js-yaml'); // installed with Docusaurus + +const git = (...args) => { + try { + return execFileSync('git', args, { + encoding: 'utf8', + stdio: ['ignore', 'pipe', 'ignore'], + }).trim(); + } catch { + return null; + } +}; + +const frontMatter = text => + yaml.load((text.match(/^---\n([\s\S]*?)\n---\n/) || [])[1] || '') || {}; + +// The URL Docusaurus gives a docs page: an absolute slug wins, then a +// relative slug or id in place of the file name. A file named index, README, +// or after its folder is the folder's page. +const url = (locale, rel, fm) => { + const dir = path.posix.dirname(rel); + const file = path.posix.basename(rel).replace(/\.mdx?$/, ''); + const name = ['index', 'README', path.posix.basename(dir)].includes(file) + ? '' + : fm.id || file; + const route = fm.slug?.startsWith('/') + ? fm.slug + : `/${dir === '.' ? '' : dir + '/'}${fm.slug || name}`.replace(/\/$/, ''); + return `http://localhost:3000${locale === 'en' ? '' : '/' + locale}/documentation${route}`; +}; + +function preChecks(locale) { + const config = fs.readFileSync('docusaurus.config.js', 'utf8'); + const locales = (config.match(/locales:\s*\[([^\]]*)\]/) || [])[1] || ''; + let yamlOk = true; + for (const f of ['glossary.yml', 'translation-rules.yml']) { + try { + yaml.load(fs.readFileSync(f, 'utf8')); + } catch (e) { + yamlOk = `${f}: ${e.message.split('\n')[0]}`; + } + } + git('fetch', '-q', 'origin', 'main'); + return [ + [ + `"${locale}" is enabled in docusaurus.config.js`, + locales.includes(`'${locale}'`), + ], + [ + 'i18n/ is not ignored by git', + git('check-ignore', '-q', `i18n/${locale}/x.md`) === null, + ], + ['glossary.yml and translation-rules.yml are valid YAML', yamlOk], + [ + `.agents/skills/translate/${locale}.md exists`, + fs.existsSync(`.agents/skills/translate/${locale}.md`), + ], + [ + 'the branch has everything on origin/main', + git('merge-base', '--is-ancestor', 'origin/main', 'HEAD') !== null, + ], + ]; +} + +const walk = p => + fs.statSync(p).isDirectory() + ? fs.readdirSync(p).flatMap(f => walk(path.join(p, f))) + : /\.mdx?$/.test(p) + ? [p] + : []; + +function main() { + const [locale, ...scope] = process.argv.slice(2); + if (!locale) { + console.error('Usage: status.js [docs/page.md | docs/folder]...'); + process.exit(1); + } + const root = `i18n/${locale}/docusaurus-plugin-content-docs/current`; + const rules = [ + 'glossary.yml', + 'translation-rules.yml', + `.agents/skills/translate/${locale}.md`, + ]; + + let failed = false; + for (const [name, ok] of preChecks(locale)) { + console.log( + `${ok === true ? 'ok ' : 'FAIL'} ${name}${typeof ok === 'string' ? ` (${ok})` : ''}` + ); + if (ok !== true) failed = true; + } + console.log(); + + const scopes = scope.length ? scope : ['docs']; + const bad = scopes.filter(s => !s.startsWith('docs') || !fs.existsSync(s)); + if (bad.length) { + console.error(`Not found: ${bad.join(', ')}. Pass a path under docs/.`); + process.exit(1); + } + const english = scopes.flatMap(walk).map(f => path.relative('docs', f)); + const translated = scopes + .map(s => path.join(root, path.relative('docs', s))) + .filter(fs.existsSync) + .flatMap(walk) + .map(f => path.relative(root, f)); + + const groups = {}; + const add = (group, line) => (groups[group] ||= []).push(line); + for (const rel of english) { + const en = `docs/${rel}`; + const tr = `${root}/${rel}`; + const hash = git('hash-object', en); + const fm = frontMatter(fs.readFileSync(en, 'utf8')); + const where = `${en} ${url(locale, rel, fm)}`; + if (!fs.existsSync(tr)) { + add('Missing: translate the whole page', `${where}\n hash ${hash}`); + continue; + } + const t = frontMatter(fs.readFileSync(tr, 'utf8')); + const status = t.translation_review_status || 'machine'; + if (t.translation_source_hash === hash) { + const last = git('log', '-1', '--format=%H', '--', tr); + const changed = + status === 'machine' && + last && + git('log', '--oneline', `${last}..HEAD`, '--', ...rules); + if (changed) + add( + 'Rules changed since the page was committed: fix only what breaks them', + `${where}\n git diff ${last} -- ${rules.join(' ')}` + ); + else add('Up to date', en); + } else if (status === 'human-reviewed') { + add( + `Stale and human-reviewed: separate PR for the reviewer`, + `${where} (${t.translation_reviewer || 'no reviewer named'})\n git diff --word-diff ${t.translation_source_hash} HEAD:${en}\n new hash ${hash}` + ); + } else { + const known = + git('cat-file', '-e', t.translation_source_hash || 'x') !== null; + add( + 'Stale: update the changed blocks', + `${where}\n ${known ? `git diff --word-diff ${t.translation_source_hash} HEAD:${en}` : 'recorded English is not in the repo: retranslate the whole page'}\n new hash ${hash}` + ); + } + } + for (const rel of translated) + if (!fs.existsSync(`docs/${rel}`)) + add('Orphaned: the English page moved or was deleted', `${root}/${rel}`); + + for (const [group, lines] of Object.entries(groups)) { + console.log(`${group} (${lines.length})`); + // With no scope, every page is in scope; listing the up-to-date ones is noise. + if (group === 'Up to date' && !scope.length) continue; + for (const l of lines) console.log(` ${l}`); + console.log(); + } + + // Interface text: has anything it comes from changed since it was last + // committed? The config check is loose; most config changes are not text. + if (!scope.length) { + const json = [ + `i18n/${locale}/code.json`, + `i18n/${locale}/docusaurus-theme-classic`, + `i18n/${locale}/docusaurus-plugin-content-docs/current.json`, + ]; + const last = git('log', '-1', '--format=%H', '--', ...json); + const changed = + last && + git( + 'diff', + '--stat', + `${last}..HEAD`, + '--', + 'sidebars-main.js', + 'docusaurus.config.js', + 'src' + ); + console.log( + changed + ? `\nInterface sources changed since the interface was last translated:\n${changed}\nCheck whether /translate interface is needed.` + : '\nInterface sources unchanged.' + ); + } + process.exit(failed ? 1 : 0); +} + +module.exports = { url, frontMatter, walk }; +if (require.main === module) main(); diff --git a/.agents/skills/update-content/SKILL.md b/.agents/skills/update-content/SKILL.md index 49e439c9252..3b59ced9efd 100644 --- a/.agents/skills/update-content/SKILL.md +++ b/.agents/skills/update-content/SKILL.md @@ -32,7 +32,8 @@ Change docs pages and open a PR. ## Finish Stop when you are done or reach 20 changed files. If work is left, list it in -the PR for the next run. Format the files you changed with +the PR for the next run. Proofread the pages you changed, following +`proofread/SKILL.md`. Format the files you changed with `yarn prettier --write `, then build the site as CI does, which fails on broken links: diff --git a/AGENTS.md b/AGENTS.md index f8a2dcc36f2..459dd794925 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -45,13 +45,17 @@ Never change them. anything. - **`update-content`** makes changes and opens a PR. It works from instructions, or from an identify-gaps report. +- **`proofread`** fixes basic typos in English pages without asking and reports + anything else that looks wrong. `translate` runs it first; run it on its own + to sweep a section. - **`release-review`** works out what the product shipped recently and passes that to identify-gaps. Suited to a monthly schedule. - **`translate`** translates English pages (`/translate pages`) or the interface - text (`/translate interface`), in its own PR per locale. + text (`/translate interface`), in its own PR per locale. It runs + `review-translation` itself when it finishes. - **`review-translation`** checks translated pages against the English, fixes - clear problems, and reports the rest. Commit the translation, then run it in a - fresh session. + clear problems, and reports the rest. It runs in a fresh session or subagent, + never the one that translated. `update-content` and `translate` open PRs, so they only run when someone asks for them by name (`/update-content`, `/translate`). When another skill hands off @@ -81,7 +85,9 @@ category from the sidebar, one folder under `docs/`, or one page. ## When to stop Stop at 20 changed files and open a PR (see `update-content/SKILL.md`). -Translations go in their own PR per locale and do not count toward the 20. +Translations go in their own PR per locale and do not count toward the 20. A +locale PR can also hold the proofreading fixes to the English pages it +translates, and no other English changes (see `translate/SKILL.md`). Mechanical changes that the build or a script checks, such as rewriting links or running Prettier, can go in one PR of any size. Keep that PR to the mechanical From 3cea010c42c6038c21a5e405d02cf8ae3b1725a9 Mon Sep 17 00:00:00 2001 From: Lucy Macartney Date: Mon, 19 Oct 2026 20:10:28 +0100 Subject: [PATCH 2/2] Tighten translate flow after review Keep only typo fixes to the English in a locale PR, translate only English on main, ask which branch to branch from, review only the pages written, settle new terms before splitting pages, check do-not-retranslate text against main, fail status.js when the fetch fails, and say when the interface has never been translated. --- .agents/skills/translate/SKILL.md | 41 ++++++++++++------- .agents/skills/translate/check-translation.js | 20 +++++++-- .agents/skills/translate/status.js | 11 +++-- 3 files changed, 50 insertions(+), 22 deletions(-) diff --git a/.agents/skills/translate/SKILL.md b/.agents/skills/translate/SKILL.md index 03438bbb540..7bb5809b2ba 100644 --- a/.agents/skills/translate/SKILL.md +++ b/.agents/skills/translate/SKILL.md @@ -44,14 +44,18 @@ It runs the checks before translating, then lists each page in scope as missing, stale, up to date, or orphaned, with the diff to read, the hash to record, and the page's local URL. For a sidebar category, pass its files or folder. If a check fails, stop and ask. Do not enable a locale yourself; that changes what -gets deployed. If the branch is behind `main`, ask to merge `main` in first. If -your branch comes off another branch, merge `main` into that one, then that one -into yours. +gets deployed. + +Translate only English that is on `main`. If the English you are asked to +translate is on a branch that has not merged, stop and say so: its PR merges +first. ## Make the locale branch -Make a branch from the current one, named after it plus the locale, such as -`fix-alerts-es`. Everything below is committed on it, and becomes one PR. +If you are on `main`, make a branch from it. If you are on another branch, such +as a translation branch like `i18n-es`, ask whether to branch from `main` or +from that branch. Name the branch after the scope plus the locale, such as +`notifications-es`. Everything below is committed on it, and becomes one PR. ## Fix the English first @@ -59,8 +63,13 @@ Settle the English before you translate it. A fix after translating makes the translation stale. Follow `.agents/skills/proofread/SKILL.md` on the pages `status.js` lists as missing or stale. Leave up-to-date pages alone; a fix there would make their translation stale. It fixes basic typos without asking and -reports the rest. If it reports anything, stop and ask the user now, and apply -the fixes they agree to. +reports the rest. + +Only those typo fixes go in the locale PR. Anything else it reports, such as a +wrong fact or an unclear sentence, needs its own English PR: list it under +"Problems in the English" and translate the English as it stands. If a problem +is bad enough that the page should not be translated until it is fixed, stop and +ask. If the English changed, run `yarn prettier --write` on those pages and commit them on their own, before you translate. Do not push. The fixes change the @@ -94,17 +103,21 @@ These apply to both tasks. the same English gets the same translation on every page. For more than three pages, split them across subagents, two or three pages each, -grouped by folder. Give each its pages, the paths to this file and `pages.md`, -and the status line for each page. Each writes its pages and runs the check. +grouped by folder. First read the pages for terms that have no translation yet, +such as a new feature name, and choose a word for each, so every subagent uses +the same one. Give each subagent its pages, those words, the paths to this file +and `pages.md`, and the status line for each page. Each writes its pages and +runs the check. ## Review When the pages are written and the check is clean, commit them on the locale -branch. Do not push. Then run `/review-translation` on the same scope in a new -subagent: read `.agents/skills/review-translation/SKILL.md` and pass it to a -fresh agent, not a fork. A translator is poor at spotting its own mistakes, so -the reviewer must not share your reasoning. It commits its fixes on their own, -so the translation and the review show as separate diffs. +branch. Do not push. Then run `/review-translation` in a new subagent, with the +English paths of the pages you just wrote as its scope, not the scope you were +given: read `.agents/skills/review-translation/SKILL.md` and pass it to a fresh +agent, not a fork. A translator is poor at spotting its own mistakes, so the +reviewer must not share your reasoning. It commits its fixes on their own, so +the translation and the review show as separate diffs. ## Build diff --git a/.agents/skills/translate/check-translation.js b/.agents/skills/translate/check-translation.js index 3bf2ed94b79..8f5785148ef 100644 --- a/.agents/skills/translate/check-translation.js +++ b/.agents/skills/translate/check-translation.js @@ -184,9 +184,21 @@ function structure(english, translation, current, previous) { found.push( `translation_source_hash is not the current English (${current}): update the page, or the hash if the translation already matches` ); - const fences = s => (s.match(//g) || []).length; - if (previous !== null && fences(translation) < fences(previous)) - found.push('a do-not-retranslate fence was removed since the last commit'); + // Every fenced block on main must still be here, word for word. + const fenced = s => + [ + ...s.matchAll( + /([\s\S]*?)/g + ), + ].map(m => m[1].trim()); + if (previous !== null) { + const now = fenced(translation); + const lost = fenced(previous).filter(f => !now.includes(f)); + if (lost.length) + found.push( + `do-not-retranslate text was removed or changed since main: ${lost.map(f => f.slice(0, 60)).join(' | ')}` + ); + } return found; } @@ -291,7 +303,7 @@ async function page(locale, enPath) { english, esText, git('hash-object', enPath), - git('show', `HEAD:${esPath}`) + git('show', `origin/main:${esPath}`) ), problems: pairs .map(r => ({ diff --git a/.agents/skills/translate/status.js b/.agents/skills/translate/status.js index c5bffc26f61..83f3dca7f5e 100644 --- a/.agents/skills/translate/status.js +++ b/.agents/skills/translate/status.js @@ -54,8 +54,9 @@ function preChecks(locale) { yamlOk = `${f}: ${e.message.split('\n')[0]}`; } } - git('fetch', '-q', 'origin', 'main'); + const fetched = git('fetch', '-q', 'origin', 'main') !== null; return [ + ['fetched origin/main', fetched], [ `"${locale}" is enabled in docusaurus.config.js`, locales.includes(`'${locale}'`), @@ -191,9 +192,11 @@ function main() { 'src' ); console.log( - changed - ? `\nInterface sources changed since the interface was last translated:\n${changed}\nCheck whether /translate interface is needed.` - : '\nInterface sources unchanged.' + !last + ? '\nThe interface has never been translated for this locale. Run /translate interface.' + : changed + ? `\nInterface sources changed since the interface was last translated:\n${changed}\nCheck whether /translate interface is needed.` + : '\nInterface sources unchanged.' ); } process.exit(failed ? 1 : 0);