2026-11-04-crowdin-translation-pilot
Crowdin translation, step 1 (pilot): implementation plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Make the Help Center build in 6 languages with a translation check in CI, and translate 5 pilot pages into 5 languages, with no change to the live site.
Architecture: All 6 locales are in the Docusaurus config. An environment variable KB_LOCALE_SET=live limits the deployed build to English plus the languages in i18n/live-locales.json (empty in this step). A Node check script compares each translated page with its English page. CI gets 3 build jobs: English (deployed), live locales (deployed, can fail without blocking English), all locales (merge requests only, not deployed).
Tech Stack: Docusaurus 3.9.2, Node 20 (CI) with node:test, @mdx-js/mdx 3, github-slugger, js-yaml, GitLab CI, @easyops-cn/docusaurus-search-local 0.55.
Spec: docs/superpowers/specs/2026-11-04-crowdin-translation-design.md. This plan covers section 8, step 1 only. Steps 2 to 6 get their own plans.
Global Constraints
- Locales:
en(default),de,ar,no,fr,it. Labels: English, Deutsch, العربية, Norsk, Français, Italiano.arhasdirection: 'rtl'. docusaurus-site/i18n/live-locales.jsonis[]at the end of this plan. The live sitehelp.staging.vianova.devshows no change.- Do not translate: Vianova, Cityscope, CityScope, VIP, Streams, Hawkeye, Intelligence Platform, MDS, GBFS, API field names, code, URLs, file paths, image paths, frontmatter keys,
slugandidvalues, admonition keywords. - Translate: frontmatter
title,sidebar_label,description, body text, imagealttext. - Translated locales do not build the API reference. All API links go to
/api-reference/hawkeye; translated locales get a redirect page at that path (ruling in Task 6:pathname://gets the locale prefix). - The English deploy is at most 2 minutes slower than before this change.
- No Crowdin call in this step. Do not change
docusaurus-site/crowdin.yml(step 3 does it). - Branch:
implement-crowdin-translation-91b92213(knowledge-base), MR intov1.0. Conventional commits (feat:,ci:,docs:), each ending withCo-Authored-By: Claude Opus 5.5 <[email protected]>. - Never search the whole disk; search only the repo.
Review Focus
- A translated page that compiles in Docusaurus but has an explicit heading id
{#top-bar}: the check script must strip heading ids before the MDX compile, as Docusaurus does, or every page with an anchor fails. Test:compareFilesaccepts a heading with{#top-bar}(Task 1). - The live-locale build fails while a language is live:
deploy-stagingrunss3 sync --delete, which would delete the live/<locale>/folder. Expected: the old folder stays. Test:deployExcludesreturns--exclude "ar/*"whenaris live andbuild/aris missing (Task 4). - An English page links to
/api-reference/...: under/de/this link goes to a page that does not exist. Expected: the translated page usespathname:///api-reference/..., and the check accepts this one rewrite only. Test in Task 1. - A non-English build with the API reference nav item: the nav item points to a docs plugin that the locale does not load, and the build crashes. Expected: the nav item, the footer link and the homepage card use
pathname:///api-reference. Verified by thearbuild in Task 2. - An Arabic page with a code block or a table: code must stay left to right. Expected: code blocks render LTR in the RTL page. Checked in the Chrome list of Task 6.
File map
| File | Responsibility |
|---|---|
docusaurus-site/scripts/i18n/check-translations.mjs | Check script: structure of one file, comparison, CLI over all locales |
docusaurus-site/scripts/i18n/check-translations.test.mjs | Tests of the check script (synthetic fixtures inline) |
docusaurus-site/scripts/i18n/deploy-excludes.mjs | Prints extra --exclude flags for live locales that have no build output |
docusaurus-site/scripts/i18n/deploy-excludes.test.mjs | Tests of the above |
docusaurus-site/scripts/i18n/glossary.json | Do-not-translate terms |
docusaurus-site/scripts/i18n/TRANSLATING.md | Rules for the agent that translates |
docusaurus-site/i18n/live-locales.json | Live languages, [] |
docusaurus-site/locales.ts | Locale list, labels, live set selection |
docusaurus-site/docusaurus.config.ts | Uses locales.ts; API reference only for en; search languages |
docusaurus-site/src/pages/index.tsx | Homepage text in <Translate> |
docusaurus-site/i18n/<locale>/** | Translated JSON and the 5 pilot pages |
.gitlab-ci.yml | Jobs check-translations, build, build-locales, build-all-locales, deploy-staging |
Task 1: Translation check script
Files:
- Create:
docusaurus-site/scripts/i18n/check-translations.mjs,docusaurus-site/scripts/i18n/check-translations.test.mjs,docusaurus-site/scripts/i18n/glossary.json - Modify:
docusaurus-site/package.json(devDependencies@mdx-js/mdx@^3,github-slugger@^2,js-yaml@^4; scripts"i18n:check": "node scripts/i18n/check-translations.mjs","i18n:test": "node --test scripts/i18n/")
Interfaces:
-
Produces:
extractStructure(source: string): Promise<Structure>whereStructure = {headings: number, headingIds: string[], codeBlocks: string[], links: string[], images: string[], slug?: string, id?: string, text: string, mdxError?: string}.headingIdsholds explicit{#x}ids and the github-slugger slug of each heading text.textis the body without code blocks (for the glossary check).compareFiles(en: string, tr: string, glossary: string[]): Promise<Problem[]>whereProblem = {rule: 'mdx'|'headings'|'code'|'images'|'links'|'anchor'|'frontmatter'|'glossary'|'orphan', detail: string}.checkSite(siteDir: string, locales: string[]): Promise<Array<Problem & {file: string, locale: string}>>: comparesdocs/**/*.{md,mdx}withi18n/<locale>/docusaurus-plugin-content-docs/current/**. A missing translated file is not a problem. A translated file with no English file givesorphan.- CLI:
node scripts/i18n/check-translations.mjs [locale...](default:de ar no fr it). Prints one line per problem<locale> <file> <rule> <detail>, exit 1 if any problem, else printsOK <n> files checkedand exit 0.
-
glossary.json:{"doNotTranslate": [<the terms of Global Constraints that are words>]}. -
Step 1: Write the failing tests in
check-translations.test.mjswithnode:testandnode:assert/strict. Synthetic English fixture: frontmattertitle: Test pageandslug: /test, headings## Top bar {#top-bar}and## Maps, a link[see](#top-bar), a link[api](/api-reference/zones), an image, a fenced block___CODE_BLOCK_0___, and the wordCityscope. Tests:extractStructure on the fixture→headings === 2,headingIdsincludestop-barandmaps,codeBlocksdeep-equals['curl https://example.invalid'],imagesdeep-equals['/img/docs/map.png'],slug === '/test'.identical translation passes→compareFiles(en, en, ['Cityscope'])returns[].translated text with kept id passes: German version (## Obere Leiste {#top-bar},## Karten, same link, image, code,Cityscope,pathname:///api-reference/zones) →[].api link not rewritten fails: the German version with/api-reference/zones→ one problem withrule === 'links'.changed code fails→ rulecode.missing image fails→ ruleimages.extra heading fails→ ruleheadings.changed slug fails→ rulefrontmatter.broken anchor fails: German heading## Obere Leistewithout{#top-bar}and the link#top-bar→ ruleanchor.translated product name fails:Cityscopereplaced byStadtblick→ ruleglossary.invalid MDX fails: a translated body with<div>not closed → rulemdx.checkSite orphan and missing: temp dir (fs.mkdtemp) withdocs/a.md,docs/b.md,i18n/de/docusaurus-plugin-content-docs/current/a.md(good) and.../c.md→ exactly one problem, ruleorphan, filec.md.
-
Step 2: Run the tests and see them fail
Run:cd docusaurus-site && npm install && npm run i18n:test
Expected: FAIL, modulecheck-translations.mjsnot found. -
Step 3: Implement the module. Frontmatter: split the
---block and parse it withjs-yaml. Before the compile, remove{#id}at the end of heading lines and record the id. Compile withcompile()of@mdx-js/mdxand a remark plugin that walks the tree and collectsheading,code,link,imagenodes (a small recursive walk; no extra dependency). A compile error setsmdxError. Link rule: the multiset of translated links equals the English one after the map/api-reference…→pathname:///api-reference…on the English side. Anchor rule: each#xlink resolves to aheadingIdsentry of the translated file. Glossary rule: each term found in the Englishtextis found in the translatedtext. -
Step 4: Run the tests and see them pass
Run:npm run i18n:test→ all pass. Thennpm run i18n:check→OK 0 files checked. -
Step 5: Commit
feat: add a check script for translated pages
Task 2: Site config for 6 locales
Files:
- Create:
docusaurus-site/locales.ts,docusaurus-site/i18n/live-locales.json([]) - Modify:
docusaurus-site/docusaurus.config.ts:23-32(i18n),:80-106(API plugins),:109-120(search),:128-152(navbar and footer API links)
Interfaces:
-
Produces in
locales.ts:ALL_LOCALES = ['en','de','ar','no','fr','it'] as const;LOCALE_CONFIGS(labels,arwithdirection: 'rtl',htmlLangnbforno);selectLocales(env: string | undefined, live: string[]): string[]→ALL_LOCALESwhenenv !== 'live', else['en', ...live]inALL_LOCALESorder, unknown codes dropped. -
Config:
const locale = process.env.DOCUSAURUS_CURRENT_LOCALE ?? 'en'. Theapi-referencedocs plugin anddocusaurus-plugin-openapi-docsare inpluginsonly whenlocale === 'en'. For other locales, the navbar item "API Reference" is{href: 'pathname:///api-reference', label: 'API Reference', position: 'left'}, and the footer link is{label: 'API Reference', href: 'pathname:///api-reference'}. Searchlanguage: ['en','de','fr','it','no','ar']. -
Step 1: Define the build checks (the config is tested by builds, not by unit tests). Check A:
KB_LOCALE_SET=live npm run build -- --locale enfinishes, andbuild/index.htmlcontains no link that starts with/de/. Check B:npm run build -- --locale arfinishes, the output is inbuild/ar/, andbuild/ar/index.htmlcontainsdir="rtl". -
Step 2: Run both builds and see them fail (
aris not a locale yet). -
Step 3: Implement
locales.tsand the config changes above. -
Step 4: Run both builds and see them pass. Also run
npm run typecheck, andnpm run build -- --locale enwith no env, and record the English build time before and after this task in the MR (limit: +2 minutes). -
Step 5: Commit
feat: add de, ar, no, fr and it to the site config
Task 3: Homepage and interface strings
Files:
- Modify:
docusaurus-site/src/pages/index.tsx(all visible text: card titles and descriptions,h1, intro,h2titles, contact line;Layouttitle and description). The API reference card link becomespathname:///api-reference. - Modify:
docusaurus-site/i18n/en/**/*.json(made again),docusaurus-site/i18n/{de,ar,no,fr,it}/**/*.json(made again)
Interfaces:
-
Translation ids:
homepage.<section>.<key>.title|description(sectionsstart,topics,resources),homepage.title,homepage.intro,homepage.heading.getStarted|browse|contact|resources,homepage.contact. Usetranslate({id, message})for the card arrays and<Translate id=…>in JSX. Keep[email protected]as avaluesplaceholder. -
Step 1: Change
index.tsx. Runnpm run write-translations -- --locale en, then for each ofde ar no fr it:npm run write-translations -- --locale <l> --override. -
Step 2: Verify:
grep -c homepage. i18n/en/code.jsongives at least 30.npm run typecheckpasses. The English homepage onnpm startlooks the same as before (screenshot in the MR). -
Step 3: Commit
feat: make the homepage text translatable
Task 4: CI jobs
Files:
- Create:
docusaurus-site/scripts/i18n/deploy-excludes.mjs,docusaurus-site/scripts/i18n/deploy-excludes.test.mjs - Modify:
.gitlab-ci.yml
Interfaces:
-
deployExcludes(live: string[], buildDir: string): string[]→ for each live locale with nobuildDir/<locale>/index.html, the two items--exclude,<locale>/*. CLI prints them space-separated, quoted. -
Jobs:
check-translations(stagelint, merge requests andv1.0):npm ci,npm run i18n:test,npm run i18n:check.build(unchanged rules):KB_LOCALE_SET=live npm run build -- --locale en.build-locales(stagebuild,v1.0and merge requests,allow_failure: true): readsi18n/live-locales.jsonwithnode -p, runsKB_LOCALE_SET=live npm run build -- --locale <l> --out-dir buildfor each, and exits 0 when the list is empty. Artifactsdocusaurus-site/build/.build-all-locales(merge requests only, not needed by any job):time npm run build. Prints the time.deploy-staging:needsbuildandbuild-locales(artifacts: true,optional: true). Adds$(node docusaurus-site/scripts/i18n/deploy-excludes.mjs)to the sync command, after--exclude "v2/*".
-
Step 1: Write failing tests for
deployExcludes: live[]→[]; live['ar']withbuild/ar/index.htmlpresent →[]; live['ar','de']with onlybuild/de/index.html→['--exclude','ar/*']. -
Step 2:
npm run i18n:test→ the 3 new tests fail. -
Step 3: Implement
deploy-excludes.mjsand the CI jobs. -
Step 4:
npm run i18n:testpasses. A CI lint passes:glab ci lint .gitlab-ci.yml(through the Keychainglabwrapper). Locally,KB_LOCALE_SET=live npm run build -- --locale de --out-dir buildwrites tobuild/de/and does not removebuild/index.htmlof an earlier English build. If it removes it, build each locale intobuild-<l>and copy it tobuild/<l>/in the job script. -
Step 5: Commit
ci: build every locale on merge requests and deploy only live locales
Task 5: Translator rules
Files:
-
Create:
docusaurus-site/scripts/i18n/TRANSLATING.md -
Step 1: Write the rules in strict ASD-STE100: the Global Constraints lists; one target folder per locale (
i18n/<l>/docusaurus-plugin-content-docs/current/<same path>); keep heading ids with{#english-id}when a page links to the anchor;/api-referencelinks becomepathname:///api-reference; tone (formal "Sie" in German, "vous" in French, "Lei" in Italian, Modern Standard Arabic, Bokmål for Norwegian); runnpm run i18n:check <locale>after each page. -
Step 2: Commit
docs: add the rules for translated pages
Task 6: Pilot pages
Files:
-
Create, for each locale
de ar no fr it, indocusaurus-site/i18n/<l>/docusaurus-plugin-content-docs/current/:
get-started/what-is-vianova.md,get-started/onboarding-videos.mdx(JSX),get-started/tour-the-interface.md(anchor links),developers/api-getting-started.md(code and API links),maps/satellite-view.md(table) -
Modify: the JSON files of
i18n/<l>/made in Task 3 (translate everymessage). -
Step 1: Translate the 5 pages and the JSON for one locale, following
TRANSLATING.md. One subagent per locale is allowed; each runsnpm run i18n:check <l>. -
Step 2: Verify:
npm run i18n:check→OK 25 files checked.npm run build(all locales) passes. -
Step 3: Chrome check on
npm run serveafter the full build, fordeandar(and one page ofno,fr,it). Screenshot each item in the MR. Arabic list: top bar and language menu; left sidebar; right table of contents; the table page; the code page (code stays left to right); admonitions; images; the search box and its results; the homepage; a recipe page (English text in the RTL layout); the API reference link opens/api-reference. -
Step 4: Fix the problems found (CSS for RTL goes in
src/css/custom.cssunder[dir='rtl']). Run Step 2 again. -
Step 5: Commit one commit per locale:
feat: add the <language> pilot pages
Task 7: Merge request
- Step 1: Push the branch. Open a draft MR into
v1.0with/mr-describe. Put in it: the English build time before and after, thebuild-all-localestime, the Chrome screenshots. - Step 2: Wait on CI with
gl-wait:check-translations,build,build-locales,build-all-localesare green. - Step 3: Run
/review-board. After it passes, use the normal merge path. After the merge, checkhelp.staging.vianova.dev: no language menu entry other than English, and the English pages are unchanged.
Not in this plan (later plans)
- The full translation (spec step 2), Crowdin connection,
crowdin.ymlchange and the one-writer check (step 3), native review (step 4), go-live (step 5), update flow and runbook (step 6). - The
v1.0rule "pipelines must succeed" (spec 6.6.5). It changes a project setting, so it needs a gated step. It is necessary before the first Crowdin merge request, in step 3.
Updated about 23 hours ago
