mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 15:20:44 +00:00
fix: harden docs i18n prompt echoes
This commit is contained in:
@@ -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`,
|
||||
"<openclaw_docs_i18n_input>\nTranslated\n</openclaw_docs_i18n_input>",
|
||||
`กำลังทำงานกับ reactions to=functions.read commentary  ̄第四色json 皇平台`,
|
||||
`คุณต้องการแผนที่เอกสาร analysis to=final code omitted`,
|
||||
`Potrzebujesz listy funkcji TUI force_parallel: false} code`,
|
||||
|
||||
@@ -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.
|
||||
|
||||
22
scripts/docs-i18n/prompt_test.go
Normal file
22
scripts/docs-i18n/prompt_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(
|
||||
"<openclaw_docs_i18n_input>", "",
|
||||
"</openclaw_docs_i18n_input>", "",
|
||||
)
|
||||
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")
|
||||
|
||||
@@ -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 "<openclaw_docs_i18n_input>\nÜbersetzt\n</openclaw_docs_i18n_input>", 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")
|
||||
|
||||
|
||||
@@ -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{"<openclaw_docs_i18n_input>", "</openclaw_docs_i18n_input>"} {
|
||||
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 == "" {
|
||||
|
||||
Reference in New Issue
Block a user