From 9d3de463a5da5378d52af5b511f5711923073a44 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 14 Jul 2026 16:35:37 -0400 Subject: [PATCH] fix(docs-i18n): preserve Max translation boundaries --- docs/.i18n/glossary.zh-CN.json | 32 ++++++++++++++ docs/plugins/reference/acpx.md | 6 ++- docs/plugins/reference/chutes.md | 2 +- docs/plugins/reference/discord.md | 2 +- docs/plugins/reference/fireworks.md | 2 +- docs/plugins/reference/googlechat.md | 2 +- docs/plugins/reference/imessage.md | 2 +- docs/plugins/reference/line.md | 2 +- docs/plugins/reference/matrix.md | 2 +- docs/plugins/reference/meta.md | 2 +- docs/plugins/reference/msteams.md | 2 +- docs/plugins/reference/opencode.md | 10 +++-- docs/plugins/reference/raft.md | 2 +- docs/plugins/reference/runway.md | 2 +- docs/plugins/reference/signal.md | 2 +- docs/plugins/reference/slack.md | 2 +- docs/plugins/reference/synthetic.md | 2 +- docs/plugins/reference/telegram.md | 2 +- docs/plugins/reference/tokenjuice.md | 2 +- docs/plugins/reference/whatsapp.md | 2 +- scripts/docs-i18n/doc_chunked_raw.go | 28 ++++++++++++ scripts/docs-i18n/doc_mode_test.go | 20 +++++---- scripts/docs-i18n/util.go | 2 +- scripts/generate-plugin-inventory-doc.mjs | 52 +++++++++++++++++++---- 24 files changed, 146 insertions(+), 38 deletions(-) diff --git a/docs/.i18n/glossary.zh-CN.json b/docs/.i18n/glossary.zh-CN.json index 34c737685b24..ce339cf3aeb8 100644 --- a/docs/.i18n/glossary.zh-CN.json +++ b/docs/.i18n/glossary.zh-CN.json @@ -547,6 +547,14 @@ "source": "Discord", "target": "Discord" }, + { + "source": "Fireworks", + "target": "Fireworks" + }, + { + "source": "Google Chat", + "target": "Google Chat" + }, { "source": "Matrix", "target": "Matrix" @@ -555,14 +563,38 @@ "source": "Microsoft Teams", "target": "Microsoft Teams" }, + { + "source": "Raft", + "target": "Raft" + }, + { + "source": "Runway", + "target": "Runway" + }, + { + "source": "Signal", + "target": "Signal" + }, { "source": "Slack", "target": "Slack" }, + { + "source": "Synthetic", + "target": "Synthetic" + }, { "source": "Telegram", "target": "Telegram" }, + { + "source": "Tokenjuice", + "target": "Tokenjuice" + }, + { + "source": "WhatsApp", + "target": "WhatsApp" + }, { "source": "Discovery + transports", "target": "设备发现 + 传输协议" diff --git a/docs/plugins/reference/acpx.md b/docs/plugins/reference/acpx.md index f19a977818fe..4fc8445accc8 100644 --- a/docs/plugins/reference/acpx.md +++ b/docs/plugins/reference/acpx.md @@ -16,7 +16,9 @@ OpenClaw ACP runtime backend with plugin-owned session and transport management. ## Surface -skills; session catalog: pi +skills + + ## Pi native sessions @@ -30,6 +32,8 @@ from the directory containing their `settings.json` file. Turn **Pi Session Catalog** off under **Config > Plugins > ACPX Runtime** to disable discovery. It is enabled by default. + + ## Related docs - [acpx](/tools/acp-agents-setup) diff --git a/docs/plugins/reference/chutes.md b/docs/plugins/reference/chutes.md index 495538545c18..b21485433fe5 100644 --- a/docs/plugins/reference/chutes.md +++ b/docs/plugins/reference/chutes.md @@ -20,4 +20,4 @@ providers: chutes ## Related docs -- [chutes](/providers/chutes) +- [Chutes](/providers/chutes) diff --git a/docs/plugins/reference/discord.md b/docs/plugins/reference/discord.md index 36d2581801b5..876cc94ef55a 100644 --- a/docs/plugins/reference/discord.md +++ b/docs/plugins/reference/discord.md @@ -20,4 +20,4 @@ channels: discord; contracts: transcriptSourceProviders; skills ## Related docs -- [discord](/channels/discord) +- [Discord](/channels/discord) diff --git a/docs/plugins/reference/fireworks.md b/docs/plugins/reference/fireworks.md index f4cf6c89897f..a7566c710130 100644 --- a/docs/plugins/reference/fireworks.md +++ b/docs/plugins/reference/fireworks.md @@ -20,4 +20,4 @@ providers: fireworks ## Related docs -- [fireworks](/providers/fireworks) +- [Fireworks](/providers/fireworks) diff --git a/docs/plugins/reference/googlechat.md b/docs/plugins/reference/googlechat.md index 988e2df0d3a4..e23a5ae7980b 100644 --- a/docs/plugins/reference/googlechat.md +++ b/docs/plugins/reference/googlechat.md @@ -20,4 +20,4 @@ channels: googlechat ## Related docs -- [googlechat](/channels/googlechat) +- [Google Chat](/channels/googlechat) diff --git a/docs/plugins/reference/imessage.md b/docs/plugins/reference/imessage.md index a5abd7371d48..b67e15f8fd23 100644 --- a/docs/plugins/reference/imessage.md +++ b/docs/plugins/reference/imessage.md @@ -20,4 +20,4 @@ channels: imessage; skills ## Related docs -- [imessage](/channels/imessage) +- [iMessage](/channels/imessage) diff --git a/docs/plugins/reference/line.md b/docs/plugins/reference/line.md index 14e4d8e3c4f3..9a191a83c2c7 100644 --- a/docs/plugins/reference/line.md +++ b/docs/plugins/reference/line.md @@ -20,4 +20,4 @@ channels: line ## Related docs -- [line](/channels/line) +- [LINE](/channels/line) diff --git a/docs/plugins/reference/matrix.md b/docs/plugins/reference/matrix.md index eaf15f5a5cb6..a334e20cac51 100644 --- a/docs/plugins/reference/matrix.md +++ b/docs/plugins/reference/matrix.md @@ -20,4 +20,4 @@ channels: matrix ## Related docs -- [matrix](/channels/matrix) +- [Matrix](/channels/matrix) diff --git a/docs/plugins/reference/meta.md b/docs/plugins/reference/meta.md index 6f25c2af491c..139edb031413 100644 --- a/docs/plugins/reference/meta.md +++ b/docs/plugins/reference/meta.md @@ -20,4 +20,4 @@ providers: meta ## Related docs -- [meta](/providers/meta) +- [Meta](/providers/meta) diff --git a/docs/plugins/reference/msteams.md b/docs/plugins/reference/msteams.md index 48c1499c889b..bd10f925b29f 100644 --- a/docs/plugins/reference/msteams.md +++ b/docs/plugins/reference/msteams.md @@ -20,4 +20,4 @@ channels: msteams ## Related docs -- [msteams](/channels/msteams) +- [Microsoft Teams](/channels/msteams) diff --git a/docs/plugins/reference/opencode.md b/docs/plugins/reference/opencode.md index 72ab23e7e18e..8689369d066a 100644 --- a/docs/plugins/reference/opencode.md +++ b/docs/plugins/reference/opencode.md @@ -1,5 +1,5 @@ --- -summary: "Adds OpenCode model provider and native session catalog support to OpenClaw." +summary: "Adds OpenCode model provider support to OpenClaw." read_when: - You are installing, configuring, or auditing the opencode plugin title: "OpenCode plugin" @@ -7,7 +7,7 @@ title: "OpenCode plugin" # OpenCode plugin -Adds OpenCode model provider and native session catalog support to OpenClaw. +Adds OpenCode model provider support to OpenClaw. ## Distribution @@ -16,7 +16,9 @@ Adds OpenCode model provider and native session catalog support to OpenClaw. ## Surface -providers: opencode; contracts: mediaUnderstandingProviders; session catalog: opencode +providers: opencode; contracts: mediaUnderstandingProviders + + ## Native sessions @@ -30,6 +32,8 @@ Gateway credentials. Turn **OpenCode Session Catalog** off under **Config > Plugins > OpenCode** to disable discovery. It is enabled by default. + + ## Related docs - [opencode](/providers/opencode) diff --git a/docs/plugins/reference/raft.md b/docs/plugins/reference/raft.md index ac35c7e5d13a..201a2d1e0863 100644 --- a/docs/plugins/reference/raft.md +++ b/docs/plugins/reference/raft.md @@ -20,4 +20,4 @@ channels: raft ## Related docs -- [raft](/channels/raft) +- [Raft](/channels/raft) diff --git a/docs/plugins/reference/runway.md b/docs/plugins/reference/runway.md index 9d7736ed5210..607ccb295ecc 100644 --- a/docs/plugins/reference/runway.md +++ b/docs/plugins/reference/runway.md @@ -20,4 +20,4 @@ contracts: videoGenerationProviders ## Related docs -- [runway](/providers/runway) +- [Runway](/providers/runway) diff --git a/docs/plugins/reference/signal.md b/docs/plugins/reference/signal.md index abb11354a212..b628b60626ed 100644 --- a/docs/plugins/reference/signal.md +++ b/docs/plugins/reference/signal.md @@ -20,4 +20,4 @@ channels: signal ## Related docs -- [signal](/channels/signal) +- [Signal](/channels/signal) diff --git a/docs/plugins/reference/slack.md b/docs/plugins/reference/slack.md index 9683db08ae0a..1c8b2686ef5e 100644 --- a/docs/plugins/reference/slack.md +++ b/docs/plugins/reference/slack.md @@ -20,4 +20,4 @@ channels: slack; skills ## Related docs -- [slack](/channels/slack) +- [Slack](/channels/slack) diff --git a/docs/plugins/reference/synthetic.md b/docs/plugins/reference/synthetic.md index bc5573d60c92..96e14d67a9e8 100644 --- a/docs/plugins/reference/synthetic.md +++ b/docs/plugins/reference/synthetic.md @@ -20,4 +20,4 @@ providers: synthetic ## Related docs -- [synthetic](/providers/synthetic) +- [Synthetic](/providers/synthetic) diff --git a/docs/plugins/reference/telegram.md b/docs/plugins/reference/telegram.md index 960f796fc445..01607f90c30b 100644 --- a/docs/plugins/reference/telegram.md +++ b/docs/plugins/reference/telegram.md @@ -20,4 +20,4 @@ channels: telegram ## Related docs -- [telegram](/channels/telegram) +- [Telegram](/channels/telegram) diff --git a/docs/plugins/reference/tokenjuice.md b/docs/plugins/reference/tokenjuice.md index 4c808a6854fc..04f4008bd361 100644 --- a/docs/plugins/reference/tokenjuice.md +++ b/docs/plugins/reference/tokenjuice.md @@ -20,4 +20,4 @@ contracts: agentToolResultMiddleware ## Related docs -- [tokenjuice](/tools/tokenjuice) +- [Tokenjuice](/tools/tokenjuice) diff --git a/docs/plugins/reference/whatsapp.md b/docs/plugins/reference/whatsapp.md index 657f02c190eb..d056f8d18449 100644 --- a/docs/plugins/reference/whatsapp.md +++ b/docs/plugins/reference/whatsapp.md @@ -20,4 +20,4 @@ channels: whatsapp; contracts: tools; skills ## Related docs -- [whatsapp](/channels/whatsapp) +- [WhatsApp](/channels/whatsapp) diff --git a/scripts/docs-i18n/doc_chunked_raw.go b/scripts/docs-i18n/doc_chunked_raw.go index 10b1354f3c7d..a71fbe5afc15 100644 --- a/scripts/docs-i18n/doc_chunked_raw.go +++ b/scripts/docs-i18n/doc_chunked_raw.go @@ -135,6 +135,7 @@ func translateDocBlockGroup(ctx context.Context, translator docsTranslator, chun } if err == nil { translated = sanitizeDocChunkProtocolWrappers(source, translated) + translated = preserveDocChunkBoundaryWhitespace(normalizedSource, translated) translated = reapplyCommonIndent(translated, commonIndent) if validationErr := validateDocChunkTranslation(source, translated); validationErr == nil { log.Printf("docs-i18n: chunk done %s out_bytes=%d", chunkID, len(translated)) @@ -183,6 +184,7 @@ func translateDocLeafBlock(ctx context.Context, translator docsTranslator, chunk return "", err } translated = sanitizeDocChunkProtocolWrappers(source, translated) + translated = preserveDocChunkBoundaryWhitespace(normalizedSource, translated) translated = reapplyCommonIndent(translated, commonIndent) if validationErr := validateDocChunkTranslation(source, translated); validationErr != nil { return "", validationErr @@ -365,6 +367,32 @@ func sanitizeDocChunkProtocolWrappers(source, translated string) string { return body } +func preserveDocChunkBoundaryWhitespace(source, translated string) string { + prefixEnd := 0 + for prefixEnd < len(source) && isDocChunkBoundaryWhitespace(source[prefixEnd]) { + prefixEnd++ + } + suffixStart := len(source) + for suffixStart > prefixEnd && isDocChunkBoundaryWhitespace(source[suffixStart-1]) { + suffixStart-- + } + + translatedStart := 0 + for translatedStart < len(translated) && isDocChunkBoundaryWhitespace(translated[translatedStart]) { + translatedStart++ + } + translatedEnd := len(translated) + for translatedEnd > translatedStart && isDocChunkBoundaryWhitespace(translated[translatedEnd-1]) { + translatedEnd-- + } + + return source[:prefixEnd] + translated[translatedStart:translatedEnd] + source[suffixStart:] +} + +func isDocChunkBoundaryWhitespace(value byte) bool { + return value == ' ' || value == '\t' || value == '\r' || value == '\n' +} + func stripBodyOnlyWrapper(source, text string) (string, bool) { sourceLower := strings.ToLower(source) // When the source itself documents tokens, a bare body-only payload is diff --git a/scripts/docs-i18n/doc_mode_test.go b/scripts/docs-i18n/doc_mode_test.go index 036cc4023908..139ccd8c43fb 100644 --- a/scripts/docs-i18n/doc_mode_test.go +++ b/scripts/docs-i18n/doc_mode_test.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "regexp" + "slices" "strconv" "strings" "testing" @@ -1911,16 +1912,19 @@ func TestTranslateDocBodyChunkedStripsUppercaseBodyWrapper(t *testing.T) { } } -func TestTranslateDocBodyChunkedRejectsListCorruptionAcrossSanitizedChunkBoundary(t *testing.T) { +func TestTranslateDocBodyChunkedPreservesListStructureAcrossSanitizedChunkBoundary(t *testing.T) { body := "Intro paragraph.\n\n1. First item\n2. Second item\n\n" t.Setenv("OPENCLAW_DOCS_I18N_DOC_CHUNK_MAX_BYTES", "20") - _, err := translateDocBodyChunked(context.Background(), boundaryWrapperTranslator{}, "example.md", body, "en", "de") - if err == nil { - t.Fatal("expected final-document list corruption across chunk boundaries to be rejected") + translated, err := translateDocBodyChunked(context.Background(), boundaryWrapperTranslator{}, "example.md", body, "en", "de") + if err != nil { + t.Fatalf("expected chunk-boundary whitespace to be restored, got %v", err) } - if !strings.Contains(err.Error(), "final document validation: list structure mismatch") { - t.Fatalf("expected final list structure mismatch, got %v", err) + if !slices.Equal(extractMarkdownListShapes(body), extractMarkdownListShapes(translated)) { + t.Fatalf("expected list structure to survive chunk assembly:\n%s", translated) + } + if !strings.Contains(translated, "Einleitung\n\n1. Erster Eintrag") { + t.Fatalf("expected paragraph/list boundary to survive chunk assembly:\n%s", translated) } } @@ -2325,8 +2329,8 @@ func TestProcessFileDocUsesFieldLevelFrontmatterTranslation(t *testing.T) { if !strings.Contains(text, "在 Fly.io 上部署 OpenClaw") { t.Fatalf("expected translated read_when entry in output:\n%s", text) } - if !strings.Contains(text, "prompt_version: 26") { - t.Fatalf("expected prompt version 26 in output metadata:\n%s", text) + if !strings.Contains(text, "prompt_version: 27") { + t.Fatalf("expected prompt version 27 in output metadata:\n%s", text) } } diff --git a/scripts/docs-i18n/util.go b/scripts/docs-i18n/util.go index 41b923633a63..4005f45855bb 100644 --- a/scripts/docs-i18n/util.go +++ b/scripts/docs-i18n/util.go @@ -12,7 +12,7 @@ import ( const ( workflowVersion = 16 - promptVersion = 26 + promptVersion = 27 docsI18nEngineName = "codex" envDocsI18nProvider = "OPENCLAW_DOCS_I18N_PROVIDER" envDocsI18nModel = "OPENCLAW_DOCS_I18N_MODEL" diff --git a/scripts/generate-plugin-inventory-doc.mjs b/scripts/generate-plugin-inventory-doc.mjs index 3f2eae28a115..1f8a06f1101f 100644 --- a/scripts/generate-plugin-inventory-doc.mjs +++ b/scripts/generate-plugin-inventory-doc.mjs @@ -34,6 +34,26 @@ const PLUGIN_DOC_ALIASES = new Map([ const SKIPPED_REFERENCE_PAGE_IDS = new Set(["parallel"]); const MANUAL_SECTION_START = ""; const MANUAL_SECTION_END = ""; +// Generated link labels are user-visible product names and translation source. +const RELATED_DOC_PRODUCT_IDS = new Set([ + "chutes", + "discord", + "fireworks", + "googlechat", + "imessage", + "line", + "matrix", + "meta", + "msteams", + "raft", + "runway", + "signal", + "slack", + "synthetic", + "telegram", + "tokenjuice", + "whatsapp", +]); function readJson(relativePath) { return JSON.parse(fs.readFileSync(path.join(ROOT, relativePath), "utf8")); @@ -72,6 +92,10 @@ function docLink({ label, href }) { return `[${label}](${href})`; } +function relatedDocLabel(value) { + return RELATED_DOC_PRODUCT_IDS.has(value) ? humanizeId(value) : value; +} + function pluginReferencePath(id) { return `/plugins/reference/${id}`; } @@ -244,7 +268,7 @@ function resolveDocs({ dirName, manifest, packageJson }) { const links = []; const pluginAlias = PLUGIN_DOC_ALIASES.get(manifest.id) ?? PLUGIN_DOC_ALIASES.get(dirName); if (pluginAlias) { - const pluginAliasLabel = manifest.id ?? dirName; + const pluginAliasLabel = relatedDocLabel(manifest.id ?? dirName); pushUniqueDocLink(links, { href: pluginAlias, label: pluginAliasLabel }); } @@ -252,7 +276,7 @@ function resolveDocs({ dirName, manifest, packageJson }) { if (channelDoc) { pushUniqueDocLink(links, { href: channelDoc, - label: channelDoc.replace(/^\/channels\//u, ""), + label: relatedDocLabel(channelDoc.replace(/^\/channels\//u, "")), }); } @@ -262,7 +286,7 @@ function resolveDocs({ dirName, manifest, packageJson }) { } const relativePath = `docs/channels/${channel}.md`; if (fileExists(relativePath)) { - pushUniqueDocLink(links, { href: `/channels/${channel}`, label: channel }); + pushUniqueDocLink(links, { href: `/channels/${channel}`, label: relatedDocLabel(channel) }); } } @@ -272,12 +296,15 @@ function resolveDocs({ dirName, manifest, packageJson }) { } const alias = PROVIDER_DOC_ALIASES.get(provider); if (alias) { - pushUniqueDocLink(links, { href: alias, label: provider }); + pushUniqueDocLink(links, { href: alias, label: relatedDocLabel(provider) }); continue; } const relativePath = `docs/providers/${provider}.md`; if (fileExists(relativePath)) { - pushUniqueDocLink(links, { href: `/providers/${provider}`, label: provider }); + pushUniqueDocLink(links, { + href: `/providers/${provider}`, + label: relatedDocLabel(provider), + }); } } @@ -286,13 +313,22 @@ function resolveDocs({ dirName, manifest, packageJson }) { continue; } if (fileExists(`docs/channels/${candidate}.md`)) { - pushUniqueDocLink(links, { href: `/channels/${candidate}`, label: candidate }); + pushUniqueDocLink(links, { + href: `/channels/${candidate}`, + label: relatedDocLabel(candidate), + }); } if (fileExists(`docs/providers/${candidate}.md`)) { - pushUniqueDocLink(links, { href: `/providers/${candidate}`, label: candidate }); + pushUniqueDocLink(links, { + href: `/providers/${candidate}`, + label: relatedDocLabel(candidate), + }); } if (fileExists(`docs/plugins/${candidate}.md`)) { - pushUniqueDocLink(links, { href: `/plugins/${candidate}`, label: candidate }); + pushUniqueDocLink(links, { + href: `/plugins/${candidate}`, + label: relatedDocLabel(candidate), + }); } }