diff --git a/docs/plugins/message-presentation.md b/docs/plugins/message-presentation.md index fe9876e5980f..5747b0cbdd43 100644 --- a/docs/plugins/message-presentation.md +++ b/docs/plugins/message-presentation.md @@ -494,7 +494,7 @@ When a channel cannot render interactive controls, button and select values fall back to plain text. The fallback behavior preserves usability while keeping opaque callback data private: -- **`command`-typed actions** render as `label: \`command\`` so users can +- **`command`-typed actions** render as `` label: `command` `` so users can copy the command and run it manually in the channel input. - **`callback`-typed actions** and legacy **`value`** fields render as label-only. The opaque callback value is not exposed in fallback text. diff --git a/scripts/docs-i18n/doc_chunked_raw.go b/scripts/docs-i18n/doc_chunked_raw.go index a71fbe5afc15..8db253a70f7e 100644 --- a/scripts/docs-i18n/doc_chunked_raw.go +++ b/scripts/docs-i18n/doc_chunked_raw.go @@ -97,6 +97,9 @@ func validateDocBodyFencedLiterals(source, translated string) error { if !slices.Equal(sourceStructure.listShapes, translatedStructure.listShapes) { return fmt.Errorf("list structure mismatch: source=%v translated=%v", sourceStructure.listShapes, translatedStructure.listShapes) } + if !sameStringMultiset(sourceStructure.inlineCodeSpans, translatedStructure.inlineCodeSpans) { + return fmt.Errorf("inline code mismatch: source=%d translated=%d", len(sourceStructure.inlineCodeSpans), len(translatedStructure.inlineCodeSpans)) + } if !slices.Equal(sourceStructure.fencedPlaceholders, translatedStructure.fencedPlaceholders) { return fmt.Errorf("fenced placeholder mismatch: source=%d translated=%d", len(sourceStructure.fencedPlaceholders), len(translatedStructure.fencedPlaceholders)) } diff --git a/scripts/docs-i18n/doc_mode_test.go b/scripts/docs-i18n/doc_mode_test.go index 7213e19f40d6..ba185630f840 100644 --- a/scripts/docs-i18n/doc_mode_test.go +++ b/scripts/docs-i18n/doc_mode_test.go @@ -744,6 +744,61 @@ func TestValidateDocChunkTranslationAcceptsReorderedInlineCode(t *testing.T) { } } +func TestMaskMarkdownDocSyntaxPreservesCanonicalNestedBackticks(t *testing.T) { + t.Parallel() + + source := strings.Join([]string{ + "- A Windows path can end in a backslash: `C:\\`.", + "- **`command`-typed actions** render as `` label: `command` `` so users can copy it.", + "- **`callback`-typed actions** and legacy **`value`** fields render label-only.", + "", + }, "\n") + state := NewPlaceholderState(source) + placeholders := []string{} + mapping := map[string]string{} + masked := maskMarkdownDocSyntax(source, state.Next, &placeholders, mapping) + + wantLiterals := []string{"`command`", "`` label: `command` ``", "`callback`", "`value`", "`C:\\`"} + for _, literal := range wantLiterals { + if strings.Contains(masked, literal) { + t.Fatalf("expected %q to be masked:\n%s", literal, masked) + } + } + for _, prose := range []string{"typed actions", "so users can copy it", "fields render label-only", "A Windows path can end in a backslash"} { + if !strings.Contains(masked, prose) { + t.Fatalf("expected translatable prose %q to remain visible:\n%s", prose, masked) + } + } + maskedLiterals := []string{} + for _, value := range mapping { + if strings.HasPrefix(value, "`") { + maskedLiterals = append(maskedLiterals, value) + } + } + if !sameStringMultiset(wantLiterals, maskedLiterals) { + t.Fatalf("masked inline literals = %v, want %v", maskedLiterals, wantLiterals) + } + if restored := unmaskMarkdown(masked, placeholders, mapping); restored != source { + t.Fatalf("inline-code round trip changed source:\n%s\nwant:\n%s", restored, source) + } +} + +func TestValidateDocBodyRejectsTranslatedInlineCode(t *testing.T) { + t.Parallel() + + source := "- **`callback`-typed actions** and legacy **`value`** fields render label-only.\n" + translated := "- **`कॉलबैक`-typed actions** and legacy **`मान`** fields render label-only.\n" + err := validateDocBodyFencedLiterals(source, translated) + if err == nil || !strings.Contains(err.Error(), "inline code mismatch") { + t.Fatalf("expected final-document inline-code mismatch, got %v", err) + } + + preserved := "- **`callback`-प्रकार की कार्रवाइयाँ** और पुराने **`value`** फ़ील्ड केवल लेबल दिखाते हैं।\n" + if err := validateDocBodyFencedLiterals(source, preserved); err != nil { + t.Fatalf("expected translated prose with preserved inline code to pass: %v", err) + } +} + func TestValidateDocChunkTranslationRejectsTranslatedMultiBacktickCode(t *testing.T) { t.Parallel() @@ -2329,8 +2384,8 @@ 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: 28") { - t.Fatalf("expected prompt version 28 in output metadata:\n%s", text) + if !strings.Contains(text, "prompt_version: 29") { + t.Fatalf("expected prompt version 29 in output metadata:\n%s", text) } } diff --git a/scripts/docs-i18n/util.go b/scripts/docs-i18n/util.go index 377345f88b4b..dc2e80b009a7 100644 --- a/scripts/docs-i18n/util.go +++ b/scripts/docs-i18n/util.go @@ -12,7 +12,7 @@ import ( const ( workflowVersion = 16 - promptVersion = 28 + promptVersion = 29 docsI18nEngineName = "codex" envDocsI18nProvider = "OPENCLAW_DOCS_I18N_PROVIDER" envDocsI18nModel = "OPENCLAW_DOCS_I18N_MODEL"