From 25da786c6847e7df0a6995e505d7c5997d80f912 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 5 Apr 2026 11:50:45 +0100 Subject: [PATCH] docs: add generated locale picker support --- .../docs-translate-trigger-release.yml | 39 +++++----- AGENTS.md | 8 +- docs/.i18n/README.md | 18 ++--- docs/.i18n/de-navigation.json | 18 +++++ docs/.i18n/es-navigation.json | 18 +++++ docs/.i18n/fr-navigation.json | 18 +++++ docs/.i18n/glossary.de.json | 5 ++ docs/.i18n/glossary.es.json | 5 ++ docs/.i18n/glossary.fr.json | 5 ++ docs/.i18n/glossary.ko.json | 5 ++ docs/.i18n/glossary.pt-BR.json | 5 ++ docs/.i18n/ko-navigation.json | 18 +++++ docs/.i18n/pt-BR-navigation.json | 18 +++++ scripts/docs-sync-publish.mjs | 74 ++++++++++--------- 14 files changed, 185 insertions(+), 69 deletions(-) create mode 100644 docs/.i18n/de-navigation.json create mode 100644 docs/.i18n/es-navigation.json create mode 100644 docs/.i18n/fr-navigation.json create mode 100644 docs/.i18n/glossary.de.json create mode 100644 docs/.i18n/glossary.es.json create mode 100644 docs/.i18n/glossary.fr.json create mode 100644 docs/.i18n/glossary.ko.json create mode 100644 docs/.i18n/glossary.pt-BR.json create mode 100644 docs/.i18n/ko-navigation.json create mode 100644 docs/.i18n/pt-BR-navigation.json diff --git a/.github/workflows/docs-translate-trigger-release.yml b/.github/workflows/docs-translate-trigger-release.yml index 67b31ed01d5..c7a9b9c487b 100644 --- a/.github/workflows/docs-translate-trigger-release.yml +++ b/.github/workflows/docs-translate-trigger-release.yml @@ -1,4 +1,4 @@ -name: Docs Trigger zh-CN Translate On Release +name: Docs Trigger Locale Translate On Release on: release: @@ -12,28 +12,25 @@ jobs: dispatch-translate: runs-on: ubuntu-latest steps: - - name: Trigger zh-CN translate in publish repo + - name: Trigger locale translates in publish repo env: GH_TOKEN: ${{ secrets.OPENCLAW_DOCS_SYNC_TOKEN }} RELEASE_TAG: ${{ github.event.release.tag_name }} run: | set -euo pipefail - gh api repos/openclaw/docs/dispatches \ - --method POST \ - -f event_type='translate-zh-cn-release' \ - -f client_payload[release_tag]="${RELEASE_TAG}" \ - -f client_payload[source_repository]="${GITHUB_REPOSITORY}" \ - -f client_payload[source_sha]="${GITHUB_SHA}" - - - name: Trigger ja-JP translate in publish repo - env: - GH_TOKEN: ${{ secrets.OPENCLAW_DOCS_SYNC_TOKEN }} - RELEASE_TAG: ${{ github.event.release.tag_name }} - run: | - set -euo pipefail - gh api repos/openclaw/docs/dispatches \ - --method POST \ - -f event_type='translate-ja-jp-release' \ - -f client_payload[release_tag]="${RELEASE_TAG}" \ - -f client_payload[source_repository]="${GITHUB_REPOSITORY}" \ - -f client_payload[source_sha]="${GITHUB_SHA}" + for event_type in \ + translate-zh-cn-release \ + translate-ja-jp-release \ + translate-es-release \ + translate-pt-br-release \ + translate-ko-release \ + translate-de-release \ + translate-fr-release + do + gh api repos/openclaw/docs/dispatches \ + --method POST \ + -f event_type="${event_type}" \ + -f client_payload[release_tag]="${RELEASE_TAG}" \ + -f client_payload[source_repository]="${GITHUB_REPOSITORY}" \ + -f client_payload[source_sha]="${GITHUB_SHA}" + done diff --git a/AGENTS.md b/AGENTS.md index e1ffa0a33ea..26ff2d01d69 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -81,13 +81,13 @@ - README (GitHub): keep absolute docs URLs (`https://docs.openclaw.ai/...`) so links work on GitHub. - Docs content must be generic: no personal device names/hostnames/paths; use placeholders like `user@gateway-host` and “gateway host”. -## Docs i18n (zh-CN / ja-JP) +## Docs i18n (generated publish locales) -- Generated publish output lives in the sibling `openclaw-docs` repo; do not add or edit `docs/zh-CN/**` or `docs/ja-JP/**` here. -- Pipeline: update English docs here → adjust glossary (`docs/.i18n/glossary.zh-CN.json`, `docs/.i18n/glossary.ja-JP.json`) → let the publish-repo sync + `scripts/docs-i18n` run in `openclaw-docs` → apply targeted fixes only if instructed. +- Generated publish output lives in the sibling `openclaw-docs` repo; do not add or edit `docs/zh-CN/**`, `docs/ja-JP/**`, `docs/es/**`, `docs/pt-BR/**`, `docs/ko/**`, `docs/de/**`, or `docs/fr/**` here. +- Pipeline: update English docs here → adjust glossary (`docs/.i18n/glossary.zh-CN.json`, `docs/.i18n/glossary.ja-JP.json`, `docs/.i18n/glossary.es.json`, `docs/.i18n/glossary.pt-BR.json`, `docs/.i18n/glossary.ko.json`, `docs/.i18n/glossary.de.json`, `docs/.i18n/glossary.fr.json`) → let the publish-repo sync + `scripts/docs-i18n` run in `openclaw-docs` → apply targeted fixes only if instructed. - Before rerunning `scripts/docs-i18n`, add glossary entries for any new technical terms, page titles, or short nav labels that must stay in English or use a fixed translation (for example `Doctor` or `Polls`). - `pnpm docs:check-i18n-glossary` enforces glossary coverage for changed English doc titles and short internal doc labels before translation reruns. -- Translation memory: `docs/.i18n/zh-CN.tm.jsonl` and `docs/.i18n/ja-JP.tm.jsonl` (generated in the publish repo). +- Translation memory: locale TM files such as `docs/.i18n/zh-CN.tm.jsonl`, `docs/.i18n/ja-JP.tm.jsonl`, `docs/.i18n/es.tm.jsonl`, `docs/.i18n/pt-BR.tm.jsonl`, `docs/.i18n/ko.tm.jsonl`, `docs/.i18n/de.tm.jsonl`, and `docs/.i18n/fr.tm.jsonl` (generated in the publish repo). - See `docs/.i18n/README.md`. - The pipeline can be slow/inefficient; if it’s dragging, ping @jospalmbier on Discord instead of hacking around it. diff --git a/docs/.i18n/README.md b/docs/.i18n/README.md index 60c54b6dc07..58f13e25f0a 100644 --- a/docs/.i18n/README.md +++ b/docs/.i18n/README.md @@ -11,16 +11,17 @@ Generated locale trees and live translation memory now live in the publish repo: - English docs are authored in `openclaw/openclaw`. - The source docs tree lives under `docs/`. -- The source repo no longer keeps committed generated locale trees such as `docs/zh-CN/**` or `docs/ja-JP/**`. +- The source repo no longer keeps committed generated locale trees such as `docs/zh-CN/**`, `docs/ja-JP/**`, `docs/es/**`, `docs/pt-BR/**`, `docs/ko/**`, `docs/de/**`, or `docs/fr/**`. ## End-to-end flow 1. Edit English docs in `openclaw/openclaw`. 2. Push to `main`. 3. `openclaw/openclaw/.github/workflows/docs-sync-publish.yml` mirrors the docs tree into `openclaw/docs`. -4. The sync script rewrites the publish `docs/docs.json` so `zh-Hans` navigation exists there even though it is no longer committed in the source repo. +4. The sync script rewrites the publish `docs/docs.json` so the generated locale picker blocks exist there even though they are no longer committed in the source repo. 5. `openclaw/docs/.github/workflows/translate-zh-cn.yml` refreshes `docs/zh-CN/**` once a day, on demand, and after source-repo release dispatches. 6. `openclaw/docs/.github/workflows/translate-ja-jp.yml` does the same for `docs/ja-JP/**`. +7. `openclaw/docs/.github/workflows/translate-es.yml`, `translate-pt-br.yml`, `translate-ko.yml`, `translate-de.yml`, and `translate-fr.yml` do the same for `docs/es/**`, `docs/pt-BR/**`, `docs/ko/**`, `docs/de/**`, and `docs/fr/**`. ## Why the split exists @@ -31,11 +32,10 @@ Generated locale trees and live translation memory now live in the publish repo: ## Files in this folder - `glossary..json` — preferred term mappings used as prompt guidance. -- `ja-navigation.json` — the `ja` Mintlify nav block reinserted into the publish repo during sync. -- `zh-Hans-navigation.json` — the `zh-Hans` Mintlify nav block reinserted into the publish repo during sync. +- `de-navigation.json`, `es-navigation.json`, `fr-navigation.json`, `ja-navigation.json`, `ko-navigation.json`, `pt-BR-navigation.json`, `zh-Hans-navigation.json` — Mintlify locale picker blocks reinserted into the publish repo during sync. - `.tm.jsonl` — translation memory keyed by workflow + model + text hash. -In this repo, generated locale TM files such as `docs/.i18n/zh-CN.tm.jsonl` and `docs/.i18n/ja-JP.tm.jsonl` are intentionally no longer committed. +In this repo, generated locale TM files such as `docs/.i18n/zh-CN.tm.jsonl`, `docs/.i18n/ja-JP.tm.jsonl`, `docs/.i18n/es.tm.jsonl`, `docs/.i18n/pt-BR.tm.jsonl`, `docs/.i18n/ko.tm.jsonl`, `docs/.i18n/de.tm.jsonl`, and `docs/.i18n/fr.tm.jsonl` are intentionally no longer committed. ## Glossary format @@ -44,9 +44,7 @@ In this repo, generated locale TM files such as `docs/.i18n/zh-CN.tm.jsonl` and ```json { "source": "troubleshooting", - "target": "故障排除", - "ignore_case": true, - "whole_word": false + "target": "故障排除" } ``` @@ -63,11 +61,11 @@ Fields: - If the pending count is `0`, the expensive translation step is skipped entirely. - If there are pending files, the workflow translates only those files. - The publish workflow retries transient model-format failures, but unchanged files stay skipped because the same hash check runs on each retry. -- The source repo also dispatches zh-CN and ja-JP refreshes after published GitHub releases so release docs can catch up without waiting for the daily cron. +- The source repo also dispatches zh-CN, ja-JP, es, pt-BR, ko, de, and fr refreshes after published GitHub releases so release docs can catch up without waiting for the daily cron. ## Operational notes - Sync metadata is written to `.openclaw-sync/source.json` in the publish repo. - Source repo secret: `OPENCLAW_DOCS_SYNC_TOKEN` - Publish repo secret: `OPENCLAW_DOCS_I18N_OPENAI_API_KEY` -- If zh-CN output looks stale, check the `Translate zh-CN` workflow in `openclaw/docs` first. +- If locale output looks stale, check the matching `Translate ` workflow in `openclaw/docs` first. diff --git a/docs/.i18n/de-navigation.json b/docs/.i18n/de-navigation.json new file mode 100644 index 00000000000..305ae5c4a7b --- /dev/null +++ b/docs/.i18n/de-navigation.json @@ -0,0 +1,18 @@ +{ + "language": "de", + "tabs": [ + { + "tab": "Loslegen", + "groups": [ + { + "group": "Überblick", + "pages": ["de/index"] + }, + { + "group": "Erste Schritte", + "pages": ["de/start/getting-started", "de/start/wizard"] + } + ] + } + ] +} diff --git a/docs/.i18n/es-navigation.json b/docs/.i18n/es-navigation.json new file mode 100644 index 00000000000..9cc2c53e41e --- /dev/null +++ b/docs/.i18n/es-navigation.json @@ -0,0 +1,18 @@ +{ + "language": "es", + "tabs": [ + { + "tab": "Comenzar", + "groups": [ + { + "group": "Resumen", + "pages": ["es/index"] + }, + { + "group": "Primeros pasos", + "pages": ["es/start/getting-started", "es/start/wizard"] + } + ] + } + ] +} diff --git a/docs/.i18n/fr-navigation.json b/docs/.i18n/fr-navigation.json new file mode 100644 index 00000000000..56e2c59ed39 --- /dev/null +++ b/docs/.i18n/fr-navigation.json @@ -0,0 +1,18 @@ +{ + "language": "fr", + "tabs": [ + { + "tab": "Commencer", + "groups": [ + { + "group": "Vue d'ensemble", + "pages": ["fr/index"] + }, + { + "group": "Premiers pas", + "pages": ["fr/start/getting-started", "fr/start/wizard"] + } + ] + } + ] +} diff --git a/docs/.i18n/glossary.de.json b/docs/.i18n/glossary.de.json new file mode 100644 index 00000000000..461226c5a1f --- /dev/null +++ b/docs/.i18n/glossary.de.json @@ -0,0 +1,5 @@ +[ + { "source": "CLI", "target": "CLI" }, + { "source": "Mintlify", "target": "Mintlify" }, + { "source": "OpenClaw", "target": "OpenClaw" } +] diff --git a/docs/.i18n/glossary.es.json b/docs/.i18n/glossary.es.json new file mode 100644 index 00000000000..461226c5a1f --- /dev/null +++ b/docs/.i18n/glossary.es.json @@ -0,0 +1,5 @@ +[ + { "source": "CLI", "target": "CLI" }, + { "source": "Mintlify", "target": "Mintlify" }, + { "source": "OpenClaw", "target": "OpenClaw" } +] diff --git a/docs/.i18n/glossary.fr.json b/docs/.i18n/glossary.fr.json new file mode 100644 index 00000000000..461226c5a1f --- /dev/null +++ b/docs/.i18n/glossary.fr.json @@ -0,0 +1,5 @@ +[ + { "source": "CLI", "target": "CLI" }, + { "source": "Mintlify", "target": "Mintlify" }, + { "source": "OpenClaw", "target": "OpenClaw" } +] diff --git a/docs/.i18n/glossary.ko.json b/docs/.i18n/glossary.ko.json new file mode 100644 index 00000000000..461226c5a1f --- /dev/null +++ b/docs/.i18n/glossary.ko.json @@ -0,0 +1,5 @@ +[ + { "source": "CLI", "target": "CLI" }, + { "source": "Mintlify", "target": "Mintlify" }, + { "source": "OpenClaw", "target": "OpenClaw" } +] diff --git a/docs/.i18n/glossary.pt-BR.json b/docs/.i18n/glossary.pt-BR.json new file mode 100644 index 00000000000..461226c5a1f --- /dev/null +++ b/docs/.i18n/glossary.pt-BR.json @@ -0,0 +1,5 @@ +[ + { "source": "CLI", "target": "CLI" }, + { "source": "Mintlify", "target": "Mintlify" }, + { "source": "OpenClaw", "target": "OpenClaw" } +] diff --git a/docs/.i18n/ko-navigation.json b/docs/.i18n/ko-navigation.json new file mode 100644 index 00000000000..28e0f7b4f7f --- /dev/null +++ b/docs/.i18n/ko-navigation.json @@ -0,0 +1,18 @@ +{ + "language": "ko", + "tabs": [ + { + "tab": "시작하기", + "groups": [ + { + "group": "개요", + "pages": ["ko/index"] + }, + { + "group": "첫 단계", + "pages": ["ko/start/getting-started", "ko/start/wizard"] + } + ] + } + ] +} diff --git a/docs/.i18n/pt-BR-navigation.json b/docs/.i18n/pt-BR-navigation.json new file mode 100644 index 00000000000..94528c8513a --- /dev/null +++ b/docs/.i18n/pt-BR-navigation.json @@ -0,0 +1,18 @@ +{ + "language": "pt-BR", + "tabs": [ + { + "tab": "Começar", + "groups": [ + { + "group": "Visão geral", + "pages": ["pt-BR/index"] + }, + { + "group": "Primeiros passos", + "pages": ["pt-BR/start/getting-started", "pt-BR/start/wizard"] + } + ] + } + ] +} diff --git a/scripts/docs-sync-publish.mjs b/scripts/docs-sync-publish.mjs index 67b5f514331..48c8aa65a29 100644 --- a/scripts/docs-sync-publish.mjs +++ b/scripts/docs-sync-publish.mjs @@ -9,10 +9,20 @@ const HERE = path.dirname(fileURLToPath(import.meta.url)); const ROOT = path.resolve(HERE, ".."); const SOURCE_DOCS_DIR = path.join(ROOT, "docs"); const SOURCE_CONFIG_PATH = path.join(SOURCE_DOCS_DIR, "docs.json"); -const JA_NAV_PATH = path.join(SOURCE_DOCS_DIR, ".i18n", "ja-navigation.json"); -const JA_TM_PATH = path.join(SOURCE_DOCS_DIR, ".i18n", "ja-JP.tm.jsonl"); -const ZH_NAV_PATH = path.join(SOURCE_DOCS_DIR, ".i18n", "zh-Hans-navigation.json"); -const ZH_TM_PATH = path.join(SOURCE_DOCS_DIR, ".i18n", "zh-CN.tm.jsonl"); +const GENERATED_LOCALES = [ + { + language: "zh-Hans", + dir: "zh-CN", + navFile: "zh-Hans-navigation.json", + tmFile: "zh-CN.tm.jsonl", + }, + { language: "ja", dir: "ja-JP", navFile: "ja-navigation.json", tmFile: "ja-JP.tm.jsonl" }, + { language: "es", dir: "es", navFile: "es-navigation.json", tmFile: "es.tm.jsonl" }, + { language: "pt-BR", dir: "pt-BR", navFile: "pt-BR-navigation.json", tmFile: "pt-BR.tm.jsonl" }, + { language: "ko", dir: "ko", navFile: "ko-navigation.json", tmFile: "ko.tm.jsonl" }, + { language: "de", dir: "de", navFile: "de-navigation.json", tmFile: "de.tm.jsonl" }, + { language: "fr", dir: "fr", navFile: "fr-navigation.json", tmFile: "fr.tm.jsonl" }, +]; function parseArgs(argv) { const args = { @@ -71,19 +81,18 @@ function writeJson(filePath, value) { function composeDocsConfig() { const sourceConfig = readJson(SOURCE_CONFIG_PATH); - const jaNavigation = readJson(JA_NAV_PATH); - const zhNavigation = readJson(ZH_NAV_PATH); const languages = sourceConfig?.navigation?.languages; if (!Array.isArray(languages)) { throw new Error("docs/docs.json is missing navigation.languages"); } - const withoutGenerated = languages.filter( - (entry) => entry?.language !== "zh-Hans" && entry?.language !== "ja", - ); + const generatedLanguageSet = new Set(GENERATED_LOCALES.map((entry) => entry.language)); + const withoutGenerated = languages.filter((entry) => !generatedLanguageSet.has(entry?.language)); const enIndex = withoutGenerated.findIndex((entry) => entry?.language === "en"); - const generated = [zhNavigation, jaNavigation]; + const generated = GENERATED_LOCALES.map((entry) => + readJson(path.join(SOURCE_DOCS_DIR, ".i18n", entry.navFile)), + ); if (enIndex === -1) { withoutGenerated.push(...generated); } else { @@ -103,39 +112,36 @@ function syncDocsTree(targetRoot) { const targetDocsDir = path.join(targetRoot, "docs"); ensureDir(targetDocsDir); + const localeFilters = GENERATED_LOCALES.flatMap((entry) => [ + "--filter", + `P ${entry.dir}/`, + "--filter", + `P .i18n/${entry.tmFile}`, + "--exclude", + `${entry.dir}/`, + "--exclude", + `.i18n/${entry.tmFile}`, + ]); + run("rsync", [ "-a", "--delete", "--filter", - "P ja-JP/", - "--filter", - "P zh-CN/", - "--filter", - "P .i18n/ja-JP.tm.jsonl", - "--filter", - "P .i18n/zh-CN.tm.jsonl", + "P .i18n/README.md", "--exclude", - "ja-JP/", - "--exclude", - "zh-CN/", - "--exclude", - ".i18n/ja-JP.tm.jsonl", - "--exclude", - ".i18n/zh-CN.tm.jsonl", + ".i18n/README.md", + ...localeFilters, `${SOURCE_DOCS_DIR}/`, `${targetDocsDir}/`, ]); - const targetJaTmPath = path.join(targetDocsDir, ".i18n", "ja-JP.tm.jsonl"); - if (!fs.existsSync(targetJaTmPath) && fs.existsSync(JA_TM_PATH)) { - ensureDir(path.dirname(targetJaTmPath)); - fs.copyFileSync(JA_TM_PATH, targetJaTmPath); - } - - const targetZhTmPath = path.join(targetDocsDir, ".i18n", "zh-CN.tm.jsonl"); - if (!fs.existsSync(targetZhTmPath) && fs.existsSync(ZH_TM_PATH)) { - ensureDir(path.dirname(targetZhTmPath)); - fs.copyFileSync(ZH_TM_PATH, targetZhTmPath); + for (const locale of GENERATED_LOCALES) { + const sourceTmPath = path.join(SOURCE_DOCS_DIR, ".i18n", locale.tmFile); + const targetTmPath = path.join(targetDocsDir, ".i18n", locale.tmFile); + if (!fs.existsSync(targetTmPath) && fs.existsSync(sourceTmPath)) { + ensureDir(path.dirname(targetTmPath)); + fs.copyFileSync(sourceTmPath, targetTmPath); + } } writeJson(path.join(targetDocsDir, "docs.json"), composeDocsConfig());