2026-11-04-crowdin-translation-design

Crowdin translation of the Help Center: design

Date: 2026-11-04 · Status: approved by the user, after a builder check and an adversarial review

1. Why

Local client staff (cities and operators) read the Help Center. Many of them do not read English well. The clients that need translations now are in Germany, in the Nordic countries and in Arabic-speaking countries. Today the site is English only. French and Italian are in scope too (user's decision); the site config already declares them.

2. Goals

  1. The main documentation is available in 5 languages: German (de), Arabic (ar), Norwegian (no), French (fr), Italian (it).
  2. Arabic pages show a right-to-left layout that reads correctly.
  3. A reader selects the language in the language menu of the top bar. Each language has its own URL path, for example /de/docs/....
  4. The translations are stored in Crowdin, so that Vianova local teams can correct them in the Crowdin web page, without git.
  5. The cost stays as low as possible (user's decision).

3. Non-goals

  • The changelog (blog/), the recipes (recipes/), the custom pages and the API reference stay in English (user's decision: option "main documentation only").
  • Swedish, Danish and Finnish are out of scope. "Nordic" means Norwegian, the same as in the frontend.
  • No paid machine translation engine, no translation agency and no Crowdin AI credits (user's decision: "you translate, we add in Crowdin").
  • No change to how editors write: editors write English Markdown only.
  • The old root folder docs/ (ReadMe export copy) is not translated. The site builds from docusaurus-site/docs/.

4. Anti-goals (the work fails if one of these occurs)

  • A translated page has broken Markdown, broken links, missing images or changed code blocks.
  • Product names are translated (for example "Cityscope", "VIP", "Streams" become local words).
  • Crowdin overwrites the agent translations with empty or English text.
  • The English deploy becomes more than 2 minutes slower, or the English deploy fails because of a translation error.
  • An Arabic page shows a broken layout (mixed direction in the menus, tables or code blocks).

5. Facts about the current state

ItemState on 2026-11-04
Pages to translate109 files in docusaurus-site/docs/ (108 .md, 1 .mdx). 2 pages contain JSX. No page uses custom heading ids ({#id})
English wordsabout 49,000
Locales in docusaurus.config.tsen, fr, de, it (no ar, no no)
Translated pages0. i18n/fr/ holds only JSON files, and these are English copies with old link descriptions
docusaurus-site/crowdin.ymlExists. It sends docs, recipes, blog, pages and all JSON. It matches *.md only, not *.mdx
CI buildnpm run build -- --locale en in job build; job deploy-staging syncs to S3 with --delete --exclude "v2/*"
API referenceA third docs instance (api-reference), about 580 generated pages with the interactive @theme/ApiItem
Local search@easyops-cn/docusaurus-search-local with language: ['en']
Crowdin login for the agentNone in the Keychain
Crowdin at VianovaThe frontend repo vianova-frontend-v2 uses the Crowdin GitLab integration. Its locales include ar and no

6. How it works

6.1 First translation

  1. The agent (Claude Code) translates the 109 pages into the 5 languages. Translation is in Claude Code sessions, so it has no translation fee.
  2. The agent writes the files to docusaurus-site/i18n/<locale>/docusaurus-plugin-content-docs/current/<same path>.
  3. The agent translates every JSON file of i18n/<locale>/ (navbar, footer, code.json, sidebar category labels of docs, recipes, API reference and changelog). It first makes them again with npx docusaurus write-translations --locale <locale>, because the current fr files are English copies.
  4. The agent uses one glossary file for all languages (6.4).

6.2 Crowdin keeps the translations: Crowdin CLI in GitLab CI (user's decision)

The user chose the Crowdin CLI in CI over the Crowdin GitLab integration and over "translations only in Crowdin". This follows the Docusaurus Crowdin guide.

  1. A Vianova Crowdin admin creates one Crowdin project for the KB, with the target languages de, ar, no, fr, it. The admin creates a personal token restricted to that project, with the smallest scopes and an expiry date.
  2. The token and the project id go in GitLab CI variables (CROWDIN_PERSONAL_TOKEN, masked and protected; CROWDIN_PROJECT_ID). The agent gets a copy in the Keychain as crowdin-token, through kc-put, and runs the CLI from docusaurus-site/ with with-secret kc:crowdin-token CROWDIN_PERSONAL_TOKEN -- npx crowdin ....
  3. First import. The agent runs crowdin upload sources, then crowdin upload translations. The translations are imported as not approved, so that the native speaker approves them (6.7).
  4. Job crowdin-upload. It runs after each merge to v1.0 that changes a source file. It runs write-translations --locale en, then crowdin upload sources --delete-obsolete.
  5. Job crowdin-download. It runs once a week (scheduled pipeline) or by hand. It runs crowdin download and opens or updates one merge request "Crowdin translations" into v1.0, with a GitLab project access token.
  6. Export setting. Crowdin exports all translations, approved or not. "Export only approved" is off, because it replaces unapproved text with English. The go-live gate is live-locales.json (6.7).
  7. One writer. After the first import, translation files in i18n/<locale>/ (not en) change in git only through the crowdin-download merge request. A CI check fails any other merge request that changes these files. Local teams and the agent correct text in Crowdin only.
  8. crowdin.yml sends only /docs/**/*.md, /docs/**/*.mdx (file type mdx_v2, with a fixed parser version) and an explicit list of the JSON files of i18n/en/. It has no entries for recipes, blog or pages. This change merges before the first import. Before the first import, crowdin upload sources --dryrun proves that each output path is a folder that the build reads, including the Norwegian folder.

6.3 Updates after an English change

  1. An editor merges an English change. The job crowdin-upload sends it to Crowdin. Crowdin marks the changed sentences as not translated.
  2. The agent translates the untranslated sentences in a session: it downloads them with the CLI, translates them, and uploads them as not approved.
  3. The next crowdin-download merge request brings the text to the repo. Until a translation exists, the changed sentence shows English text in that language.
  4. A deleted or renamed English page: --delete-obsolete removes the old file in Crowdin, and the next download removes the translated file.
  5. A red crowdin-download merge request: the agent fixes the text in Crowdin and runs the job again.
  6. Before the Crowdin connection (steps 1 and 2 of section 8), the agent finds English changes with git diff since the commit of its translation, and updates the translated files in git.

6.4 Glossary and rules for the translator

  • Do not translate: product and brand names (Vianova, Cityscope, VIP, Streams, Hawkeye, and similar), API field names, code, URLs, file paths, image paths, frontmatter keys, slug and id values, admonition keywords (:::note, :::tip).
  • Translate: frontmatter title, sidebar_label, description, body text, image alt text.
  • Keep the same heading structure. A link to a heading anchor must resolve in the translated page: add an explicit heading id with the English anchor, or update the link.
  • Keep the glossary as a file in the repo, so that every session uses the same words. The check script reads it (6.6).

6.5 Site config

  1. All 6 locales stay in i18n.locales at all times: en, de, ar, no, fr, it. So every locale can be built and previewed on this machine and in CI.
  2. ar has direction: 'rtl'. Labels: English, Deutsch, العربية, Norsk, Français, Italiano.
  3. no is a placeholder code. The builder uses the code that both Docusaurus and Crowdin accept for the same folder (no or nb).
  4. The homepage: the agent puts the visible text of src/pages/index.tsx in <Translate> tags, then runs write-translations --locale en again, so that the text goes into code.json. This is the only code change outside the config and CI.
  5. The translated locales do not build the API reference again. Every API reference link (menu, footer, homepage) goes to /api-reference/hawkeye. In a translated locale, a small redirect page at /<locale>/api-reference/hawkeye sends the reader to the English page. (A pathname:// link does not work: Docusaurus adds the locale prefix to it.)
  6. The local search indexes each language. Each locale searches its own pages.

6.6 Build, deploy and checks

  1. Job build (English). It builds en only, as today. deploy-staging needs this job.
  2. Job build-locales. It builds the locales listed in docusaurus-site/i18n/live-locales.json. deploy-staging adds its output to the English build. If it fails, English still deploys, and the pipeline shows the failure.
  3. Job build-all-locales (merge requests only). It builds all 6 locales and is not deployed. It proves that every locale builds before go-live, and it reports the build time.
  4. Check script. It runs in CI on merge requests only, never on v1.0. It blocks a merge request that changes translated files or the check; on other merge requests it only warns, because an English edit can make a translation out of date. The English build does not wait for it. For each translated file, it checks against the English file: the file compiles as MDX; same number of headings, code blocks, links and images; same image paths and code text; same slug and id; every do-not-translate glossary term of the English file is also in the translated file; each link target exists. A missing translated file passes (English shows). A translated file with no English file fails.
  5. Branch rule. v1.0 requires a green pipeline before a merge, so a red Crowdin merge request cannot merge.
  6. onBrokenLinks stays warn. The deploy job keeps aws s3 sync --delete --exclude "v2/*". Each locale is a sub-folder (/de/, /ar/), so no new routing rule is necessary.

6.7 A native speaker reads each language before it goes live (user's decision)

  1. A language goes on the site only after a Vianova person who speaks it has read and approved the text. The user chose this over "live after the automatic checks".
  2. The person reads and approves the text in Crowdin, with the Crowdin proofread step.
  3. A language goes live only when the native speaker has approved 100% of its text in Crowdin.
  4. docusaurus-site/i18n/live-locales.json lists the live languages. The language menu shows only en and the live languages. A merge request that adds a language to this file is the go-live step.
  5. Before the go-live, the agent checks the language on a local build, in Chrome.
  6. Rollback. A merge request that removes the language from live-locales.json. The next deploy deletes /<locale>/ from S3, because the sync uses --delete. The user decides a rollback.
  7. The site in this repo is help.staging.vianova.dev. The CI has no other environment.

7. Cost

ItemCost
TranslationNo fee. Agent session time only: about 49,000 words × 5 languages
CrowdinHosted words: about 49,000 source words added to the Vianova account. The admin checks the plan limit before the first import
CI1 extra build job on each merge request (all locales), and 1 extra build job before the deploy (live locales)

8. Order of work and done conditions

StepWorkDone when
1. PilotSite config for the 6 locales, live-locales.json empty, the 3 build jobs, the check script, the glossary, the homepage <Translate> tags, and 5 pages in each language (one page with tables, images and code; the JSX page)Check script green. build-all-locales green, and its time is known. The English deploy is at most 2 minutes slower. Chrome check of the pilot pages on a local build, and for Arabic the list below. The live site shows no change
2. Full translationThe other pages, one merge request for each languageCheck script green on all 109 pages × 5 languages. The live site shows no change
3. Crowdin connectionThe admin creates the project and the token. The agent changes crowdin.yml, adds the jobs crowdin-upload and crowdin-download and the one-writer check, and runs the first importDry run paths are correct. An edit in Crowdin comes back in the crowdin-download merge request. No translated file goes back to English or becomes empty
4. Native reviewA native speaker proofreads each language in Crowdin100% of the language is approved in Crowdin, and the crowdin-download merge request with that text is merged
5. Go-liveOne merge request for each language adds it to live-locales.json. Before the first go-live: (a) measure the deploy time with the live languages, and if the English deploy is more than 2 minutes slower, deploy English first and the languages in a second job; (b) make a failed build-locales job on v1.0 visible (a notification), and do not show a language in the menu before its first build passedThe language shows in the menu of help.staging.vianova.dev, and the Chrome check passes on the live site
6. Update flowA runbook for editors and the agentOne English page changes, the flow runs, and the crowdin-download merge request has the new translation and a green check

Arabic Chrome check list: top bar and language menu, left sidebar, table of contents on the right, a page with a table, a page with a code block, admonitions, images with captions, the search box and its results, the homepage, a recipe page (English text in the Arabic layout), and the link to the English API reference.

9. Left to the builder

  • How to split the translation work between agents (by language or by topic folder).
  • How the check script parses MDX and counts the elements.
  • How deploy-staging merges the outputs of build and build-locales.

10. Open items for the user or a Crowdin admin

  • Who is the Crowdin admin who creates the KB project and the token, and does the Crowdin plan have space for about 49,000 more words. Step 3 needs this.
  • Who is the native speaker for each language (German, Arabic, Norwegian, French, Italian). Step 4 needs these names.

11. Sources


Did this page help you?