mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-11 01:01:13 +00:00
docs: add generated locale picker support
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
18
docs/.i18n/de-navigation.json
Normal file
18
docs/.i18n/de-navigation.json
Normal 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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
18
docs/.i18n/es-navigation.json
Normal file
18
docs/.i18n/es-navigation.json
Normal 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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
18
docs/.i18n/fr-navigation.json
Normal file
18
docs/.i18n/fr-navigation.json
Normal 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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
5
docs/.i18n/glossary.de.json
Normal file
5
docs/.i18n/glossary.de.json
Normal file
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{ "source": "CLI", "target": "CLI" },
|
||||
{ "source": "Mintlify", "target": "Mintlify" },
|
||||
{ "source": "OpenClaw", "target": "OpenClaw" }
|
||||
]
|
||||
5
docs/.i18n/glossary.es.json
Normal file
5
docs/.i18n/glossary.es.json
Normal file
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{ "source": "CLI", "target": "CLI" },
|
||||
{ "source": "Mintlify", "target": "Mintlify" },
|
||||
{ "source": "OpenClaw", "target": "OpenClaw" }
|
||||
]
|
||||
5
docs/.i18n/glossary.fr.json
Normal file
5
docs/.i18n/glossary.fr.json
Normal file
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{ "source": "CLI", "target": "CLI" },
|
||||
{ "source": "Mintlify", "target": "Mintlify" },
|
||||
{ "source": "OpenClaw", "target": "OpenClaw" }
|
||||
]
|
||||
5
docs/.i18n/glossary.ko.json
Normal file
5
docs/.i18n/glossary.ko.json
Normal file
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{ "source": "CLI", "target": "CLI" },
|
||||
{ "source": "Mintlify", "target": "Mintlify" },
|
||||
{ "source": "OpenClaw", "target": "OpenClaw" }
|
||||
]
|
||||
5
docs/.i18n/glossary.pt-BR.json
Normal file
5
docs/.i18n/glossary.pt-BR.json
Normal file
@@ -0,0 +1,5 @@
|
||||
[
|
||||
{ "source": "CLI", "target": "CLI" },
|
||||
{ "source": "Mintlify", "target": "Mintlify" },
|
||||
{ "source": "OpenClaw", "target": "OpenClaw" }
|
||||
]
|
||||
18
docs/.i18n/ko-navigation.json
Normal file
18
docs/.i18n/ko-navigation.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"language": "ko",
|
||||
"tabs": [
|
||||
{
|
||||
"tab": "시작하기",
|
||||
"groups": [
|
||||
{
|
||||
"group": "개요",
|
||||
"pages": ["ko/index"]
|
||||
},
|
||||
{
|
||||
"group": "첫 단계",
|
||||
"pages": ["ko/start/getting-started", "ko/start/wizard"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
18
docs/.i18n/pt-BR-navigation.json
Normal file
18
docs/.i18n/pt-BR-navigation.json
Normal 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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
Reference in New Issue
Block a user