Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
294 lines
8.6 KiB
Go
294 lines
8.6 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"gopkg.in/yaml.v3"
|
|
)
|
|
|
|
const (
|
|
localizedLinkPostprocessPending = "pending"
|
|
localizedLinkPostprocessVersion = "locale-links-v1"
|
|
)
|
|
|
|
func processFile(ctx context.Context, translator docsTranslator, tm *TranslationMemory, docsRoot, filePath, srcLang, tgtLang string) (bool, string, error) {
|
|
absPath, relPath, err := resolveDocsPath(docsRoot, filePath)
|
|
if err != nil {
|
|
return false, "", err
|
|
}
|
|
|
|
content, err := os.ReadFile(absPath)
|
|
if err != nil {
|
|
return false, "", err
|
|
}
|
|
|
|
frontMatter, body := splitFrontMatter(string(content))
|
|
frontData := map[string]any{}
|
|
if frontMatter != "" {
|
|
if err := yaml.Unmarshal([]byte(frontMatter), &frontData); err != nil {
|
|
return false, "", fmt.Errorf("frontmatter parse failed for %s: %w", relPath, err)
|
|
}
|
|
}
|
|
|
|
if err := translateFrontMatter(ctx, translator, tm, frontData, relPath, srcLang, tgtLang); err != nil {
|
|
return false, "", err
|
|
}
|
|
|
|
body, err = translateHTMLBlocks(ctx, translator, body, srcLang, tgtLang)
|
|
if err != nil {
|
|
return false, "", err
|
|
}
|
|
|
|
segments, err := extractSegments(body, relPath)
|
|
if err != nil {
|
|
return false, "", err
|
|
}
|
|
|
|
namespace := cacheNamespace()
|
|
for i := range segments {
|
|
seg := &segments[i]
|
|
seg.CacheKey = cacheKey(namespace, srcLang, tgtLang, seg.SegmentID, seg.TextHash)
|
|
if entry, ok := tm.Get(seg.CacheKey); ok {
|
|
seg.Translated = entry.Translated
|
|
continue
|
|
}
|
|
translated, err := translator.Translate(ctx, seg.Text, srcLang, tgtLang)
|
|
if err != nil {
|
|
return false, "", fmt.Errorf("translate failed (%s): %w", relPath, err)
|
|
}
|
|
seg.Translated = translated
|
|
entry := TMEntry{
|
|
CacheKey: seg.CacheKey,
|
|
SegmentID: seg.SegmentID,
|
|
SourcePath: relPath,
|
|
TextHash: seg.TextHash,
|
|
Text: seg.Text,
|
|
Translated: translated,
|
|
Provider: docsI18nProvider(),
|
|
Model: docsI18nModel(),
|
|
SrcLang: srcLang,
|
|
TgtLang: tgtLang,
|
|
UpdatedAt: time.Now().UTC().Format(time.RFC3339),
|
|
}
|
|
tm.Put(entry)
|
|
}
|
|
|
|
translatedBody := applyTranslations(body, segments)
|
|
updatedFront, err := encodeFrontMatter(frontData, relPath, content)
|
|
if err != nil {
|
|
return false, "", err
|
|
}
|
|
|
|
outputPath := filepath.Join(docsRoot, tgtLang, relPath)
|
|
if err := os.MkdirAll(filepath.Dir(outputPath), 0o755); err != nil {
|
|
return false, "", err
|
|
}
|
|
|
|
output := updatedFront + translatedBody
|
|
return false, outputPath, os.WriteFile(outputPath, []byte(output), 0o644)
|
|
}
|
|
|
|
func splitFrontMatter(content string) (string, string) {
|
|
if !strings.HasPrefix(content, "---") {
|
|
return "", content
|
|
}
|
|
lines := strings.Split(content, "\n")
|
|
if len(lines) < 2 {
|
|
return "", content
|
|
}
|
|
endIndex := -1
|
|
for i := 1; i < len(lines); i++ {
|
|
if strings.TrimSpace(lines[i]) == "---" {
|
|
endIndex = i
|
|
break
|
|
}
|
|
}
|
|
if endIndex == -1 {
|
|
return "", content
|
|
}
|
|
front := strings.Join(lines[1:endIndex], "\n")
|
|
body := strings.Join(lines[endIndex+1:], "\n")
|
|
if strings.HasPrefix(body, "\n") {
|
|
body = body[1:]
|
|
}
|
|
return front, body
|
|
}
|
|
|
|
func encodeFrontMatter(frontData map[string]any, relPath string, source []byte) (string, error) {
|
|
if frontData == nil {
|
|
frontData = map[string]any{}
|
|
}
|
|
frontData["x-i18n"] = map[string]any{
|
|
"source_path": relPath,
|
|
"source_hash": hashBytes(source),
|
|
"provider": docsI18nProvider(),
|
|
"model": docsI18nModel(),
|
|
"workflow": workflowVersion,
|
|
"generated_at": time.Now().UTC().Format(time.RFC3339),
|
|
"postprocess_version": localizedLinkPostprocessPending,
|
|
}
|
|
encoded, err := yaml.Marshal(frontData)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return fmt.Sprintf("---\n%s---\n\n", string(encoded)), nil
|
|
}
|
|
|
|
func translateFrontMatter(ctx context.Context, translator docsTranslator, tm *TranslationMemory, data map[string]any, relPath, srcLang, tgtLang string) error {
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
if summary, ok := data["summary"].(string); ok {
|
|
if docsI18nVerboseLogs() {
|
|
log.Printf("docs-i18n: frontmatter start %s field=summary bytes=%d", relPath, len(summary))
|
|
}
|
|
translated, err := translateSnippet(ctx, translator, tm, relPath+":frontmatter:summary", summary, srcLang, tgtLang)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if docsI18nVerboseLogs() {
|
|
log.Printf("docs-i18n: frontmatter done %s field=summary out_bytes=%d", relPath, len(translated))
|
|
}
|
|
data["summary"] = translated
|
|
}
|
|
if title, ok := data["title"].(string); ok {
|
|
if docsI18nVerboseLogs() {
|
|
log.Printf("docs-i18n: frontmatter start %s field=title bytes=%d", relPath, len(title))
|
|
}
|
|
translated, err := translateSnippet(ctx, translator, tm, relPath+":frontmatter:title", title, srcLang, tgtLang)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if docsI18nVerboseLogs() {
|
|
log.Printf("docs-i18n: frontmatter done %s field=title out_bytes=%d", relPath, len(translated))
|
|
}
|
|
data["title"] = translated
|
|
}
|
|
if readWhen, ok := data["read_when"].([]any); ok {
|
|
translated := make([]any, 0, len(readWhen))
|
|
for idx, item := range readWhen {
|
|
textValue, ok := item.(string)
|
|
if !ok {
|
|
translated = append(translated, item)
|
|
continue
|
|
}
|
|
if docsI18nVerboseLogs() {
|
|
log.Printf("docs-i18n: frontmatter start %s field=read_when[%d] bytes=%d", relPath, idx, len(textValue))
|
|
}
|
|
value, err := translateSnippet(ctx, translator, tm, fmt.Sprintf("%s:frontmatter:read_when:%d", relPath, idx), textValue, srcLang, tgtLang)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if docsI18nVerboseLogs() {
|
|
log.Printf("docs-i18n: frontmatter done %s field=read_when[%d] out_bytes=%d", relPath, idx, len(value))
|
|
}
|
|
translated = append(translated, value)
|
|
}
|
|
data["read_when"] = translated
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func docsI18nVerboseLogs() bool {
|
|
value := strings.TrimSpace(os.Getenv("OPENCLAW_DOCS_I18N_VERBOSE_LOGS"))
|
|
if value == "" {
|
|
return false
|
|
}
|
|
switch strings.ToLower(value) {
|
|
case "1", "true", "yes", "on", "debug", "verbose":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
func translateSnippet(ctx context.Context, translator docsTranslator, tm *TranslationMemory, segmentID, textValue, srcLang, tgtLang string) (string, error) {
|
|
if strings.TrimSpace(textValue) == "" {
|
|
return textValue, nil
|
|
}
|
|
namespace := cacheNamespace()
|
|
textHash := hashText(textValue)
|
|
ck := cacheKey(namespace, srcLang, tgtLang, segmentID, textHash)
|
|
if entry, ok := tm.Get(ck); ok {
|
|
return entry.Translated, nil
|
|
}
|
|
translated, err := translator.Translate(ctx, textValue, srcLang, tgtLang)
|
|
if err != nil {
|
|
log.Printf("docs-i18n: frontmatter fallback %s reason=%v", segmentID, err)
|
|
return textValue, nil
|
|
}
|
|
shouldCache := true
|
|
if validationErr := validateFrontmatterScalarTranslation(textValue, translated); validationErr != nil {
|
|
log.Printf("docs-i18n: frontmatter fallback %s reason=%v", segmentID, validationErr)
|
|
translated = textValue
|
|
shouldCache = false
|
|
}
|
|
sourcePath := segmentID
|
|
if path, _, ok := strings.Cut(segmentID, ":frontmatter:"); ok {
|
|
sourcePath = path
|
|
}
|
|
entry := TMEntry{
|
|
CacheKey: ck,
|
|
SegmentID: segmentID,
|
|
SourcePath: sourcePath,
|
|
TextHash: textHash,
|
|
Text: textValue,
|
|
Translated: translated,
|
|
Provider: docsI18nProvider(),
|
|
Model: docsI18nModel(),
|
|
SrcLang: srcLang,
|
|
TgtLang: tgtLang,
|
|
UpdatedAt: time.Now().UTC().Format(time.RFC3339),
|
|
}
|
|
if shouldCache {
|
|
tm.Put(entry)
|
|
}
|
|
return translated, nil
|
|
}
|
|
|
|
func validateFrontmatterScalarTranslation(source, translated string) error {
|
|
trimmed := strings.TrimSpace(translated)
|
|
if trimmed == "" {
|
|
return fmt.Errorf("empty translation")
|
|
}
|
|
lower := strings.ToLower(trimmed)
|
|
if strings.Contains(lower, "<frontmatter>") || strings.Contains(lower, "</frontmatter>") || strings.Contains(lower, "<body>") || strings.Contains(lower, "</body>") {
|
|
return fmt.Errorf("tagged document wrapper detected")
|
|
}
|
|
if err := validateNoTranslationTranscriptArtifacts(source, trimmed); err != nil {
|
|
return err
|
|
}
|
|
if strings.Contains(trimmed, "[[[FM_") {
|
|
return fmt.Errorf("frontmatter marker leaked into scalar translation")
|
|
}
|
|
if strings.Contains(trimmed, "\n---\n") || strings.HasPrefix(trimmed, "---\n") {
|
|
return fmt.Errorf("yaml document boundary detected")
|
|
}
|
|
if !strings.Contains(source, "\n") && strings.Count(trimmed, "\n") >= 3 {
|
|
return fmt.Errorf("unexpected multiline expansion")
|
|
}
|
|
sourceLen := len(strings.TrimSpace(source))
|
|
translatedLen := len(trimmed)
|
|
if sourceLen > 0 {
|
|
limit := sourceLen*8 + 256
|
|
if limit < 512 {
|
|
limit = 512
|
|
}
|
|
if translatedLen > limit {
|
|
return fmt.Errorf("unexpected size expansion source=%d translated=%d", sourceLen, translatedLen)
|
|
}
|
|
}
|
|
for _, key := range []string{"title:", "summary:", "read_when:"} {
|
|
if strings.Contains(lower, "\n"+key) || strings.HasPrefix(lower, key) {
|
|
return fmt.Errorf("frontmatter key leaked into scalar translation")
|
|
}
|
|
}
|
|
return nil
|
|
}
|