Skip to content

Commit 58e8440

Browse files
committed
fix(docs-i18n): reject leaked placeholder variants
1 parent 024b7d5 commit 58e8440

4 files changed

Lines changed: 60 additions & 3 deletions

File tree

scripts/docs-i18n/doc_chunked_raw.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,9 @@ func validateDocBodyFencedLiterals(source, translated string) error {
7878
}
7979
sourceStructure := summarizeDocChunkStructure(source)
8080
translatedStructure := summarizeDocChunkStructure(translated)
81+
if !sameI18NProtocolMarkers(source, translated) {
82+
return fmt.Errorf("i18n placeholder mismatch")
83+
}
8184
if !slices.Equal(sourceStructure.listShapes, translatedStructure.listShapes) {
8285
return fmt.Errorf("list structure mismatch: source=%v translated=%v", sourceStructure.listShapes, translatedStructure.listShapes)
8386
}
@@ -236,6 +239,12 @@ func validateDocChunkTranslation(source, translated string) error {
236239
sourceLower := strings.ToLower(source)
237240
translatedLower := strings.ToLower(translated)
238241
for _, token := range docsProtocolTokens {
242+
if token == "__OC_I18N_" {
243+
if !sameI18NProtocolMarkers(source, translated) {
244+
return fmt.Errorf("protocol token leaked: %s", token)
245+
}
246+
continue
247+
}
239248
tokenLower := strings.ToLower(token)
240249
if strings.Contains(sourceLower, tokenLower) {
241250
continue
@@ -278,6 +287,16 @@ func validateDocChunkTranslation(source, translated string) error {
278287
return nil
279288
}
280289

290+
func sameI18NProtocolMarkers(source, translated string) bool {
291+
if !sameStringMultiset(placeholderRe.FindAllString(source, -1), placeholderRe.FindAllString(translated, -1)) {
292+
return false
293+
}
294+
sourceResidual := placeholderRe.ReplaceAllString(source, "")
295+
translatedResidual := placeholderRe.ReplaceAllString(translated, "")
296+
return strings.Count(strings.ToLower(sourceResidual), "__oc_i18n_") ==
297+
strings.Count(strings.ToLower(translatedResidual), "__oc_i18n_")
298+
}
299+
281300
func sameStringMultiset(left, right []string) bool {
282301
if len(left) != len(right) {
283302
return false

scripts/docs-i18n/doc_mode_test.go

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,6 +546,44 @@ func TestValidateDocChunkTranslationRejectsInventedI18NPlaceholder(t *testing.T)
546546
}
547547
}
548548

549+
func TestValidateDocChunkTranslationRejectsAdditionalI18NPlaceholder(t *testing.T) {
550+
t.Parallel()
551+
552+
source := "```text\n__OC_I18N_900000__\n```\n"
553+
translated := "```text\n__OC_I18N_900000__\n```\n__OC_I18N_900014__\n"
554+
555+
err := validateDocChunkTranslation(source, translated)
556+
if err == nil {
557+
t.Fatal("expected additional i18n placeholder to be rejected")
558+
}
559+
if !strings.Contains(err.Error(), "protocol token leaked: __OC_I18N_") {
560+
t.Fatalf("expected i18n placeholder leakage error, got %v", err)
561+
}
562+
}
563+
564+
func TestValidateDocChunkTranslationRejectsMalformedI18NPlaceholder(t *testing.T) {
565+
t.Parallel()
566+
567+
for _, leaked := range []string{"__oc_i18n_900014__", "__OC_I18N_invalid__"} {
568+
err := validateDocChunkTranslation("Regular paragraph.\n", "Обычный абзац.\n"+leaked+"\n")
569+
if err == nil {
570+
t.Fatalf("expected malformed i18n placeholder %q to be rejected", leaked)
571+
}
572+
}
573+
}
574+
575+
func TestValidateDocBodyFencedLiteralsRejectsRestoredPlaceholderLeak(t *testing.T) {
576+
t.Parallel()
577+
578+
source := "Before.\n\n```ts\nconst value = \"<user-id>\";\n```\n\nAfter.\n"
579+
translated := "До.\n\n__OC_I18N_900014__\n\nПосле.\n"
580+
581+
err := validateDocBodyFencedLiterals(source, translated)
582+
if err == nil {
583+
t.Fatal("expected restored placeholder leak to be rejected")
584+
}
585+
}
586+
549587
func TestValidateDocChunkTranslationRejectsHeadingLoss(t *testing.T) {
550588
t.Parallel()
551589

@@ -2142,7 +2180,7 @@ func TestProcessFileDocUsesFieldLevelFrontmatterTranslation(t *testing.T) {
21422180
if !strings.Contains(text, "在 Fly.io 上部署 OpenClaw") {
21432181
t.Fatalf("expected translated read_when entry in output:\n%s", text)
21442182
}
2145-
if !strings.Contains(text, "prompt_version: 16") {
2183+
if !strings.Contains(text, "prompt_version: 17") {
21462184
t.Fatalf("expected prompt version 15 in output metadata:\n%s", text)
21472185
}
21482186
}

scripts/docs-i18n/util.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212

1313
const (
1414
workflowVersion = 16
15-
promptVersion = 16
15+
promptVersion = 17
1616
docsI18nEngineName = "codex"
1717
envDocsI18nProvider = "OPENCLAW_DOCS_I18N_PROVIDER"
1818
envDocsI18nModel = "OPENCLAW_DOCS_I18N_MODEL"

scripts/docs-i18n/util_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import (
88
func TestCacheNamespaceIncludesPromptVersion(t *testing.T) {
99
t.Parallel()
1010

11-
if want := "prompt=16"; !strings.Contains(cacheNamespace(), want) {
11+
if want := "prompt=17"; !strings.Contains(cacheNamespace(), want) {
1212
t.Fatalf("expected cache namespace to contain %q, got %q", want, cacheNamespace())
1313
}
1414
}

0 commit comments

Comments
 (0)