mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(docs-i18n): preserve Max translation boundaries
This commit is contained in:
@@ -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": "设备发现 + 传输协议"
|
||||
|
||||
@@ -16,7 +16,9 @@ OpenClaw ACP runtime backend with plugin-owned session and transport management.
|
||||
|
||||
## Surface
|
||||
|
||||
skills; session catalog: pi
|
||||
skills
|
||||
|
||||
<!-- openclaw-plugin-reference:manual-start -->
|
||||
|
||||
## 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.
|
||||
|
||||
<!-- openclaw-plugin-reference:manual-end -->
|
||||
|
||||
## Related docs
|
||||
|
||||
- [acpx](/tools/acp-agents-setup)
|
||||
|
||||
@@ -20,4 +20,4 @@ providers: chutes
|
||||
|
||||
## Related docs
|
||||
|
||||
- [chutes](/providers/chutes)
|
||||
- [Chutes](/providers/chutes)
|
||||
|
||||
@@ -20,4 +20,4 @@ channels: discord; contracts: transcriptSourceProviders; skills
|
||||
|
||||
## Related docs
|
||||
|
||||
- [discord](/channels/discord)
|
||||
- [Discord](/channels/discord)
|
||||
|
||||
@@ -20,4 +20,4 @@ providers: fireworks
|
||||
|
||||
## Related docs
|
||||
|
||||
- [fireworks](/providers/fireworks)
|
||||
- [Fireworks](/providers/fireworks)
|
||||
|
||||
@@ -20,4 +20,4 @@ channels: googlechat
|
||||
|
||||
## Related docs
|
||||
|
||||
- [googlechat](/channels/googlechat)
|
||||
- [Google Chat](/channels/googlechat)
|
||||
|
||||
@@ -20,4 +20,4 @@ channels: imessage; skills
|
||||
|
||||
## Related docs
|
||||
|
||||
- [imessage](/channels/imessage)
|
||||
- [iMessage](/channels/imessage)
|
||||
|
||||
@@ -20,4 +20,4 @@ channels: line
|
||||
|
||||
## Related docs
|
||||
|
||||
- [line](/channels/line)
|
||||
- [LINE](/channels/line)
|
||||
|
||||
@@ -20,4 +20,4 @@ channels: matrix
|
||||
|
||||
## Related docs
|
||||
|
||||
- [matrix](/channels/matrix)
|
||||
- [Matrix](/channels/matrix)
|
||||
|
||||
@@ -20,4 +20,4 @@ providers: meta
|
||||
|
||||
## Related docs
|
||||
|
||||
- [meta](/providers/meta)
|
||||
- [Meta](/providers/meta)
|
||||
|
||||
@@ -20,4 +20,4 @@ channels: msteams
|
||||
|
||||
## Related docs
|
||||
|
||||
- [msteams](/channels/msteams)
|
||||
- [Microsoft Teams](/channels/msteams)
|
||||
|
||||
@@ -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
|
||||
|
||||
<!-- openclaw-plugin-reference:manual-start -->
|
||||
|
||||
## 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.
|
||||
|
||||
<!-- openclaw-plugin-reference:manual-end -->
|
||||
|
||||
## Related docs
|
||||
|
||||
- [opencode](/providers/opencode)
|
||||
|
||||
@@ -20,4 +20,4 @@ channels: raft
|
||||
|
||||
## Related docs
|
||||
|
||||
- [raft](/channels/raft)
|
||||
- [Raft](/channels/raft)
|
||||
|
||||
@@ -20,4 +20,4 @@ contracts: videoGenerationProviders
|
||||
|
||||
## Related docs
|
||||
|
||||
- [runway](/providers/runway)
|
||||
- [Runway](/providers/runway)
|
||||
|
||||
@@ -20,4 +20,4 @@ channels: signal
|
||||
|
||||
## Related docs
|
||||
|
||||
- [signal](/channels/signal)
|
||||
- [Signal](/channels/signal)
|
||||
|
||||
@@ -20,4 +20,4 @@ channels: slack; skills
|
||||
|
||||
## Related docs
|
||||
|
||||
- [slack](/channels/slack)
|
||||
- [Slack](/channels/slack)
|
||||
|
||||
@@ -20,4 +20,4 @@ providers: synthetic
|
||||
|
||||
## Related docs
|
||||
|
||||
- [synthetic](/providers/synthetic)
|
||||
- [Synthetic](/providers/synthetic)
|
||||
|
||||
@@ -20,4 +20,4 @@ channels: telegram
|
||||
|
||||
## Related docs
|
||||
|
||||
- [telegram](/channels/telegram)
|
||||
- [Telegram](/channels/telegram)
|
||||
|
||||
@@ -20,4 +20,4 @@ contracts: agentToolResultMiddleware
|
||||
|
||||
## Related docs
|
||||
|
||||
- [tokenjuice](/tools/tokenjuice)
|
||||
- [Tokenjuice](/tools/tokenjuice)
|
||||
|
||||
@@ -20,4 +20,4 @@ channels: whatsapp; contracts: tools; skills
|
||||
|
||||
## Related docs
|
||||
|
||||
- [whatsapp](/channels/whatsapp)
|
||||
- [WhatsApp](/channels/whatsapp)
|
||||
|
||||
@@ -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 <body> tokens, a bare body-only payload is
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import (
|
||||
|
||||
const (
|
||||
workflowVersion = 16
|
||||
promptVersion = 26
|
||||
promptVersion = 27
|
||||
docsI18nEngineName = "codex"
|
||||
envDocsI18nProvider = "OPENCLAW_DOCS_I18N_PROVIDER"
|
||||
envDocsI18nModel = "OPENCLAW_DOCS_I18N_MODEL"
|
||||
|
||||
@@ -34,6 +34,26 @@ const PLUGIN_DOC_ALIASES = new Map([
|
||||
const SKIPPED_REFERENCE_PAGE_IDS = new Set(["parallel"]);
|
||||
const MANUAL_SECTION_START = "<!-- openclaw-plugin-reference:manual-start -->";
|
||||
const MANUAL_SECTION_END = "<!-- openclaw-plugin-reference:manual-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),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user