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. ar has direction: 'rtl'.
  • docusaurus-site/i18n/live-locales.json is [] at the end of this plan. The live site help.staging.vianova.dev shows 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, slug and id values, admonition keywords.
  • Translate: frontmatter title, sidebar_label, description, body text, image alt text.
  • 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 into v1.0. Conventional commits (feat:, ci:, docs:), each ending with Co-Authored-By: Claude Opus 5.5 <[email protected]>.
  • Never search the whole disk; search only the repo.

Review Focus

  1. 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: compareFiles accepts a heading with {#top-bar} (Task 1).
  2. The live-locale build fails while a language is live: deploy-staging runs s3 sync --delete, which would delete the live /<locale>/ folder. Expected: the old folder stays. Test: deployExcludes returns --exclude "ar/*" when ar is live and build/ar is missing (Task 4).
  3. An English page links to /api-reference/...: under /de/ this link goes to a page that does not exist. Expected: the translated page uses pathname:///api-reference/..., and the check accepts this one rewrite only. Test in Task 1.
  4. 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 the ar build in Task 2.
  5. 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

FileResponsibility
docusaurus-site/scripts/i18n/check-translations.mjsCheck script: structure of one file, comparison, CLI over all locales
docusaurus-site/scripts/i18n/check-translations.test.mjsTests of the check script (synthetic fixtures inline)
docusaurus-site/scripts/i18n/deploy-excludes.mjsPrints extra --exclude flags for live locales that have no build output
docusaurus-site/scripts/i18n/deploy-excludes.test.mjsTests of the above
docusaurus-site/scripts/i18n/glossary.jsonDo-not-translate terms
docusaurus-site/scripts/i18n/TRANSLATING.mdRules for the agent that translates
docusaurus-site/i18n/live-locales.jsonLive languages, []
docusaurus-site/locales.tsLocale list, labels, live set selection
docusaurus-site/docusaurus.config.tsUses locales.ts; API reference only for en; search languages
docusaurus-site/src/pages/index.tsxHomepage text in <Translate>
docusaurus-site/i18n/<locale>/**Translated JSON and the 5 pilot pages
.gitlab-ci.ymlJobs 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> where Structure = {headings: number, headingIds: string[], codeBlocks: string[], links: string[], images: string[], slug?: string, id?: string, text: string, mdxError?: string}. headingIds holds explicit {#x} ids and the github-slugger slug of each heading text. text is the body without code blocks (for the glossary check).
    • compareFiles(en: string, tr: string, glossary: string[]): Promise<Problem[]> where Problem = {rule: 'mdx'|'headings'|'code'|'images'|'links'|'anchor'|'frontmatter'|'glossary'|'orphan', detail: string}.
    • checkSite(siteDir: string, locales: string[]): Promise<Array<Problem & {file: string, locale: string}>>: compares docs/**/*.{md,mdx} with i18n/<locale>/docusaurus-plugin-content-docs/current/**. A missing translated file is not a problem. A translated file with no English file gives orphan.
    • 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 prints OK <n> files checked and exit 0.
  • glossary.json: {"doNotTranslate": [<the terms of Global Constraints that are words>]}.

  • Step 1: Write the failing tests in check-translations.test.mjs with node:test and node:assert/strict. Synthetic English fixture: frontmatter title: Test page and slug: /test, headings ## Top bar {#top-bar} and ## Maps, a link [see](#top-bar), a link [api](/api-reference/zones), an image ![Map](/img/docs/map.png), a fenced block ___CODE_BLOCK_0___, and the word Cityscope. Tests:

    • extractStructure on the fixture → headings === 2, headingIds includes top-bar and maps, codeBlocks deep-equals ['curl https://example.invalid'], images deep-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 with rule === 'links'.
    • changed code fails → rule code. missing image fails → rule images. extra heading fails → rule headings. changed slug fails → rule frontmatter.
    • broken anchor fails: German heading ## Obere Leiste without {#top-bar} and the link #top-bar → rule anchor.
    • translated product name fails: Cityscope replaced by Stadtblick → rule glossary.
    • invalid MDX fails: a translated body with <div> not closed → rule mdx.
    • checkSite orphan and missing: temp dir (fs.mkdtemp) with docs/a.md, docs/b.md, i18n/de/docusaurus-plugin-content-docs/current/a.md (good) and .../c.md → exactly one problem, rule orphan, file c.md.
  • Step 2: Run the tests and see them fail
    Run: cd docusaurus-site && npm install && npm run i18n:test
    Expected: FAIL, module check-translations.mjs not found.

  • Step 3: Implement the module. Frontmatter: split the --- block and parse it with js-yaml. Before the compile, remove {#id} at the end of heading lines and record the id. Compile with compile() of @mdx-js/mdx and a remark plugin that walks the tree and collects heading, code, link, image nodes (a small recursive walk; no extra dependency). A compile error sets mdxError. 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 #x link resolves to a headingIds entry of the translated file. Glossary rule: each term found in the English text is found in the translated text.

  • Step 4: Run the tests and see them pass
    Run: npm run i18n:test → all pass. Then npm 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, ar with direction: 'rtl', htmlLang nb for no); selectLocales(env: string | undefined, live: string[]): string[] → ALL_LOCALES when env !== 'live', else ['en', ...live] in ALL_LOCALES order, unknown codes dropped.

  • Config: const locale = process.env.DOCUSAURUS_CURRENT_LOCALE ?? 'en'. The api-reference docs plugin and docusaurus-plugin-openapi-docs are in plugins only when locale === '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'}. Search language: ['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 en finishes, and build/index.html contains no link that starts with /de/. Check B: npm run build -- --locale ar finishes, the output is in build/ar/, and build/ar/index.html contains dir="rtl".

  • Step 2: Run both builds and see them fail (ar is not a locale yet).

  • Step 3: Implement locales.ts and the config changes above.

  • Step 4: Run both builds and see them pass. Also run npm run typecheck, and npm run build -- --locale en with 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, h2 titles, contact line; Layout title and description). The API reference card link becomes pathname:///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 (sections start, topics, resources), homepage.title, homepage.intro, homepage.heading.getStarted|browse|contact|resources, homepage.contact. Use translate({id, message}) for the card arrays and <Translate id=…> in JSX. Keep [email protected] as a values placeholder.

  • Step 1: Change index.tsx. Run npm run write-translations -- --locale en, then for each of de ar no fr it: npm run write-translations -- --locale <l> --override.

  • Step 2: Verify: grep -c homepage. i18n/en/code.json gives at least 30. npm run typecheck passes. The English homepage on npm start looks 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 no buildDir/<locale>/index.html, the two items --exclude, <locale>/*. CLI prints them space-separated, quoted.

  • Jobs:

    • check-translations (stage lint, merge requests and v1.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 (stage build, v1.0 and merge requests, allow_failure: true): reads i18n/live-locales.json with node -p, runs KB_LOCALE_SET=live npm run build -- --locale <l> --out-dir build for each, and exits 0 when the list is empty. Artifacts docusaurus-site/build/.
    • build-all-locales (merge requests only, not needed by any job): time npm run build. Prints the time.
    • deploy-staging: needs build and build-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'] with build/ar/index.html present → []; live ['ar','de'] with only build/de/index.html → ['--exclude','ar/*'].

  • Step 2: npm run i18n:test → the 3 new tests fail.

  • Step 3: Implement deploy-excludes.mjs and the CI jobs.

  • Step 4: npm run i18n:test passes. A CI lint passes: glab ci lint .gitlab-ci.yml (through the Keychain glab wrapper). Locally, KB_LOCALE_SET=live npm run build -- --locale de --out-dir build writes to build/de/ and does not remove build/index.html of an earlier English build. If it removes it, build each locale into build-<l> and copy it to build/<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-reference links become pathname:///api-reference; tone (formal "Sie" in German, "vous" in French, "Lei" in Italian, Modern Standard Arabic, Bokmål for Norwegian); run npm 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, in docusaurus-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 every message).

  • Step 1: Translate the 5 pages and the JSON for one locale, following TRANSLATING.md. One subagent per locale is allowed; each runs npm 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 serve after the full build, for de and ar (and one page of no, 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.css under [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.0 with /mr-describe. Put in it: the English build time before and after, the build-all-locales time, the Chrome screenshots.
  • Step 2: Wait on CI with gl-wait: check-translations, build, build-locales, build-all-locales are green.
  • Step 3: Run /review-board. After it passes, use the normal merge path. After the merge, check help.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.yml change and the one-writer check (step 3), native review (step 4), go-live (step 5), update flow and runbook (step 6).
  • The v1.0 rule "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.

Did this page help you?