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 == "" {