docs: add generated locale picker support

This commit is contained in:
Peter Steinberger
2026-04-05 11:50:45 +01:00
parent cfe66c6e02
commit 25da786c68
14 changed files with 185 additions and 69 deletions

View File

@@ -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

View File

@@ -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 its dragging, ping @jospalmbier on Discord instead of hacking around it.

View File

@@ -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.<lang>.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.
- `<lang>.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 <locale>` workflow in `openclaw/docs` first.

View File

@@ -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"]
}
]
}
]
}

View File

@@ -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"]
}
]
}
]
}

View File

@@ -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"]
}
]
}
]
}

View File

@@ -0,0 +1,5 @@
[
{ "source": "CLI", "target": "CLI" },
{ "source": "Mintlify", "target": "Mintlify" },
{ "source": "OpenClaw", "target": "OpenClaw" }
]

View File

@@ -0,0 +1,5 @@
[
{ "source": "CLI", "target": "CLI" },
{ "source": "Mintlify", "target": "Mintlify" },
{ "source": "OpenClaw", "target": "OpenClaw" }
]

View File

@@ -0,0 +1,5 @@
[
{ "source": "CLI", "target": "CLI" },
{ "source": "Mintlify", "target": "Mintlify" },
{ "source": "OpenClaw", "target": "OpenClaw" }
]

View File

@@ -0,0 +1,5 @@
[
{ "source": "CLI", "target": "CLI" },
{ "source": "Mintlify", "target": "Mintlify" },
{ "source": "OpenClaw", "target": "OpenClaw" }
]

View File

@@ -0,0 +1,5 @@
[
{ "source": "CLI", "target": "CLI" },
{ "source": "Mintlify", "target": "Mintlify" },
{ "source": "OpenClaw", "target": "OpenClaw" }
]

View File

@@ -0,0 +1,18 @@
{
"language": "ko",
"tabs": [
{
"tab": "시작하기",
"groups": [
{
"group": "개요",
"pages": ["ko/index"]
},
{
"group": "첫 단계",
"pages": ["ko/start/getting-started", "ko/start/wizard"]
}
]
}
]
}

View File

@@ -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"]
}
]
}
]
}

View File

@@ -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());