fix(docs-i18n): reject leaked placeholder variants

This commit is contained in:
Peter Steinberger
2026-07-12 21:53:37 -04:00
parent 024b7d5aa8
commit 58e8440190
4 changed files with 60 additions and 3 deletions
+19
View File
@@ -78,6 +78,9 @@ func validateDocBodyFencedLiterals(source, translated string) error {
}
sourceStructure := summarizeDocChunkStructure(source)
translatedStructure := summarizeDocChunkStructure(translated)
if !sameI18NProtocolMarkers(source, translated) {
return fmt.Errorf("i18n placeholder mismatch")
}
if !slices.Equal(sourceStructure.listShapes, translatedStructure.listShapes) {
return fmt.Errorf("list structure mismatch: source=%v translated=%v", sourceStructure.listShapes, translatedStructure.listShapes)
}
@@ -236,6 +239,12 @@ func validateDocChunkTranslation(source, translated string) error {
sourceLower := strings.ToLower(source)
translatedLower := strings.ToLower(translated)
for _, token := range docsProtocolTokens {
if token == "__OC_I18N_" {
if !sameI18NProtocolMarkers(source, translated) {
return fmt.Errorf("protocol token leaked: %s", token)
}
continue
}
tokenLower := strings.ToLower(token)
if strings.Contains(sourceLower, tokenLower) {
continue
@@ -278,6 +287,16 @@ func validateDocChunkTranslation(source, translated string) error {
return nil
}
func sameI18NProtocolMarkers(source, translated string) bool {
if !sameStringMultiset(placeholderRe.FindAllString(source, -1), placeholderRe.FindAllString(translated, -1)) {
return false
}
sourceResidual := placeholderRe.ReplaceAllString(source, "")
translatedResidual := placeholderRe.ReplaceAllString(translated, "")
return strings.Count(strings.ToLower(sourceResidual), "__oc_i18n_") ==
strings.Count(strings.ToLower(translatedResidual), "__oc_i18n_")
}
func sameStringMultiset(left, right []string) bool {
if len(left) != len(right) {
return false
+39 -1
View File
@@ -546,6 +546,44 @@ func TestValidateDocChunkTranslationRejectsInventedI18NPlaceholder(t *testing.T)
}
}
func TestValidateDocChunkTranslationRejectsAdditionalI18NPlaceholder(t *testing.T) {
t.Parallel()
source := "```text\n__OC_I18N_900000__\n```\n"
translated := "```text\n__OC_I18N_900000__\n```\n__OC_I18N_900014__\n"
err := validateDocChunkTranslation(source, translated)
if err == nil {
t.Fatal("expected additional i18n placeholder to be rejected")
}
if !strings.Contains(err.Error(), "protocol token leaked: __OC_I18N_") {
t.Fatalf("expected i18n placeholder leakage error, got %v", err)
}
}
func TestValidateDocChunkTranslationRejectsMalformedI18NPlaceholder(t *testing.T) {
t.Parallel()
for _, leaked := range []string{"__oc_i18n_900014__", "__OC_I18N_invalid__"} {
err := validateDocChunkTranslation("Regular paragraph.\n", "Обычный абзац.\n"+leaked+"\n")
if err == nil {
t.Fatalf("expected malformed i18n placeholder %q to be rejected", leaked)
}
}
}
func TestValidateDocBodyFencedLiteralsRejectsRestoredPlaceholderLeak(t *testing.T) {
t.Parallel()
source := "Before.\n\n```ts\nconst value = \"<user-id>\";\n```\n\nAfter.\n"
translated := "До.\n\n__OC_I18N_900014__\n\nПосле.\n"
err := validateDocBodyFencedLiterals(source, translated)
if err == nil {
t.Fatal("expected restored placeholder leak to be rejected")
}
}
func TestValidateDocChunkTranslationRejectsHeadingLoss(t *testing.T) {
t.Parallel()
@@ -2142,7 +2180,7 @@ 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: 16") {
if !strings.Contains(text, "prompt_version: 17") {
t.Fatalf("expected prompt version 15 in output metadata:\n%s", text)
}
}
+1 -1
View File
@@ -12,7 +12,7 @@ import (
const (
workflowVersion = 16
promptVersion = 16
promptVersion = 17
docsI18nEngineName = "codex"
envDocsI18nProvider = "OPENCLAW_DOCS_I18N_PROVIDER"
envDocsI18nModel = "OPENCLAW_DOCS_I18N_MODEL"
+1 -1
View File
@@ -8,7 +8,7 @@ import (
func TestCacheNamespaceIncludesPromptVersion(t *testing.T) {
t.Parallel()
if want := "prompt=16"; !strings.Contains(cacheNamespace(), want) {
if want := "prompt=17"; !strings.Contains(cacheNamespace(), want) {
t.Fatalf("expected cache namespace to contain %q, got %q", want, cacheNamespace())
}
}