diff --git a/scripts/check-docs-mdx.mjs b/scripts/check-docs-mdx.mjs
index 485683b2383..90a391378dd 100644
--- a/scripts/check-docs-mdx.mjs
+++ b/scripts/check-docs-mdx.mjs
@@ -64,6 +64,10 @@ const POISON_TEXT_PATTERNS = [
pattern: /\b[A-Za-z_\u3400-\u9fff][\w\u3400-\u9fff-]*_input=\{/u,
message: "Leaked tool-call input payload.",
},
+ {
+ pattern: /<\/?openclaw_docs_i18n_input>/iu,
+ message: "Leaked docs i18n prompt wrapper.",
+ },
{
pattern: /\/home\/runner\/work\//u,
message: "Leaked GitHub Actions workspace path.",
diff --git a/scripts/docs-i18n/main_test.go b/scripts/docs-i18n/main_test.go
index 5da9f53e94c..b1f21cc86ea 100644
--- a/scripts/docs-i18n/main_test.go
+++ b/scripts/docs-i18n/main_test.go
@@ -144,6 +144,7 @@ func TestValidateNoTranslationTranscriptArtifacts(t *testing.T) {
tests := []string{
`表情回应 analysis to=functions.read {"path":"/home/runner/work/docs/docs/source/.agents/skills/openclaw-qa-testing/SKILL.md"} code`,
+ "\nTranslated\n",
`กำลังทำงานกับ reactions to=functions.read commentary  ̄第四色json 皇平台`,
`คุณต้องการแผนที่เอกสาร analysis to=final code omitted`,
`Potrzebujesz listy funkcji TUI force_parallel: false} code`,
diff --git a/scripts/docs-i18n/prompt.go b/scripts/docs-i18n/prompt.go
index 9543e5207d4..4c566d6356a 100644
--- a/scripts/docs-i18n/prompt.go
+++ b/scripts/docs-i18n/prompt.go
@@ -17,6 +17,8 @@ func prettyLanguageLabel(lang string) string {
return "Simplified Chinese"
case strings.EqualFold(trimmed, "ja-JP"):
return "Japanese"
+ case strings.EqualFold(trimmed, "de"):
+ return "German"
case strings.EqualFold(trimmed, "th"):
return "Thai"
case strings.EqualFold(trimmed, "uk"):
@@ -38,7 +40,16 @@ func translationPrompt(srcLang, tgtLang string, glossary []GlossaryEntry) string
case strings.EqualFold(tgtLang, "ja-JP"):
return strings.TrimSpace(fmt.Sprintf(jaJPPromptTemplate, srcLabel, tgtLabel, glossaryBlock))
default:
- return strings.TrimSpace(fmt.Sprintf(genericPromptTemplate, srcLabel, tgtLabel, glossaryBlock))
+ return strings.TrimSpace(fmt.Sprintf(genericPromptTemplate, srcLabel, tgtLabel, localePromptRules(tgtLang), glossaryBlock))
+ }
+}
+
+func localePromptRules(tgtLang string) string {
+ switch {
+ case strings.EqualFold(tgtLang, "de"):
+ return "- For German docs, use formal address consistently: “Sie/Ihr/Ihnen”. Avoid informal “du/dein/dir”.\n- Use established technical German; keep “Provider” where it is clearer than “Anbieter”, and avoid awkward mixed compounds."
+ default:
+ return ""
}
}
@@ -135,6 +146,7 @@ Rules:
- Do not remove, reorder, or summarize content.
- Use fluent, idiomatic technical language in the target language; avoid slang or jokes.
- Use neutral documentation tone.
+%s
- Glossary terms are mandatory. When a source term matches a glossary entry, use
the glossary target exactly, including headings, link labels, and short
UI-style labels.
diff --git a/scripts/docs-i18n/prompt_test.go b/scripts/docs-i18n/prompt_test.go
new file mode 100644
index 00000000000..30a8f70c6dd
--- /dev/null
+++ b/scripts/docs-i18n/prompt_test.go
@@ -0,0 +1,22 @@
+package main
+
+import (
+ "strings"
+ "testing"
+)
+
+func TestTranslationPromptAddsGermanStyleRules(t *testing.T) {
+ t.Parallel()
+
+ prompt := translationPrompt("en", "de", nil)
+
+ for _, want := range []string{
+ "Translate from English to German.",
+ "Sie/Ihr/Ihnen",
+ "Avoid informal “du/dein/dir”",
+ } {
+ if !strings.Contains(prompt, want) {
+ t.Fatalf("expected %q in German prompt:\n%s", want, prompt)
+ }
+ }
+}
diff --git a/scripts/docs-i18n/translator.go b/scripts/docs-i18n/translator.go
index 9153390f6e6..a65226cc23b 100644
--- a/scripts/docs-i18n/translator.go
+++ b/scripts/docs-i18n/translator.go
@@ -110,7 +110,7 @@ func (t *CodexTranslator) translateMasked(ctx context.Context, core string) (str
if err != nil {
return "", err
}
- translated := strings.TrimSpace(resText)
+ translated := stripCodexI18nInputWrappers(strings.TrimSpace(resText))
if translated == "" {
return "", errEmptyTranslation
}
@@ -125,13 +125,21 @@ func (t *CodexTranslator) translateRaw(ctx context.Context, core string) (string
if err != nil {
return "", err
}
- translated := strings.TrimSpace(resText)
+ translated := stripCodexI18nInputWrappers(strings.TrimSpace(resText))
if translated == "" {
return "", errEmptyTranslation
}
return translated, nil
}
+func stripCodexI18nInputWrappers(text string) string {
+ replacer := strings.NewReplacer(
+ "", "",
+ "", "",
+ )
+ return strings.TrimSpace(replacer.Replace(text))
+}
+
func (t *CodexTranslator) prompt(ctx context.Context, message string) (string, error) {
if t.runPrompt == nil {
return "", errors.New("codex prompt runner unavailable")
diff --git a/scripts/docs-i18n/translator_test.go b/scripts/docs-i18n/translator_test.go
index ffbff9553b1..307110ce9ed 100644
--- a/scripts/docs-i18n/translator_test.go
+++ b/scripts/docs-i18n/translator_test.go
@@ -119,6 +119,26 @@ func TestCodexTranslatorRetriesTransientFailure(t *testing.T) {
}
}
+func TestCodexTranslatorStripsInputWrapperEcho(t *testing.T) {
+ t.Parallel()
+
+ translator := &CodexTranslator{
+ systemPrompt: "Translate from English to German.",
+ thinking: "high",
+ runPrompt: func(context.Context, codexPromptRequest) (string, error) {
+ return "\nÜbersetzt\n", nil
+ },
+ }
+
+ got, err := translator.TranslateRaw(context.Background(), "Translate me", "en", "de")
+ if err != nil {
+ t.Fatalf("TranslateRaw returned error: %v", err)
+ }
+ if got != "Übersetzt" {
+ t.Fatalf("unexpected translation %q", got)
+ }
+}
+
func TestBuildCodexTranslationPromptIncludesGuardrailsAndInput(t *testing.T) {
prompt := buildCodexTranslationPrompt("System prompt.", "Hello\nworld")
diff --git a/scripts/docs-i18n/util.go b/scripts/docs-i18n/util.go
index 71eba5d15ba..ecc3ae330be 100644
--- a/scripts/docs-i18n/util.go
+++ b/scripts/docs-i18n/util.go
@@ -11,7 +11,7 @@ import (
)
const (
- workflowVersion = 15
+ workflowVersion = 16
docsI18nEngineName = "codex"
envDocsI18nProvider = "OPENCLAW_DOCS_I18N_PROVIDER"
envDocsI18nModel = "OPENCLAW_DOCS_I18N_MODEL"
@@ -101,6 +101,11 @@ func isWhitespace(b byte) bool {
func validateNoTranslationTranscriptArtifacts(source, translated string) error {
sourceLower := strings.ToLower(source)
+ for _, token := range []string{"", ""} {
+ if strings.Contains(strings.ToLower(translated), token) && !strings.Contains(sourceLower, token) {
+ return fmt.Errorf("agent transcript artifact leaked into translation: %q", token)
+ }
+ }
for _, match := range translationTranscriptArtifactRE.FindAllString(translated, -1) {
match = strings.TrimSpace(match)
if match == "" {