diff --git a/docs/docs.json b/docs/docs.json index 953372c4525..f1cbbfb3ddc 100644 --- a/docs/docs.json +++ b/docs/docs.json @@ -124,6 +124,14 @@ "source": "/context", "destination": "/concepts/context" }, + { + "source": "/zh-CN", + "destination": "/zh-CN/index" + }, + { + "source": "/zh-CN/", + "destination": "/zh-CN/index" + }, { "source": "/compaction", "destination": "/concepts/compaction" diff --git a/docs/plugins/community.md b/docs/plugins/community.md index ad7e944577f..8eaa047c26c 100644 --- a/docs/plugins/community.md +++ b/docs/plugins/community.md @@ -13,6 +13,10 @@ channels, tools, providers, or other capabilities. They are built and maintained by the community, published on [ClawHub](/tools/clawhub) or npm, and installable with a single command. +ClawHub is the canonical discovery surface for community plugins. Do not open +docs-only PRs just to add your plugin here for discoverability; publish it on +ClawHub instead. + ```bash openclaw plugins install ``` @@ -116,14 +120,13 @@ We welcome community plugins that are useful, documented, and safe to operate. - - Add your plugin to this page with: + + You do not need a docs PR just to make your plugin discoverable. Publish it + on ClawHub instead. - - Plugin name - - npm package name - - GitHub repository URL - - One-line description - - Install command + Open a docs PR only when OpenClaw's source docs need an actual content + change, such as correcting install guidance or adding cross-repo + documentation that belongs in the main docs set. diff --git a/docs/start/getting-started.md b/docs/start/getting-started.md index 7e25f9d1503..321de2179af 100644 --- a/docs/start/getting-started.md +++ b/docs/start/getting-started.md @@ -88,6 +88,38 @@ Need to install Node? See [Node setup](/install/node). + + If you maintain a localized or customized dashboard build, point + `gateway.controlUi.root` to a directory that contains your built static + assets and `index.html`. + +```bash +mkdir -p "$HOME/.openclaw/control-ui-custom" +# Copy your built static files into that directory. +``` + +Then set: + +```json +{ + "gateway": { + "controlUi": { + "enabled": true, + "root": "$HOME/.openclaw/control-ui-custom" + } + } +} +``` + +Restart the gateway and reopen the dashboard: + +```bash +openclaw gateway restart +openclaw dashboard +``` + + + ## What to do next diff --git a/scripts/docs-i18n/doc_mode.go b/scripts/docs-i18n/doc_mode.go index ce09dfa4773..ad71bdb2b08 100644 --- a/scripts/docs-i18n/doc_mode.go +++ b/scripts/docs-i18n/doc_mode.go @@ -17,7 +17,7 @@ const ( bodyTagEnd = "" ) -func processFileDoc(ctx context.Context, translator *PiTranslator, docsRoot, filePath, srcLang, tgtLang string, overwrite bool) (bool, error) { +func processFileDoc(ctx context.Context, translator *PiTranslator, docsRoot, filePath, srcLang, tgtLang string, overwrite bool, routes *routeIndex) (bool, error) { absPath, relPath, err := resolveDocsPath(docsRoot, filePath) if err != nil { return false, err @@ -65,6 +65,7 @@ func processFileDoc(ctx context.Context, translator *PiTranslator, docsRoot, fil if err := applyFrontmatterTranslations(frontData, markers, translatedFront); err != nil { return false, fmt.Errorf("frontmatter translation failed for %s: %w", relPath, err) } + translatedBody = routes.localizeBodyLinks(translatedBody) updatedFront, err := encodeFrontMatter(frontData, relPath, content) if err != nil { diff --git a/scripts/docs-i18n/localized_links.go b/scripts/docs-i18n/localized_links.go new file mode 100644 index 00000000000..76037009dcd --- /dev/null +++ b/scripts/docs-i18n/localized_links.go @@ -0,0 +1,398 @@ +package main + +import ( + "encoding/json" + "os" + "path/filepath" + "regexp" + "strings" + + "gopkg.in/yaml.v3" +) + +type routeIndex struct { + targetLang string + redirects map[string]string + sourceRoutes map[string]struct{} + localizedRoutes map[string]struct{} + localePrefixes map[string]struct{} +} + +type docsConfig struct { + Redirects []docsRedirect `json:"redirects"` +} + +type docsRedirect struct { + Source string `json:"source"` + Destination string `json:"destination"` +} + +var ( + localeDirRe = regexp.MustCompile(`^[a-z]{2,3}(?:-[A-Za-z0-9]{2,8})?$`) + fencedBacktickCodeBlock = regexp.MustCompile("(?ms)(^|\\n)[ \\t]*```[^\\n]*\\n.*?\\n[ \\t]*```[ \\t]*(?:\\n|$)") + fencedTildeCodeBlock = regexp.MustCompile("(?ms)(^|\\n)[ \\t]*~~~[^\\n]*\\n.*?\\n[ \\t]*~~~[ \\t]*(?:\\n|$)") + markdownLinkTargetRe = regexp.MustCompile(`!?\[[^\]]*\]\(([^)]+)\)`) + hrefDoubleQuotedValueRe = regexp.MustCompile(`\bhref\s*=\s*"([^"]*)"`) + hrefSingleQuotedValueRe = regexp.MustCompile(`\bhref\s*=\s*'([^']*)'`) +) + +func loadRouteIndex(docsRoot, targetLang string) (*routeIndex, error) { + index := &routeIndex{ + targetLang: strings.TrimSpace(targetLang), + redirects: map[string]string{}, + sourceRoutes: map[string]struct{}{}, + localizedRoutes: map[string]struct{}{}, + localePrefixes: map[string]struct{}{}, + } + + if err := index.loadRedirects(filepath.Join(docsRoot, "docs.json")); err != nil { + return nil, err + } + if err := index.loadRoutes(docsRoot); err != nil { + return nil, err + } + + return index, nil +} + +func (ri *routeIndex) loadRedirects(configPath string) error { + data, err := os.ReadFile(configPath) + if err != nil { + return err + } + var config docsConfig + if err := json.Unmarshal(data, &config); err != nil { + return err + } + for _, item := range config.Redirects { + source := normalizeRoute(item.Source) + destination := normalizeRoute(item.Destination) + if source == "" || destination == "" { + continue + } + ri.redirects[source] = destination + } + return nil +} + +func (ri *routeIndex) loadRoutes(docsRoot string) error { + localePrefixes, err := discoverLocalePrefixes(docsRoot) + if err != nil { + return err + } + if ri.targetLang != "" { + localePrefixes[ri.targetLang] = struct{}{} + } + ri.localePrefixes = localePrefixes + + return filepath.WalkDir(docsRoot, func(path string, entry os.DirEntry, walkErr error) error { + if walkErr != nil { + return walkErr + } + if entry.IsDir() { + return nil + } + if !isMarkdownFile(path) { + return nil + } + + relPath, err := filepath.Rel(docsRoot, path) + if err != nil { + return err + } + relPath = normalizeSlashes(relPath) + firstSegment := firstPathSegment(relPath) + + content, err := os.ReadFile(path) + if err != nil { + return err + } + permalinks := extractPermalinks(content) + + switch { + case firstSegment == ri.targetLang: + trimmedRel := strings.TrimPrefix(relPath, firstSegment+"/") + addRouteCandidates(ri.localizedRoutes, trimmedRel, permalinks) + case ri.isLocalePrefix(firstSegment): + return nil + default: + addRouteCandidates(ri.sourceRoutes, relPath, permalinks) + } + return nil + }) +} + +func discoverLocalePrefixes(docsRoot string) (map[string]struct{}, error) { + entries, err := os.ReadDir(docsRoot) + if err != nil { + return nil, err + } + locales := map[string]struct{}{} + for _, entry := range entries { + if !entry.IsDir() { + continue + } + name := entry.Name() + if !localeDirRe.MatchString(name) { + continue + } + locales[name] = struct{}{} + } + return locales, nil +} + +func isMarkdownFile(path string) bool { + return strings.HasSuffix(path, ".md") || strings.HasSuffix(path, ".mdx") +} + +func normalizeSlashes(path string) string { + return strings.ReplaceAll(path, "\\", "/") +} + +func firstPathSegment(relPath string) string { + if relPath == "" { + return "" + } + parts := strings.SplitN(relPath, "/", 2) + return parts[0] +} + +func addRouteCandidates(routes map[string]struct{}, relPath string, permalinks []string) { + base := strings.TrimSuffix(strings.TrimSuffix(relPath, ".md"), ".mdx") + if base != relPath { + addRoute(routes, normalizeRoute(base)) + switch { + case base == "index": + addRoute(routes, "/") + case strings.HasSuffix(base, "/index"): + addRoute(routes, normalizeRoute(strings.TrimSuffix(base, "/index"))) + } + } + + for _, permalink := range permalinks { + addRoute(routes, normalizeRoute(permalink)) + } +} + +func addRoute(routes map[string]struct{}, route string) { + if route == "" { + return + } + routes[route] = struct{}{} +} + +func extractPermalinks(content []byte) []string { + frontMatter, _ := splitFrontMatter(string(content)) + if strings.TrimSpace(frontMatter) == "" { + return nil + } + + data := map[string]any{} + if err := yaml.Unmarshal([]byte(frontMatter), &data); err != nil { + return nil + } + + raw, ok := data["permalink"].(string) + if !ok { + return nil + } + permalink := strings.TrimSpace(raw) + if permalink == "" { + return nil + } + return []string{permalink} +} + +func normalizeRoute(path string) string { + trimmed := strings.TrimSpace(path) + if trimmed == "" { + return "" + } + stripped := strings.Trim(trimmed, "/") + if stripped == "" { + return "/" + } + return "/" + stripped +} + +func (ri *routeIndex) localizeBodyLinks(body string) string { + if ri == nil || ri.targetLang == "" || strings.EqualFold(ri.targetLang, "en") { + return body + } + + state := NewPlaceholderState(body) + placeholders := make([]string, 0, 8) + mapping := map[string]string{} + masked := maskMatches(body, fencedBacktickCodeBlock, state.Next, &placeholders, mapping) + masked = maskMatches(masked, fencedTildeCodeBlock, state.Next, &placeholders, mapping) + masked = maskMatches(masked, inlineCodeRe, state.Next, &placeholders, mapping) + + masked = rewriteMarkdownLinkTargets(masked, ri) + masked = rewriteHrefTargets(masked, ri) + + return unmaskMarkdown(masked, placeholders, mapping) +} + +func rewriteMarkdownLinkTargets(text string, ri *routeIndex) string { + matches := markdownLinkTargetRe.FindAllStringSubmatchIndex(text, -1) + if len(matches) == 0 { + return text + } + + var out strings.Builder + pos := 0 + for _, span := range matches { + fullStart, targetStart, targetEnd := span[0], span[2], span[3] + if fullStart < pos { + continue + } + + out.WriteString(text[pos:targetStart]) + target := text[targetStart:targetEnd] + if text[fullStart] == '!' { + out.WriteString(target) + } else { + out.WriteString(ri.localizeURL(target)) + } + pos = targetEnd + } + out.WriteString(text[pos:]) + return out.String() +} + +func rewriteHrefTargets(text string, ri *routeIndex) string { + text = rewriteCapturedTargets(text, hrefDoubleQuotedValueRe, 2, ri) + text = rewriteCapturedTargets(text, hrefSingleQuotedValueRe, 2, ri) + return text +} + +func rewriteCapturedTargets(text string, re *regexp.Regexp, groupIndex int, ri *routeIndex) string { + matches := re.FindAllStringSubmatchIndex(text, -1) + if len(matches) == 0 { + return text + } + + var out strings.Builder + pos := 0 + for _, span := range matches { + start, end := span[groupIndex], span[groupIndex+1] + if start < pos || start < 0 || end < 0 { + continue + } + out.WriteString(text[pos:start]) + out.WriteString(ri.localizeURL(text[start:end])) + pos = end + } + out.WriteString(text[pos:]) + return out.String() +} + +func (ri *routeIndex) localizeURL(raw string) string { + trimmed := strings.TrimSpace(raw) + if trimmed == "" { + return raw + } + if strings.HasPrefix(trimmed, "#") || strings.HasPrefix(trimmed, "//") { + return raw + } + if hasURLScheme(trimmed) { + return raw + } + + pathPart, suffix := splitURLSuffix(trimmed) + if !strings.HasPrefix(pathPart, "/") { + return raw + } + + normalized := normalizeRoute(pathPart) + if ri.routeHasLocalePrefix(normalized) { + return raw + } + + canonical, ok := ri.resolveRoute(normalized) + if !ok { + return raw + } + if _, ok := ri.localizedRoutes[canonical]; !ok { + return raw + } + + return prefixLocaleRoute(ri.targetLang, canonical) + suffix +} + +func hasURLScheme(raw string) bool { + switch { + case strings.HasPrefix(raw, "http://"), strings.HasPrefix(raw, "https://"): + return true + case strings.HasPrefix(raw, "mailto:"), strings.HasPrefix(raw, "tel:"): + return true + case strings.HasPrefix(raw, "data:"), strings.HasPrefix(raw, "javascript:"): + return true + default: + return false + } +} + +func splitURLSuffix(raw string) (string, string) { + index := strings.IndexAny(raw, "?#") + if index == -1 { + return raw, "" + } + return raw[:index], raw[index:] +} + +func prefixLocaleRoute(lang, route string) string { + if route == "/" { + return "/" + lang + } + return "/" + lang + route +} + +func (ri *routeIndex) routeHasLocalePrefix(route string) bool { + if route == "/" { + return false + } + firstSegment := strings.TrimPrefix(route, "/") + firstSegment = strings.SplitN(firstSegment, "/", 2)[0] + return ri.isLocalePrefix(firstSegment) +} + +func (ri *routeIndex) isLocalePrefix(segment string) bool { + if segment == "" { + return false + } + _, ok := ri.localePrefixes[segment] + return ok +} + +func (ri *routeIndex) resolveRoute(route string) (string, bool) { + current := normalizeRoute(route) + if current == "" { + return "", false + } + + seen := map[string]struct{}{current: {}} + for { + next, ok := ri.redirects[current] + if !ok { + break + } + current = next + if _, ok := seen[current]; ok { + return "", false + } + seen[current] = struct{}{} + } + + if current == "/" { + _, ok := ri.localizedRoutes[current] + return current, ok + } + if _, ok := ri.sourceRoutes[current]; ok { + return current, true + } + if _, ok := ri.localizedRoutes[current]; ok { + return current, true + } + return "", false +} diff --git a/scripts/docs-i18n/localized_links_test.go b/scripts/docs-i18n/localized_links_test.go new file mode 100644 index 00000000000..0e50d934d20 --- /dev/null +++ b/scripts/docs-i18n/localized_links_test.go @@ -0,0 +1,172 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestLocalizeBodyLinks(t *testing.T) { + docsRoot := setupDocsTree(t) + routes, err := loadRouteIndex(docsRoot, "zh-CN") + if err != nil { + t.Fatalf("loadRouteIndex failed: %v", err) + } + + tests := []struct { + name string + input string + want string + }{ + { + name: "markdown link", + input: `See [Config](/gateway/configuration).`, + want: `See [Config](/zh-CN/gateway/configuration).`, + }, + { + name: "href attribute", + input: ``, + want: ``, + }, + { + name: "redirect source resolves to canonical localized page", + input: `See [Sandbox](/sandboxing).`, + want: `See [Sandbox](/zh-CN/gateway/sandboxing).`, + }, + { + name: "fragment is preserved", + input: `See [Hooks](/gateway/configuration#hooks).`, + want: `See [Hooks](/zh-CN/gateway/configuration#hooks).`, + }, + { + name: "images stay unchanged", + input: `![Diagram](/images/diagram.svg)`, + want: `![Diagram](/images/diagram.svg)`, + }, + { + name: "already localized stays unchanged", + input: `See [Config](/zh-CN/gateway/configuration).`, + want: `See [Config](/zh-CN/gateway/configuration).`, + }, + { + name: "missing localized page stays unchanged", + input: `See [FAQ](/help/faq).`, + want: `See [FAQ](/help/faq).`, + }, + { + name: "permalink route localizes", + input: `See [Formal verification](/security/formal-verification).`, + want: `See [Formal verification](/zh-CN/security/formal-verification).`, + }, + { + name: "inline code stays unchanged", + input: "Use `[Config](/gateway/configuration)` in examples.\n\n" + + "See [Config](/gateway/configuration).", + want: "Use `[Config](/gateway/configuration)` in examples.\n\n" + + "See [Config](/zh-CN/gateway/configuration).", + }, + { + name: "fenced code block stays unchanged", + input: "```md\n[Config](/gateway/configuration)\n```\n\n" + + "See [Config](/gateway/configuration).", + want: "```md\n[Config](/gateway/configuration)\n```\n\n" + + "See [Config](/zh-CN/gateway/configuration).", + }, + { + name: "inline code does not swallow later paragraphs", + input: strings.Join([]string{ + "Use `channels.matrix.accounts` and `name`.", + "", + "See [Config](/gateway/configuration).", + "", + "Then review [Troubleshooting](/channels/troubleshooting).", + }, "\n"), + want: strings.Join([]string{ + "Use `channels.matrix.accounts` and `name`.", + "", + "See [Config](/zh-CN/gateway/configuration).", + "", + "Then review [Troubleshooting](/zh-CN/channels/troubleshooting).", + }, "\n"), + }, + { + name: "indented fenced code block does not swallow later paragraphs", + input: strings.Join([]string{ + "1. Setup:", + "", + " ```bash", + " echo hi", + " ```", + "", + "Use `channels.matrix.accounts` and `name`.", + "", + "For triage: [/channels/troubleshooting](/channels/troubleshooting).", + "See [Config](/gateway/configuration).", + }, "\n"), + want: strings.Join([]string{ + "1. Setup:", + "", + " ```bash", + " echo hi", + " ```", + "", + "Use `channels.matrix.accounts` and `name`.", + "", + "For triage: [/channels/troubleshooting](/zh-CN/channels/troubleshooting).", + "See [Config](/zh-CN/gateway/configuration).", + }, "\n"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := routes.localizeBodyLinks(tt.input) + if got != tt.want { + t.Fatalf("unexpected rewrite\nwant: %q\ngot: %q", tt.want, got) + } + }) + } +} + +func setupDocsTree(t *testing.T) string { + t.Helper() + + root := t.TempDir() + writeFile(t, filepath.Join(root, "docs.json"), `{ + "redirects": [ + { "source": "/sandboxing", "destination": "/gateway/sandboxing" } + ] +}`) + + files := map[string]string{ + "index.md": "# Home\n", + "channels/troubleshooting.md": "# Troubleshooting\n", + "gateway/configuration.md": "# Config\n", + "gateway/sandboxing.md": "# Sandboxing\n", + "security/formal-verification.md": "---\npermalink: /security/formal-verification/\n---\n\n# Formal verification\n", + "help/faq.md": "# FAQ\n", + "zh-CN/index.md": "# Home\n", + "zh-CN/channels/troubleshooting.md": "# Troubleshooting\n", + "zh-CN/gateway/configuration.md": "# Config\n", + "zh-CN/gateway/sandboxing.md": "# Sandboxing\n", + "zh-CN/security/formal-verification.md": "---\npermalink: /security/formal-verification/\n---\n\n# Formal verification\n", + "ja-JP/index.md": "# Home\n", + } + + for relPath, content := range files { + writeFile(t, filepath.Join(root, relPath), content) + } + + return root +} + +func writeFile(t *testing.T, path string, content string) { + t.Helper() + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatalf("mkdir failed for %s: %v", path, err) + } + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("write failed for %s: %v", path, err) + } +} diff --git a/scripts/docs-i18n/main.go b/scripts/docs-i18n/main.go index 85a61039b12..91b60e511a4 100644 --- a/scripts/docs-i18n/main.go +++ b/scripts/docs-i18n/main.go @@ -58,6 +58,11 @@ func main() { fatal(err) } + routes, err := loadRouteIndex(resolvedDocsRoot, *targetLang) + if err != nil { + fatal(err) + } + translator, err := NewPiTranslator(*sourceLang, *targetLang, glossary, *thinking) if err != nil { fatal(err) @@ -100,14 +105,14 @@ func main() { switch *mode { case "doc": if *parallel > 1 { - proc, skip, err := runDocParallel(context.Background(), ordered, resolvedDocsRoot, *sourceLang, *targetLang, *overwrite, *parallel, glossary, *thinking) + proc, skip, err := runDocParallel(context.Background(), ordered, resolvedDocsRoot, *sourceLang, *targetLang, *overwrite, *parallel, glossary, *thinking, routes) if err != nil { fatal(err) } processed += proc skipped += skip } else { - proc, skip, err := runDocSequential(context.Background(), ordered, translator, resolvedDocsRoot, *sourceLang, *targetLang, *overwrite) + proc, skip, err := runDocSequential(context.Background(), ordered, translator, resolvedDocsRoot, *sourceLang, *targetLang, *overwrite, routes) if err != nil { fatal(err) } @@ -118,7 +123,7 @@ func main() { if *parallel > 1 { fatal(fmt.Errorf("parallel processing is only supported in doc mode")) } - proc, err := runSegmentSequential(context.Background(), ordered, translator, tm, resolvedDocsRoot, *sourceLang, *targetLang) + proc, err := runSegmentSequential(context.Background(), ordered, translator, tm, resolvedDocsRoot, *sourceLang, *targetLang, routes) if err != nil { fatal(err) } @@ -134,14 +139,14 @@ func main() { log.Printf("docs-i18n: completed processed=%d skipped=%d elapsed=%s", processed, skipped, elapsed) } -func runDocSequential(ctx context.Context, ordered []string, translator *PiTranslator, docsRoot, srcLang, tgtLang string, overwrite bool) (int, int, error) { +func runDocSequential(ctx context.Context, ordered []string, translator *PiTranslator, docsRoot, srcLang, tgtLang string, overwrite bool, routes *routeIndex) (int, int, error) { processed := 0 skipped := 0 for index, file := range ordered { relPath := resolveRelPath(docsRoot, file) log.Printf("docs-i18n: [%d/%d] start %s", index+1, len(ordered), relPath) start := time.Now() - skip, err := processFileDoc(ctx, translator, docsRoot, file, srcLang, tgtLang, overwrite) + skip, err := processFileDoc(ctx, translator, docsRoot, file, srcLang, tgtLang, overwrite, routes) if err != nil { return processed, skipped, err } @@ -156,7 +161,7 @@ func runDocSequential(ctx context.Context, ordered []string, translator *PiTrans return processed, skipped, nil } -func runDocParallel(ctx context.Context, ordered []string, docsRoot, srcLang, tgtLang string, overwrite bool, parallel int, glossary []GlossaryEntry, thinking string) (int, int, error) { +func runDocParallel(ctx context.Context, ordered []string, docsRoot, srcLang, tgtLang string, overwrite bool, parallel int, glossary []GlossaryEntry, thinking string, routes *routeIndex) (int, int, error) { jobs := make(chan docJob) results := make(chan docResult, len(ordered)) ctx, cancel := context.WithCancel(ctx) @@ -179,7 +184,7 @@ func runDocParallel(ctx context.Context, ordered []string, docsRoot, srcLang, tg } log.Printf("docs-i18n: [w%d %d/%d] start %s", workerID, job.index, len(ordered), job.rel) start := time.Now() - skip, err := processFileDoc(ctx, translator, docsRoot, job.path, srcLang, tgtLang, overwrite) + skip, err := processFileDoc(ctx, translator, docsRoot, job.path, srcLang, tgtLang, overwrite, routes) results <- docResult{ index: job.index, rel: job.rel, @@ -222,13 +227,13 @@ func runDocParallel(ctx context.Context, ordered []string, docsRoot, srcLang, tg return processed, skipped, nil } -func runSegmentSequential(ctx context.Context, ordered []string, translator *PiTranslator, tm *TranslationMemory, docsRoot, srcLang, tgtLang string) (int, error) { +func runSegmentSequential(ctx context.Context, ordered []string, translator *PiTranslator, tm *TranslationMemory, docsRoot, srcLang, tgtLang string, routes *routeIndex) (int, error) { processed := 0 for index, file := range ordered { relPath := resolveRelPath(docsRoot, file) log.Printf("docs-i18n: [%d/%d] start %s", index+1, len(ordered), relPath) start := time.Now() - if _, err := processFile(ctx, translator, tm, docsRoot, file, srcLang, tgtLang); err != nil { + if _, err := processFile(ctx, translator, tm, docsRoot, file, srcLang, tgtLang, routes); err != nil { return processed, err } processed++ diff --git a/scripts/docs-i18n/process.go b/scripts/docs-i18n/process.go index cbcd1d4abc2..e7ba487717b 100644 --- a/scripts/docs-i18n/process.go +++ b/scripts/docs-i18n/process.go @@ -11,7 +11,7 @@ import ( "gopkg.in/yaml.v3" ) -func processFile(ctx context.Context, translator *PiTranslator, tm *TranslationMemory, docsRoot, filePath, srcLang, tgtLang string) (bool, error) { +func processFile(ctx context.Context, translator *PiTranslator, tm *TranslationMemory, docsRoot, filePath, srcLang, tgtLang string, routes *routeIndex) (bool, error) { absPath, relPath, err := resolveDocsPath(docsRoot, filePath) if err != nil { return false, err @@ -74,6 +74,7 @@ func processFile(ctx context.Context, translator *PiTranslator, tm *TranslationM } translatedBody := applyTranslations(body, segments) + translatedBody = routes.localizeBodyLinks(translatedBody) updatedFront, err := encodeFrontMatter(frontData, relPath, content) if err != nil { return false, err