diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5ff786059ef..b5ec7260679 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
- Agents/local models: add `agents.defaults.localModelMode: "lean"` to drop heavyweight default tools like `browser`, `cron`, and `message`, reducing prompt size for weaker local-model setups without changing the normal path. Thanks @ImLukeF.
- QA/Matrix: split Matrix live QA into a source-linked `qa-matrix` runner and keep repo-private `qa-*` surfaces out of packaged and published builds. (#66723) Thanks @gumadeiras.
- Control UI/Overview: add a Model Auth status card showing OAuth token health and provider rate-limit pressure at a glance, with attention callouts when OAuth tokens are expiring or expired. Backed by a new `models.authStatus` gateway method that strips credentials and caches for 60s. (#66211) Thanks @omarshahine.
+- docs-i18n: add behavior baseline fixtures (#64073). Thanks @hxy91819
### Fixes
diff --git a/scripts/docs-i18n/behavior_baseline_test.go b/scripts/docs-i18n/behavior_baseline_test.go
new file mode 100644
index 00000000000..262574db9d6
--- /dev/null
+++ b/scripts/docs-i18n/behavior_baseline_test.go
@@ -0,0 +1,218 @@
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "os"
+ "path/filepath"
+ "strings"
+ "testing"
+)
+
+type behaviorReplacePair struct {
+ From string `json:"from"`
+ To string `json:"to"`
+}
+
+type behaviorRule struct {
+ Method string `json:"method"`
+ MatchAll []string `json:"match_all"`
+ ResponseFile string `json:"response_file,omitempty"`
+ ReplacePairs []behaviorReplacePair `json:"replace_pairs,omitempty"`
+}
+
+type behaviorFixture struct {
+ Name string `json:"name"`
+ Mode string `json:"mode"`
+ RelPath string `json:"rel_path"`
+ SourceFile string `json:"source_file"`
+ ExpectedFile string `json:"expected_file,omitempty"`
+ ExpectedErrorContains string `json:"expected_error_contains,omitempty"`
+ ExpectedOutputContains []string `json:"expected_output_contains,omitempty"`
+ ExpectedOutputNotContains []string `json:"expected_output_not_contains,omitempty"`
+ Rules []behaviorRule `json:"rules"`
+}
+
+type behaviorFixtureTranslator struct {
+ t *testing.T
+ dir string
+ rules []behaviorRule
+}
+
+func (tr *behaviorFixtureTranslator) Translate(_ context.Context, text, _, _ string) (string, error) {
+ return tr.run("masked", text), nil
+}
+
+func (tr *behaviorFixtureTranslator) TranslateRaw(_ context.Context, text, _, _ string) (string, error) {
+ return tr.run("raw", text), nil
+}
+
+func (tr *behaviorFixtureTranslator) Close() {}
+
+func (tr *behaviorFixtureTranslator) run(method, text string) string {
+ tr.t.Helper()
+ for _, rule := range tr.rules {
+ if rule.Method != method {
+ continue
+ }
+ if !matchesAll(text, rule.MatchAll) {
+ continue
+ }
+ switch {
+ case rule.ResponseFile != "":
+ return readFixtureTextInDir(tr.t, tr.dir, rule.ResponseFile)
+ case len(rule.ReplacePairs) > 0:
+ out := text
+ for _, pair := range rule.ReplacePairs {
+ out = strings.ReplaceAll(out, pair.From, pair.To)
+ }
+ return out
+ default:
+ return text
+ }
+ }
+ return text
+}
+
+func matchesAll(text string, fragments []string) bool {
+ for _, fragment := range fragments {
+ if !strings.Contains(text, fragment) {
+ return false
+ }
+ }
+ return true
+}
+
+func TestDocsI18nBehaviorBaselines(t *testing.T) {
+ t.Parallel()
+
+ root := filepath.Join("testdata", "behavior")
+ entries, err := os.ReadDir(root)
+ if err != nil {
+ t.Fatalf("ReadDir(%q): %v", root, err)
+ }
+
+ found := false
+ for _, entry := range entries {
+ if !entry.IsDir() {
+ continue
+ }
+ found = true
+ dir := filepath.Join(root, entry.Name())
+ fixture := loadBehaviorFixture(t, dir)
+ name := fixture.Name
+ if name == "" {
+ name = entry.Name()
+ }
+ t.Run(name, func(t *testing.T) {
+ t.Parallel()
+ runBehaviorFixture(t, dir, fixture)
+ })
+ }
+
+ if !found {
+ t.Fatalf("no behavior fixtures found under %s", root)
+ }
+}
+
+func loadBehaviorFixture(t *testing.T, dir string) behaviorFixture {
+ t.Helper()
+ data, err := os.ReadFile(filepath.Join(dir, "case.json"))
+ if err != nil {
+ t.Fatalf("ReadFile(case.json): %v", err)
+ }
+ var fixture behaviorFixture
+ if err := json.Unmarshal(data, &fixture); err != nil {
+ t.Fatalf("Unmarshal(case.json): %v", err)
+ }
+ return fixture
+}
+
+func runBehaviorFixture(t *testing.T, dir string, fixture behaviorFixture) {
+ t.Helper()
+
+ source := readFixtureTextInDir(t, dir, fixture.SourceFile)
+ translator := &behaviorFixtureTranslator{
+ t: t,
+ dir: dir,
+ rules: fixture.Rules,
+ }
+
+ var (
+ got string
+ err error
+ )
+
+ switch fixture.Mode {
+ case "doc_body_chunked":
+ got, err = translateDocBodyChunked(context.Background(), translator, fixture.RelPath, source, "en", "zh-CN")
+ case "frontmatter_scalar":
+ got, err = translateSnippet(
+ context.Background(),
+ translator,
+ &TranslationMemory{entries: map[string]TMEntry{}},
+ fixture.RelPath+":frontmatter:title",
+ source,
+ "en",
+ "zh-CN",
+ )
+ default:
+ t.Fatalf("unsupported fixture mode %q", fixture.Mode)
+ }
+
+ if fixture.ExpectedErrorContains != "" {
+ if err == nil {
+ t.Fatalf("expected error containing %q, got nil", fixture.ExpectedErrorContains)
+ }
+ if !strings.Contains(err.Error(), fixture.ExpectedErrorContains) {
+ t.Fatalf("expected error containing %q, got %v", fixture.ExpectedErrorContains, err)
+ }
+ return
+ }
+ if err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ if fixture.ExpectedFile != "" {
+ want := readFixtureTextInDir(t, dir, fixture.ExpectedFile)
+ if normalizeBehaviorText(got) != normalizeBehaviorText(want) {
+ t.Fatalf("unexpected output\nwant:\n%s\n\ngot:\n%s", want, got)
+ }
+ }
+
+ for _, fragment := range fixture.ExpectedOutputContains {
+ if !strings.Contains(got, fragment) {
+ t.Fatalf("expected output to contain %q\noutput:\n%s", fragment, got)
+ }
+ }
+ for _, fragment := range fixture.ExpectedOutputNotContains {
+ if strings.Contains(got, fragment) {
+ t.Fatalf("expected output to exclude %q\noutput:\n%s", fragment, got)
+ }
+ }
+}
+
+func readFixtureText(t *testing.T, path string) string {
+ t.Helper()
+ data, err := os.ReadFile(path)
+ if err != nil {
+ t.Fatalf("ReadFile(%q): %v", path, err)
+ }
+ return string(data)
+}
+
+func readFixtureTextInDir(t *testing.T, dir, name string) string {
+ t.Helper()
+ if filepath.IsAbs(name) {
+ t.Fatalf("absolute fixture paths are not allowed: %q", name)
+ }
+ clean := filepath.Clean(name)
+ if clean == ".." || strings.HasPrefix(clean, ".."+string(filepath.Separator)) {
+ t.Fatalf("fixture path escapes dir: %q", name)
+ }
+ return readFixtureText(t, filepath.Join(dir, clean))
+}
+
+func normalizeBehaviorText(value string) string {
+ return strings.TrimSpace(strings.ReplaceAll(value, "\r\n", "\n"))
+}
diff --git a/scripts/docs-i18n/testdata/behavior/fenced-singleton-retry/case.json b/scripts/docs-i18n/testdata/behavior/fenced-singleton-retry/case.json
new file mode 100644
index 00000000000..b0a399ba55c
--- /dev/null
+++ b/scripts/docs-i18n/testdata/behavior/fenced-singleton-retry/case.json
@@ -0,0 +1,24 @@
+{
+ "name": "fenced singleton retries after malformed raw output",
+ "mode": "doc_body_chunked",
+ "rel_path": "gateway/configuration-reference.md",
+ "source_file": "source.txt",
+ "expected_output_contains": [
+ "Translated line 01",
+ "Translated line 02",
+ "Translated line 03",
+ "Translated line 04"
+ ],
+ "expected_output_not_contains": ["Line 01", "Line 02", "Line 03", "Line 04"],
+ "rules": [
+ {
+ "method": "raw",
+ "match_all": ["Line 01", "Line 04"],
+ "response_file": "raw-malformed.txt"
+ },
+ {
+ "method": "raw",
+ "replace_pairs": [{ "from": "Line ", "to": "Translated line " }]
+ }
+ ]
+}
diff --git a/scripts/docs-i18n/testdata/behavior/fenced-singleton-retry/raw-malformed.txt b/scripts/docs-i18n/testdata/behavior/fenced-singleton-retry/raw-malformed.txt
new file mode 100644
index 00000000000..dd40e712a74
--- /dev/null
+++ b/scripts/docs-i18n/testdata/behavior/fenced-singleton-retry/raw-malformed.txt
@@ -0,0 +1,6 @@
+```md
+Line 01
+Line 02
+Line 03
+Line 04
+
diff --git a/scripts/docs-i18n/testdata/behavior/fenced-singleton-retry/source.txt b/scripts/docs-i18n/testdata/behavior/fenced-singleton-retry/source.txt
new file mode 100644
index 00000000000..27990717eac
--- /dev/null
+++ b/scripts/docs-i18n/testdata/behavior/fenced-singleton-retry/source.txt
@@ -0,0 +1,7 @@
+```md
+Line 01
+Line 02
+Line 03
+Line 04
+```
+
diff --git a/scripts/docs-i18n/testdata/behavior/frontmatter-fallback/case.json b/scripts/docs-i18n/testdata/behavior/frontmatter-fallback/case.json
new file mode 100644
index 00000000000..e8140a84c78
--- /dev/null
+++ b/scripts/docs-i18n/testdata/behavior/frontmatter-fallback/case.json
@@ -0,0 +1,14 @@
+{
+ "name": "frontmatter scalar falls back instead of keeping tagged wrapper",
+ "mode": "frontmatter_scalar",
+ "rel_path": "install/fly.md",
+ "source_file": "source.txt",
+ "expected_file": "expected.txt",
+ "rules": [
+ {
+ "method": "masked",
+ "match_all": ["Deploying OpenClaw on Fly.io"],
+ "response_file": "masked-tagged-wrapper.txt"
+ }
+ ]
+}
diff --git a/scripts/docs-i18n/testdata/behavior/frontmatter-fallback/expected.txt b/scripts/docs-i18n/testdata/behavior/frontmatter-fallback/expected.txt
new file mode 100644
index 00000000000..5b65021bed5
--- /dev/null
+++ b/scripts/docs-i18n/testdata/behavior/frontmatter-fallback/expected.txt
@@ -0,0 +1 @@
+Deploying OpenClaw on Fly.io
diff --git a/scripts/docs-i18n/testdata/behavior/frontmatter-fallback/masked-tagged-wrapper.txt b/scripts/docs-i18n/testdata/behavior/frontmatter-fallback/masked-tagged-wrapper.txt
new file mode 100644
index 00000000000..664850c1904
--- /dev/null
+++ b/scripts/docs-i18n/testdata/behavior/frontmatter-fallback/masked-tagged-wrapper.txt
@@ -0,0 +1,7 @@
+