chore(i18n): add translation automation scripts with improved V3 prompt#5334
chore(i18n): add translation automation scripts with improved V3 prompt#5334leaanthony wants to merge 5 commits intomasterfrom
Conversation
…provement
Adds three scripts to docs/scripts/ for automated Wails v3 doc translation:
translate-docs.py — Ollama-based batch translator (qwen3.6:35b)
- Updated system prompt (V3): explicit d2 string-label rule with example,
stronger product-name preservation, completeness guarantee, terminology
consistency, loanword guidance
- post_process(): auto-fixes frontmatter artifacts, asset paths, link rewrites
- Translation cache keyed on source MD5 hash
translate-qa.py — Heuristic + optional AI quality scorer
- 15 checks covering: length ratio, frontmatter, code blocks, imports,
locale character sets, URLs, d2 string labels (new check 14), artifact
detection, asset paths, internal link rewrites
- --ai-verify flag: calls z.ai (ZAI_API_KEY env var) for prose quality
scoring blended 60% heuristic / 40% AI
improve-prompts.py — 3-iteration prompt improvement loop
- Translates test files with V0 prompt, AI-evaluates results, generates
improved prompt, repeats N times, outputs A/B comparison table
- Applies winning prompt back to translate-docs.py
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
WalkthroughAdds three CLI Python scripts to translate, evaluate, and iteratively improve MDX documentation prompts using an Ollama streaming chat backend with optional z.ai verification; includes chunked translation, per-file caching, deterministic post-processing/link rewriting, heuristic+AI QA scoring, prompt-improvement iterations, and JSON reports/artifacts. ChangesDocs Translation / QA / Prompt-Improvement Pipeline
Sequence Diagram(s)sequenceDiagram
participant User as User/CLI
participant Translate as translate-docs.py
participant Improve as improve-prompts.py
participant QA as translate-qa.py
participant Ollama as Ollama API
participant ZAI as z.ai API
participant FS as File System
User->>Translate: run_locale(locale, files)
Translate->>FS: load_cache(locale)
loop per source file
Translate->>Ollama: translate_with_ollama(streaming chunks)
Ollama-->>Translate: streaming message.content tokens
Translate->>Translate: post_process(translated_text)
Translate->>FS: write translated file & update cache
end
Translate-->>User: summary (translated, cached, failed)
User->>QA: score_locale(locale, --ai-verify?)
QA->>FS: read source & translated files
QA->>QA: check_file_pair (heuristics)
alt ai-verify enabled
QA->>ZAI: ai_verify_translation(excerpt)
ZAI-->>QA: JSON score + issues
QA->>QA: blend heuristic + AI score
end
QA->>FS: write .translation-qa reports
QA-->>User: per-file scores
User->>Improve: run_iterations(locale)
loop iterations
loop per test file
Improve->>Ollama: translate(test_file, current_prompt)
Ollama-->>Improve: translation
Improve->>Improve: heuristic_score + evaluate via Ollama
end
Improve->>Ollama: generate_improved_prompt(aggregated_issues)
Ollama-->>Improve: new system prompt
Improve->>FS: save prompt versions & per-iteration reports
end
Improve-->>User: comparison table, select winning prompt
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (12)
docs/scripts/translate-qa.py (4)
256-271: ⚡ Quick win
commonly_translatedlink allowlist will go stale.The hard-coded set duplicates implicit knowledge already encoded in
translate-docs.py(PRIORITY_FILES,PATH_REMAP). Any time priority pages change, this allowlist needs a manual update or the QA check silently stops flagging real link-rewrite regressions. Consider importing the path list fromtranslate-docs.py(or extracting both modules' shared constants into a small_shared.py) so QA tracks reality.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/scripts/translate-qa.py` around lines 256 - 271, Replace the hard-coded allowlist in commonly_translated with a canonical source so it won't go stale: import the relevant path set from translate-docs.py (e.g. PRIORITY_FILES and/or PATH_REMAP) or move shared constants into a new _shared.py and import that into both scripts; then build commonly_translated from the imported set (preserving the existing comparison logic that computes src_internal, tgt_internal, unchanged_links and missing_rewrites) so QA link-rewrite checks automatically follow changes to the canonical translate-docs path lists.
370-373: ⚡ Quick winLocale validation is missing on the QA path.
Unlike
translate-docs.py, this script accepts arbitrary locale strings via--locale foo,barand just iterates them.score_localewill return{"error": "Locale directory not found", ...}and continue, so typos look like "no translations exist" rather than "unknown locale". Validate againstLOCALE_NAMESbefore running, mirroring the translator script.♻️ Proposed validation
if args.locale == "all": locales = ALL_LOCALES else: locales = [l.strip() for l in args.locale.split(",")] + unknown = [l for l in locales if l not in LOCALE_NAMES] + if unknown: + print(f"Unknown locale(s): {', '.join(unknown)}. Valid: {', '.join(ALL_LOCALES)}") + sys.exit(1)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/scripts/translate-qa.py` around lines 370 - 373, Validate the locales passed to translate-qa.py by checking the parsed list (from args.locale or ALL_LOCALES) against the canonical LOCALE_NAMES before processing; for each locale in the computed locales list, if it's not in LOCALE_NAMES, raise/print a clear error and exit (or skip with a descriptive message) so mis-typed locales don't silently produce "no translations" results from score_locale; update the locale selection logic that currently sets locales = ALL_LOCALES or locales = [l.strip() ...] to perform this membership check and report unknown locales referencing args.locale, ALL_LOCALES, LOCALE_NAMES, and score_locale.
405-416: 💤 Low valueReports overwrite same-day runs and
importis misplaced.Two small things:
import datetimebelongs at the top of the module with the other imports; placing it insidemain()re-imports on every call and obscures dependencies.- Per-locale and combined report filenames are keyed on
todayonly, so multiple runs on the same day silently clobber prior reports. Add a time component (or a run id) if you intend to keep history.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/scripts/translate-qa.py` around lines 405 - 416, Move the inline "import datetime" out of the function and into the module-level imports so the module imports are declared once (reference: the datetime import currently near the creation of "today"); change the report filename keying so runs don't clobber same-day reports by including a time component or run id when building file names (reference: the "today" variable used to create "per_locale_path" and "combined_path" under "QA_DIR" and writing "all_results" / per-locale "result"); ensure the time/run-id uses a stable, filesystem-safe string and update the filename construction for per_locale_path and combined_path accordingly.
60-67: 💤 Low valueFrontmatter extractor doesn't require a delimiter line.
extract_frontmatteraccepts any content starting with the literal"---"and finds the next"---"substring. A file whose body legitimately contains---(e.g. a horizontal rule on the first non-frontmatter line) without a frontmatter block will be misparsed. Anchor on---\nand the closing\n---\n/\n---$to make the contract explicit and align with the Starlight/MDX frontmatter spec.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/scripts/translate-qa.py` around lines 60 - 67, The extract_frontmatter function currently treats any occurrence of the literal '---' as frontmatter delimiters, causing misparsing when the body contains '---'; update extract_frontmatter to only accept frontmatter when the file starts with '---\n' and to search for a closing delimiter that is either '\n---\n' or '\n---' at EOF, then return the text between those explicit delimiters as the frontmatter and the rest as the body (reference: extract_frontmatter(content: str) -> tuple[str, str]).docs/scripts/translate-docs.py (5)
25-26: ⚡ Quick winHard-coded internal Ollama endpoint and model.
OLLAMA_BASEpoints at a private Tailscale hostname andMODELis a specific tag (qwen3.6:35b) only available on that host. Anyone running this outside the author's environment will fail with a connection error. Consider reading both from environment variables with sensible defaults so the script is portable and so the same script can target a local Ollama instance.♻️ Proposed env-driven config
-OLLAMA_BASE = "http://ai-master.taileaa27f.ts.net:11434" -MODEL = "qwen3.6:35b" +OLLAMA_BASE = os.environ.get("OLLAMA_BASE", "http://localhost:11434") +MODEL = os.environ.get("OLLAMA_MODEL", "qwen3:32b")Also, please confirm
qwen3.6:35bis the intended Ollama model tag — Qwen's public lineup isqwen3(with sizes like:32b,:14b, etc.), and a3.6:35btag would need to be a custom local pull. The same identifier is used inimprove-prompts.py.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/scripts/translate-docs.py` around lines 25 - 26, Replace the hard-coded Ollama host and model by reading them from environment variables with sensible defaults: change OLLAMA_BASE to use an env var (e.g., OLLAMA_BASE or OLLAMA_URL) defaulting to "http://localhost:11434" and change MODEL to use an env var (e.g., OLLAMA_MODEL) defaulting to a public tag such as "qwen3:32b" (or whatever public tag you intend), then update both translate-docs.py and improve-prompts.py to use these env-driven values (symbols: OLLAMA_BASE and MODEL) so the scripts work outside the author’s private network and can target local Ollama instances; also verify and, if needed, correct the MODEL default to the intended Qwen tag.
18-23: ⚡ Quick winAvoid silently
pip install-ing dependencies on import.
os.system("pip3 install requests -q")modifies the user's environment without consent, may run as root in CI, and can fail in restricted/offline environments while still falling through toimport requests(which will then raise). Prefer either a pinnedrequirements.txt/pyproject.tomlnext to the scripts or an explicit error message instructing the user how to install. The same pattern appears inimprove-prompts.pyandtranslate-qa.py.♻️ Proposed graceful failure
try: import requests except ImportError: - print("Installing requests...") - os.system("pip3 install requests -q") - import requests + sys.exit("Missing dependency 'requests'. Install with: pip3 install requests")🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/scripts/translate-docs.py` around lines 18 - 23, Replace the current try/except that auto-installs requests (the import requests block using os.system("pip3 install requests -q")) with a graceful failure: do not run pip from the script; instead catch ImportError and print or raise a clear message telling the user to install the dependency (e.g., "Please install requests via pip3 install -r requirements.txt or add requests to your pyproject.toml"), then exit non-zero. Apply the same change pattern to the analogous blocks in improve-prompts.py and translate-qa.py so scripts fail fast with actionable instructions rather than modifying the environment silently.
391-399: ⚡ Quick winSubstring path filter can over-match unrelated files.
any(part in str(p) for part in ["blog", "changelog", "whats-new", "showcase"])does a substring check on the full path string. Any future file or directory whose name contains one of these tokens (e.g.weblog.md,showcase-template/) will be silently skipped from translation. Compare againstp.partsinstead so the match is by path component.♻️ Proposed component-level filter
- all_files = set( - p for p in DOCS_ROOT.rglob("*.mdx") - if not any(part in str(p) for part in ["blog", "changelog", "whats-new", "showcase"]) - and p.parts[len(DOCS_ROOT.parts)] not in ALL_LOCALES - ) | set( - p for p in DOCS_ROOT.rglob("*.md") - if not any(part in str(p) for part in ["blog", "changelog", "whats-new", "showcase"]) - and p.parts[len(DOCS_ROOT.parts)] not in ALL_LOCALES - ) + excluded = {"blog", "changelog", "whats-new", "showcase"} + all_files = { + p for p in (*DOCS_ROOT.rglob("*.mdx"), *DOCS_ROOT.rglob("*.md")) + if not (set(p.parts) & excluded) + and p.parts[len(DOCS_ROOT.parts)] not in ALL_LOCALES + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/scripts/translate-docs.py` around lines 391 - 399, The current all_files set comprehension uses substring matching on the full path via any(part in str(p) for part in ["blog", "changelog", "whats-new", "showcase"]) which can erroneously skip files whose names merely contain those tokens; update the filter in the all_files calculation to check path components instead (e.g., use any(token in p.parts for token in ["blog","changelog","whats-new","showcase"])) so matching is done by path segment, keeping the rest of the logic (DOCS_ROOT, ALL_LOCALES, and the p.parts[len(DOCS_ROOT.parts)] locale check) unchanged.
273-273: ⚡ Quick winReturn type annotation should reflect
Noneon failure.
translate_with_ollamareturnsNoneon timeouts, exceptions, and empty output, but the signature is annotated-> str. Same applies totranslate_file's call site (it correctly handles theNoneviaif not translated). Update the annotation toOptional[str](orstr | None) so the type contract matches the runtime behavior and downstream callers/IDEs can flagNonehandling.♻️ Proposed annotation fix
-def translate_with_ollama(content: str, lang: str, file_path: str) -> str: +def translate_with_ollama(content: str, lang: str, file_path: str) -> str | None:Also applies to: 338-338
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/scripts/translate-docs.py` at line 273, The function signature for translate_with_ollama currently declares -> str but can return None on timeouts/exceptions/empty output; update its return annotation to Optional[str] (or use str | None) and do the same for translate_file's signature/callsite annotation so the type contract matches runtime behavior; also add the necessary typing import (from typing import Optional) if using Optional, and ensure any mypy/IDE checks pass after the change.
231-243: 💤 Low valueTrailing-slash branch in
rewrite_markdown_linkis unreachable.The capturing regex
(/?)only ever matches an empty string or a single/, sosuffixcan never equal')'. The conditionalsuffix if suffix != ')' else ''is dead code; you can drop the ternary and just appendsuffix.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/scripts/translate-docs.py` around lines 231 - 243, The ternary in rewrite_markdown_link is dead because the regex' third capture (/?) can only be '' or '/', so remove the unnecessary conditional and simply append suffix; update the return in rewrite_markdown_link to use f"[{text}]({new_path}{suffix})" (keeping the existing behavior) and leave the re.sub and pattern as-is.docs/scripts/improve-prompts.py (3)
415-426: ⚡ Quick winSilent locale filtering hides typos.
[l.strip() for l in args.locale.split(",") if l.strip() in LOCALE_NAMES]drops any unknown locale without warning. A user invoking--locale de,jp(typo forja) sees only the German run with no indication their second locale was ignored. Validate explicitly and exit on unknown locales, mirroring the behavior intranslate-docs.py:main.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/scripts/improve-prompts.py` around lines 415 - 426, The locale parsing silently drops unknown entries; update main() to explicitly validate args.locale by splitting on commas, trimming entries and collecting unknowns (compare each against LOCALE_NAMES), and if any unknowns exist print an error listing the invalid locales and the valid LOCALE_NAMES and exit with non-zero status; otherwise assign the validated locales to the locales variable (keep using LOCALE_NAMES, args.locale and locales identifiers) — mirror the explicit validation behavior used in translate-docs.py:main.
247-262: 💤 Low valueBrittle prompt name derivation via
current_prompt['name'][1].
f"V{current_prompt['name'][1]} improved"indexes the second character of the name string, which only works for single-digitV0–V9and breaks immediately atV10. In practice this name is overwritten on line 465 withf"V{iteration+1}", so the value computed here is dead. Either drop the field here entirely or compute it correctly from the parsed iteration index.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/scripts/improve-prompts.py` around lines 247 - 262, In generate_improved_prompt: the name construction uses brittle indexing current_prompt['name'][1] (second character), which fails for multi-digit iterations and is effectively dead because the name is overwritten later; fix by either omitting the "name" key from the returned dict (leave only "system" and "user") or compute the iteration properly by parsing the numeric suffix from current_prompt['name'] (e.g., strip the leading "V" and parse the rest as an int) before formatting the new name; update the returned dict accordingly in generate_improved_prompt so it no longer relies on current_prompt['name'][1].
213-223: ⚡ Quick winGreedy
\{[\s\S]+\}regex can swallow trailing prose and produce invalid JSON.
re.search(r'\{[\s\S]+\}', result)is greedy and will match from the first{to the last}in the response. If the model emits any commentary after the JSON object that contains a stray}(or multiple JSON-ish blocks), the captured span won't parse. Switch to a non-greedy match anchored to a balanced extraction, or use the samejson.loads(raw)path you already use after fence stripping.♻️ More resilient extraction
- # Find JSON object - m = re.search(r'\{[\s\S]+\}', result) - if not m: - return {"score": 0.5, "issues": [], "prompt_improvements": [], "summary": result[:200]} try: - return json.loads(m.group(0)) + return json.loads(result) except json.JSONDecodeError: - return {"score": 0.5, "issues": [], "prompt_improvements": [result[:500]], "summary": "Parse error"} + # Fallback: attempt to find the first balanced JSON object + depth, start = 0, None + for i, ch in enumerate(result): + if ch == "{": + if start is None: + start = i + depth += 1 + elif ch == "}": + depth -= 1 + if depth == 0 and start is not None: + try: + return json.loads(result[start:i+1]) + except json.JSONDecodeError: + break + return {"score": 0.5, "issues": [], "prompt_improvements": [result[:500]], "summary": "Parse error"}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/scripts/improve-prompts.py` around lines 213 - 223, The current extraction uses a greedy regex re.search(r'\{[\s\S]+\}', result) which can capture past the intended JSON; instead, after stripping markdown fences (the result variable handling), first try json.loads(result) to parse a pure JSON response, and if that fails fall back to a non-greedy regex (e.g., use a non-greedy pattern like r'\{[\s\S]+?\}' to find the first JSON-looking object) before calling json.loads(m.group(0)); update the branches around the m = re.search(...) and the subsequent json.loads call to use this order so trailing prose or extra braces won't break parsing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/scripts/improve-prompts.py`:
- Around line 496-509: The generated winning prompt may contain raw
triple-quotes which will break the source patching; instead of interpolating the
prompt into the translate-docs.py source, have apply_winning_prompt write the
system prompt to a sidecar file (e.g. WORK_DIR / locale / "system_prompt.txt")
and change the patching behavior so translate-docs.py reads SYSTEM_PROMPT from
that file at runtime if present; update the code paths around
apply_winning_prompt, generate_improved_prompt, and the SYSTEM_PROMPT usage in
translate-docs.py to (1) write winning_prompt to the sidecar file when
best_idx>0 and winning_prompt_path.exists(), and (2) make translate-docs.py
prefer loading WORK_DIR/locale/system_prompt.txt over embedding a triple-quoted
string, avoiding any direct interpolation of raw """ into the source.
In `@docs/scripts/translate-docs.py`:
- Around line 164-179: In build_translated_paths, the existence check is
currently masked by the dead "or True" (out_path.exists() or True) so paths
always includes every input; remove the "or True" and let the condition be "if
out_path.exists():" (or alternatively drop the if entirely if you want to always
assume translation) so that paths only gains url_path when the cached translated
file at out_path actually exists; update the function to use DOCS_ROOT,
out_path, rel and files as-is when implementing this change.
In `@docs/scripts/translate-qa.py`:
- Around line 275-287: The current d2-label check using zip(src_labels,
tgt_labels) misses added/removed labels and reordering; update the block that
compares src_labels and tgt_labels to (1) first check length mismatch and append
an issues entry when len(src_labels) != len(tgt_labels) (mention counts and
which side has extras) and decrement score accordingly, (2) check
set(src_labels) != set(tgt_labels) and if so report added/removed labels by
computing set differences to capture reorder/add/remove, and (3) still compute
per-index diffs by zipping only up to min(len(src_labels), len(tgt_labels)) to
report direct translations for differing positions (keep the existing changed
logic for those). Refer to variables src_d2_blocks, tgt_d2_blocks, src_labels,
tgt_labels, changed, issues, and score when making these edits.
---
Nitpick comments:
In `@docs/scripts/improve-prompts.py`:
- Around line 415-426: The locale parsing silently drops unknown entries; update
main() to explicitly validate args.locale by splitting on commas, trimming
entries and collecting unknowns (compare each against LOCALE_NAMES), and if any
unknowns exist print an error listing the invalid locales and the valid
LOCALE_NAMES and exit with non-zero status; otherwise assign the validated
locales to the locales variable (keep using LOCALE_NAMES, args.locale and
locales identifiers) — mirror the explicit validation behavior used in
translate-docs.py:main.
- Around line 247-262: In generate_improved_prompt: the name construction uses
brittle indexing current_prompt['name'][1] (second character), which fails for
multi-digit iterations and is effectively dead because the name is overwritten
later; fix by either omitting the "name" key from the returned dict (leave only
"system" and "user") or compute the iteration properly by parsing the numeric
suffix from current_prompt['name'] (e.g., strip the leading "V" and parse the
rest as an int) before formatting the new name; update the returned dict
accordingly in generate_improved_prompt so it no longer relies on
current_prompt['name'][1].
- Around line 213-223: The current extraction uses a greedy regex
re.search(r'\{[\s\S]+\}', result) which can capture past the intended JSON;
instead, after stripping markdown fences (the result variable handling), first
try json.loads(result) to parse a pure JSON response, and if that fails fall
back to a non-greedy regex (e.g., use a non-greedy pattern like r'\{[\s\S]+?\}'
to find the first JSON-looking object) before calling json.loads(m.group(0));
update the branches around the m = re.search(...) and the subsequent json.loads
call to use this order so trailing prose or extra braces won't break parsing.
In `@docs/scripts/translate-docs.py`:
- Around line 25-26: Replace the hard-coded Ollama host and model by reading
them from environment variables with sensible defaults: change OLLAMA_BASE to
use an env var (e.g., OLLAMA_BASE or OLLAMA_URL) defaulting to
"http://localhost:11434" and change MODEL to use an env var (e.g., OLLAMA_MODEL)
defaulting to a public tag such as "qwen3:32b" (or whatever public tag you
intend), then update both translate-docs.py and improve-prompts.py to use these
env-driven values (symbols: OLLAMA_BASE and MODEL) so the scripts work outside
the author’s private network and can target local Ollama instances; also verify
and, if needed, correct the MODEL default to the intended Qwen tag.
- Around line 18-23: Replace the current try/except that auto-installs requests
(the import requests block using os.system("pip3 install requests -q")) with a
graceful failure: do not run pip from the script; instead catch ImportError and
print or raise a clear message telling the user to install the dependency (e.g.,
"Please install requests via pip3 install -r requirements.txt or add requests to
your pyproject.toml"), then exit non-zero. Apply the same change pattern to the
analogous blocks in improve-prompts.py and translate-qa.py so scripts fail fast
with actionable instructions rather than modifying the environment silently.
- Around line 391-399: The current all_files set comprehension uses substring
matching on the full path via any(part in str(p) for part in ["blog",
"changelog", "whats-new", "showcase"]) which can erroneously skip files whose
names merely contain those tokens; update the filter in the all_files
calculation to check path components instead (e.g., use any(token in p.parts for
token in ["blog","changelog","whats-new","showcase"])) so matching is done by
path segment, keeping the rest of the logic (DOCS_ROOT, ALL_LOCALES, and the
p.parts[len(DOCS_ROOT.parts)] locale check) unchanged.
- Line 273: The function signature for translate_with_ollama currently declares
-> str but can return None on timeouts/exceptions/empty output; update its
return annotation to Optional[str] (or use str | None) and do the same for
translate_file's signature/callsite annotation so the type contract matches
runtime behavior; also add the necessary typing import (from typing import
Optional) if using Optional, and ensure any mypy/IDE checks pass after the
change.
- Around line 231-243: The ternary in rewrite_markdown_link is dead because the
regex' third capture (/?) can only be '' or '/', so remove the unnecessary
conditional and simply append suffix; update the return in rewrite_markdown_link
to use f"[{text}]({new_path}{suffix})" (keeping the existing behavior) and leave
the re.sub and pattern as-is.
In `@docs/scripts/translate-qa.py`:
- Around line 256-271: Replace the hard-coded allowlist in commonly_translated
with a canonical source so it won't go stale: import the relevant path set from
translate-docs.py (e.g. PRIORITY_FILES and/or PATH_REMAP) or move shared
constants into a new _shared.py and import that into both scripts; then build
commonly_translated from the imported set (preserving the existing comparison
logic that computes src_internal, tgt_internal, unchanged_links and
missing_rewrites) so QA link-rewrite checks automatically follow changes to the
canonical translate-docs path lists.
- Around line 370-373: Validate the locales passed to translate-qa.py by
checking the parsed list (from args.locale or ALL_LOCALES) against the canonical
LOCALE_NAMES before processing; for each locale in the computed locales list, if
it's not in LOCALE_NAMES, raise/print a clear error and exit (or skip with a
descriptive message) so mis-typed locales don't silently produce "no
translations" results from score_locale; update the locale selection logic that
currently sets locales = ALL_LOCALES or locales = [l.strip() ...] to perform
this membership check and report unknown locales referencing args.locale,
ALL_LOCALES, LOCALE_NAMES, and score_locale.
- Around line 405-416: Move the inline "import datetime" out of the function and
into the module-level imports so the module imports are declared once
(reference: the datetime import currently near the creation of "today"); change
the report filename keying so runs don't clobber same-day reports by including a
time component or run id when building file names (reference: the "today"
variable used to create "per_locale_path" and "combined_path" under "QA_DIR" and
writing "all_results" / per-locale "result"); ensure the time/run-id uses a
stable, filesystem-safe string and update the filename construction for
per_locale_path and combined_path accordingly.
- Around line 60-67: The extract_frontmatter function currently treats any
occurrence of the literal '---' as frontmatter delimiters, causing misparsing
when the body contains '---'; update extract_frontmatter to only accept
frontmatter when the file starts with '---\n' and to search for a closing
delimiter that is either '\n---\n' or '\n---' at EOF, then return the text
between those explicit delimiters as the frontmatter and the rest as the body
(reference: extract_frontmatter(content: str) -> tuple[str, str]).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 01110527-6d96-49c5-92b2-f572602ca1f4
📒 Files selected for processing (3)
docs/scripts/improve-prompts.pydocs/scripts/translate-docs.pydocs/scripts/translate-qa.py
| # Apply winning prompt to translate-docs.py (use last iteration's prompt as winner) | ||
| # The winning prompt is the one from the best scoring iteration | ||
| # We need to find which prompt object corresponds to best_idx | ||
| # Prompts are: iter 0 = V0, iter 1 = V1, iter 2 = V2 | ||
| # The prompt used in iteration i is saved at prompt_v{i}.txt (for i>0) or is PROMPT_V0 | ||
| if not args.no_patch and best_idx > 0: | ||
| winning_prompt_path = WORK_DIR / locale / f"prompt_v{best_idx}.txt" | ||
| if winning_prompt_path.exists(): | ||
| winning_prompt = { | ||
| "system": winning_prompt_path.read_text(encoding="utf-8"), | ||
| "user": PROMPT_V0["user"], | ||
| } | ||
| print(f"\nApplying winning prompt ({best_name}) to translate-docs.py...", flush=True) | ||
| apply_winning_prompt(winning_prompt) |
There was a problem hiding this comment.
apply_winning_prompt may inject unescaped """ into the patched script.
generate_improved_prompt returns whatever the model produces, and apply_winning_prompt interpolates that text directly between SYSTEM_PROMPT = """ ... """ (line 409 of improve-prompts.py and the regex anchored to triple quotes). If the LLM ever emits a """ in the improved prompt — entirely possible since it's free-form English — the patched translate-docs.py will be syntactically broken, breaking subsequent translation runs and CI.
Either reject/escape """ in the generated prompt before patching, or write to a sidecar file (e.g. system_prompt.txt) that translate-docs.py reads at runtime, instead of editing the source file.
🛡️ Minimal guard before patching
def apply_winning_prompt(winning_prompt: dict):
"""Patch translate-docs.py with the winning system prompt."""
+ if '"""' in winning_prompt["system"]:
+ print(" ✗ Winning prompt contains triple quotes; refusing to patch translate-docs.py", flush=True)
+ return False
script_path = Path(__file__).parent / "translate-docs.py"🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/scripts/improve-prompts.py` around lines 496 - 509, The generated
winning prompt may contain raw triple-quotes which will break the source
patching; instead of interpolating the prompt into the translate-docs.py source,
have apply_winning_prompt write the system prompt to a sidecar file (e.g.
WORK_DIR / locale / "system_prompt.txt") and change the patching behavior so
translate-docs.py reads SYSTEM_PROMPT from that file at runtime if present;
update the code paths around apply_winning_prompt, generate_improved_prompt, and
the SYSTEM_PROMPT usage in translate-docs.py to (1) write winning_prompt to the
sidecar file when best_idx>0 and winning_prompt_path.exists(), and (2) make
translate-docs.py prefer loading WORK_DIR/locale/system_prompt.txt over
embedding a triple-quoted string, avoiding any direct interpolation of raw """
into the source.
| def build_translated_paths(locale: str, files: list) -> set: | ||
| """ | ||
| Return the set of root-relative paths (e.g. '/quick-start/next-steps') | ||
| that have translated versions for this locale, based on the files list. | ||
| This drives automatic link rewriting in post_process(). | ||
| """ | ||
| paths = set() | ||
| for src_path in files: | ||
| rel = src_path.relative_to(DOCS_ROOT) | ||
| # Strip extension to get the URL path | ||
| url_path = "/" + str(rel.with_suffix("")) | ||
| # Also check if the locale file actually exists (cached from prior run) | ||
| out_path = DOCS_ROOT / locale / rel | ||
| if out_path.exists() or True: # optimistic: assume we'll translate it | ||
| paths.add(url_path) | ||
| return paths |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Dead or True makes existence check meaningless.
The if out_path.exists() or True: branch is always taken, so the existence check has no effect and paths always includes every input file. Either trust the optimistic intent (and drop the dead check entirely) or actually gate on out_path.exists().
♻️ Proposed simplification
paths = set()
for src_path in files:
rel = src_path.relative_to(DOCS_ROOT)
# Strip extension to get the URL path
url_path = "/" + str(rel.with_suffix(""))
- # Also check if the locale file actually exists (cached from prior run)
- out_path = DOCS_ROOT / locale / rel
- if out_path.exists() or True: # optimistic: assume we'll translate it
- paths.add(url_path)
+ # Optimistic: every source file in this run is treated as translated
+ # so internal links can be rewritten to the locale-prefixed version.
+ paths.add(url_path)
return paths🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/scripts/translate-docs.py` around lines 164 - 179, In
build_translated_paths, the existence check is currently masked by the dead "or
True" (out_path.exists() or True) so paths always includes every input; remove
the "or True" and let the condition be "if out_path.exists():" (or alternatively
drop the if entirely if you want to always assume translation) so that paths
only gains url_path when the cached translated file at out_path actually exists;
update the function to use DOCS_ROOT, out_path, rel and files as-is when
implementing this change.
| src_d2_blocks = re.findall(r'```d2[\s\S]*?```', src) | ||
| tgt_d2_blocks = re.findall(r'```d2[\s\S]*?```', tgt) | ||
| for i, (sd, td) in enumerate(zip(src_d2_blocks, tgt_d2_blocks)): | ||
| src_labels = re.findall(r'"([^"]+)"', sd) | ||
| tgt_labels = re.findall(r'"([^"]+)"', td) | ||
| if src_labels != tgt_labels: | ||
| changed = [(s, t) for s, t in zip(src_labels, tgt_labels) if s != t] | ||
| if changed: | ||
| issues.append( | ||
| f"d2 diagram string labels translated in block {i+1}: " | ||
| + ", ".join(f'"{s}" → "{t}"' for s, t in changed[:3]) | ||
| ) | ||
| score -= 0.15 |
There was a problem hiding this comment.
d2 label diff misses count-mismatch and ordering changes.
Two issues with this check:
zip(src_labels, tgt_labels)silently truncates if the model adds/removes a label in the d2 block, so a diagram with extra/missing nodes never trips the flag even thoughsrc_labels != tgt_labelsis true.- If labels were reordered or some translated and others added,
changedcould be empty (because pairwise differences are masked by reordering), causing the outerif changed:to suppress an issue you already detected on line 280.
Suggest reporting whenever the label sets or lengths diverge, in addition to per-pair diffs.
🛡️ Proposed fix
- for i, (sd, td) in enumerate(zip(src_d2_blocks, tgt_d2_blocks)):
+ for i, (sd, td) in enumerate(zip(src_d2_blocks, tgt_d2_blocks)):
src_labels = re.findall(r'"([^"]+)"', sd)
tgt_labels = re.findall(r'"([^"]+)"', td)
if src_labels != tgt_labels:
- changed = [(s, t) for s, t in zip(src_labels, tgt_labels) if s != t]
- if changed:
- issues.append(
- f"d2 diagram string labels translated in block {i+1}: "
- + ", ".join(f'"{s}" → "{t}"' for s, t in changed[:3])
- )
- score -= 0.15
+ changed = [(s, t) for s, t in zip(src_labels, tgt_labels) if s != t]
+ detail = (
+ ", ".join(f'"{s}" → "{t}"' for s, t in changed[:3])
+ if changed
+ else f"label count {len(src_labels)} → {len(tgt_labels)}"
+ )
+ issues.append(f"d2 diagram string labels modified in block {i+1}: {detail}")
+ score -= 0.15🧰 Tools
🪛 Ruff (0.15.12)
[warning] 277-277: zip() without an explicit strict= parameter
Add explicit value for parameter strict=
(B905)
[warning] 281-281: zip() without an explicit strict= parameter
Add explicit value for parameter strict=
(B905)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/scripts/translate-qa.py` around lines 275 - 287, The current d2-label
check using zip(src_labels, tgt_labels) misses added/removed labels and
reordering; update the block that compares src_labels and tgt_labels to (1)
first check length mismatch and append an issues entry when len(src_labels) !=
len(tgt_labels) (mention counts and which side has extras) and decrement score
accordingly, (2) check set(src_labels) != set(tgt_labels) and if so report
added/removed labels by computing set differences to capture reorder/add/remove,
and (3) still compute per-index diffs by zipping only up to min(len(src_labels),
len(tgt_labels)) to report direct translations for differing positions (keep the
existing changed logic for those). Refer to variables src_d2_blocks,
tgt_d2_blocks, src_labels, tgt_labels, changed, issues, and score when making
these edits.
There was a problem hiding this comment.
Pull request overview
Adds a new docs-translation toolchain under docs/scripts/ for Wails v3: one script to generate localized docs with Ollama, one to score translation quality heuristically/optionally with AI, and one to iterate on the translation prompt. In the docs codebase, this is intended to automate localization output and prompt tuning for the Astro/Starlight docs site.
Changes:
- Adds
translate-docs.pyto batch-translate Markdown/MDX docs, rewrite links/assets, and cache results per locale. - Adds
translate-qa.pyto score translated files with structural heuristics and optional z.ai verification. - Adds
improve-prompts.pyto run iterative prompt experiments and patch the winning prompt back into the translator.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 11 comments.
| File | Description |
|---|---|
docs/scripts/translate-docs.py |
New Ollama-based translation pipeline with prompt rules, link/asset post-processing, and MD5 cache handling. |
docs/scripts/translate-qa.py |
New QA scorer for translated docs, including heuristic checks, optional AI verification, and report output. |
docs/scripts/improve-prompts.py |
New prompt-optimization loop that translates sample files, evaluates results, and updates the translator prompt. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| os.system("pip3 install requests -q") | ||
| import requests | ||
|
|
||
| OLLAMA_BASE = "http://ai-master.taileaa27f.ts.net:11434" |
| # e.g. the primary installation CTA links to quick-start/installation but we | ||
| # translate getting-started/installation, so the link should be rewritten. | ||
| PATH_REMAP = { | ||
| "/quick-start/installation": "/getting-started/installation", | ||
| } |
| # Also check if the locale file actually exists (cached from prior run) | ||
| out_path = DOCS_ROOT / locale / rel | ||
| if out_path.exists() or True: # optimistic: assume we'll translate it |
| # Source root: docs/src/content/docs/index.mdx → ../../assets = docs/src/assets ✓ | ||
| # Locale root: docs/src/content/docs/de/index.mdx → ../../assets = docs/src/content/assets ✗ | ||
| # Fix: ../../assets/ → ../../../assets/ | ||
| content = content.replace("../../assets/", "../../../assets/") |
| # Check cache | ||
| if cache_key in cache and cache[cache_key].get("hash") == h: | ||
| rel = src_path.relative_to(DOCS_ROOT) | ||
| out_path = DOCS_ROOT / locale / rel | ||
| if out_path.exists(): | ||
| return True, "cached" |
| # Links that appear in both source and target unchanged could mean missing locale rewrite | ||
| unchanged_links = src_internal & tgt_internal | ||
| # Only flag if the link is to a page that is commonly translated (heuristic check) | ||
| commonly_translated = {"/quick-start/next-steps", "/getting-started/installation", |
| # Source uses ../../assets/; locale files should use ../../../assets/ | ||
| if re.search(r'(?<!\.)\.\.\/\.\.\/assets\/', tgt): | ||
| issues.append("Incorrect relative asset path ../../assets/ (should be ../../../assets/ for locale files)") | ||
| score -= 0.2 |
| os.system("pip3 install requests -q") | ||
| import requests | ||
|
|
||
| OLLAMA_BASE = "http://ai-master.taileaa27f.ts.net:11434" |
| parser.add_argument("--dry-run", action="store_true", help="Use cached translations if available") | ||
| parser.add_argument("--no-patch", action="store_true", help="Don't write winning prompt to translate-docs.py") |
| # Apply winning prompt to translate-docs.py (use last iteration's prompt as winner) | ||
| # The winning prompt is the one from the best scoring iteration | ||
| # We need to find which prompt object corresponds to best_idx | ||
| # Prompts are: iter 0 = V0, iter 1 = V1, iter 2 = V2 | ||
| # The prompt used in iteration i is saved at prompt_v{i}.txt (for i>0) or is PROMPT_V0 | ||
| if not args.no_patch and best_idx > 0: | ||
| winning_prompt_path = WORK_DIR / locale / f"prompt_v{best_idx}.txt" | ||
| if winning_prompt_path.exists(): | ||
| winning_prompt = { | ||
| "system": winning_prompt_path.read_text(encoding="utf-8"), | ||
| "user": PROMPT_V0["user"], | ||
| } | ||
| print(f"\nApplying winning prompt ({best_name}) to translate-docs.py...", flush=True) | ||
| apply_winning_prompt(winning_prompt) |
…model to GLM-5-Turbo The ZAI_BASE default was https://api.z.ai/v1 (returns 404). Correct endpoint per docs.z.ai is https://api.z.ai/api/paas/v4. Also update default model from z1-mini to GLM-5-Turbo. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
docs/scripts/translate-qa.py (1)
278-288:⚠️ Potential issue | 🟠 Major | ⚡ Quick winExtra or missing d2 labels still bypass this check.
When the label lists differ only by count,
zip(src_labels, tgt_labels)truncates to the shared prefix andchangedstays empty, so no issue is recorded. That leaves added/removed diagram labels unpenalized.Suggested fix
for i, (sd, td) in enumerate(zip(src_d2_blocks, tgt_d2_blocks)): src_labels = re.findall(r'"([^"]+)"', sd) tgt_labels = re.findall(r'"([^"]+)"', td) if src_labels != tgt_labels: - changed = [(s, t) for s, t in zip(src_labels, tgt_labels) if s != t] - if changed: - issues.append( - f"d2 diagram string labels translated in block {i+1}: " - + ", ".join(f'"{s}" → "{t}"' for s, t in changed[:3]) - ) - score -= 0.15 + compare_len = min(len(src_labels), len(tgt_labels)) + changed = [ + (s, t) + for s, t in zip(src_labels[:compare_len], tgt_labels[:compare_len]) + if s != t + ] + details = [f'"{s}" → "{t}"' for s, t in changed[:3]] + if len(src_labels) != len(tgt_labels): + details.append(f"label count {len(src_labels)} → {len(tgt_labels)}") + issues.append( + f"d2 diagram string labels modified in block {i+1}: " + + ", ".join(details) + ) + score -= 0.15🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/scripts/translate-qa.py` around lines 278 - 288, The current d2 label check (inside the loop over src_d2_blocks/tgt_d2_blocks using src_labels, tgt_labels and zip) misses added/removed labels because zip truncates to the shorter list; update the logic in that loop to detect length mismatches and report extra/missing labels: after computing changed from pairwise comparisons, also compute any trailing/unpaired labels by checking len(src_labels) vs len(tgt_labels) and collecting extra_src = src_labels[len(tgt_labels):] and extra_tgt = tgt_labels[len(src_labels):] (or use set/list difference) and include those in the issues message (same block i+1) and apply the same penalty to score; keep the existing changed handling and message formatting when combining pairwise changes with added/removed labels.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/scripts/translate-qa.py`:
- Around line 61-68: The extract_frontmatter function incorrectly finds any
subsequent '---' anywhere; change the closing delimiter detection to only match
a triple-dash that is anchored to its own line (i.e., preceded and followed by a
line boundary) rather than using content.find("---", 3); update the search to
use a regex or line-aware search that looks for a newline + '---' + newline (or
start/end-of-line anchors) so YAML values containing '---' won't prematurely
terminate the frontmatter; ensure the function still returns the trimmed
frontmatter and body as before.
- Around line 366-418: The script prints status, warnings and the summary table
to stdout even when args.json is set, making --json output invalid; change those
non-JSON prints to either go to stderr (use file=sys.stderr on print calls) or
wrap them behind "if not args.json:" so only the json.dumps(all_results, ...)
stays on stdout; update the initial warning about ZAI_API_KEY, the QA Scoring
Results header/table loop prints, the final "QA results saved to" line, and any
other prints surrounding the JSON output (references: args.json, the various
print(...) calls in the main block, json.dumps(all_results, ...), QA_DIR, and
combined_path write) so that JSON mode emits only the JSON to stdout.
- Around line 17-21: Remove the top-level auto-install of requests and instead
perform a lazy import inside the ai_verify_translation() function: delete the
try/except that runs os.system("pip3 install requests -q") near the top of
translate-qa.py, and add a local import (with a clear ImportError message) at
the start of ai_verify_translation() so requests is only required when
--ai-verify is used and no environment mutation occurs.
---
Duplicate comments:
In `@docs/scripts/translate-qa.py`:
- Around line 278-288: The current d2 label check (inside the loop over
src_d2_blocks/tgt_d2_blocks using src_labels, tgt_labels and zip) misses
added/removed labels because zip truncates to the shorter list; update the logic
in that loop to detect length mismatches and report extra/missing labels: after
computing changed from pairwise comparisons, also compute any trailing/unpaired
labels by checking len(src_labels) vs len(tgt_labels) and collecting extra_src =
src_labels[len(tgt_labels):] and extra_tgt = tgt_labels[len(src_labels):] (or
use set/list difference) and include those in the issues message (same block
i+1) and apply the same penalty to score; keep the existing changed handling and
message formatting when combining pairwise changes with added/removed labels.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 7ed68758-b7c8-4e19-812e-6cd2d398318f
📒 Files selected for processing (1)
docs/scripts/translate-qa.py
| try: | ||
| import requests | ||
| except ImportError: | ||
| os.system("pip3 install requests -q") | ||
| import requests |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n docs/scripts/translate-qa.py | head -50Repository: wailsapp/wails
Length of output: 1714
🏁 Script executed:
# Check for requirements files in the docs directory
find docs -name "requirements*.txt" -o -name "setup.py" -o -name "pyproject.toml" | head -20
# Check the rest of the script to see if requests is actually used
grep -n "requests" docs/scripts/translate-qa.py | head -20Repository: wailsapp/wails
Length of output: 183
🏁 Script executed:
# Check context around line 126 where requests is used
sed -n '120,135p' docs/scripts/translate-qa.py
# Look for CI configuration and how this script is invoked
find . -name "*.yml" -o -name "*.yaml" | xargs grep -l "translate-qa" 2>/dev/null | head -5
# Check for README or setup docs in docs directory
ls -la docs/ | grep -i readmeRepository: wailsapp/wails
Length of output: 694
🏁 Script executed:
# Check the docs README for instructions
cat docs/README.md
# Look for any CI files that might call this script
find . -name ".github" -type d 2>/dev/null | head -1 | xargs find | grep -E "\.(yml|yaml)$" | head -10Repository: wailsapp/wails
Length of output: 2770
🏁 Script executed:
# Check the build and test workflow to see if translate-qa is invoked
cat .github/workflows/build-and-test-v3.yml | grep -A5 -B5 "translate-qa"
# Also check for any documentation on running this script
grep -r "translate-qa" docs/ --include="*.md" 2>/dev/null
# Check the full beginning of the script to understand its purpose and any setup instructions
head -100 docs/scripts/translate-qa.py | tail -50Repository: wailsapp/wails
Length of output: 1966
🏁 Script executed:
# Check if there's setup documentation or requirements in the repo root
cat README.md 2>/dev/null | head -100
# Look for any requirements or setup docs that mention this script
grep -r "translate-qa\|translation.*QA" . --include="*.md" --include="*.txt" 2>/dev/null | grep -v ".git"
# Check if requests is actually optional (only used with --ai-verify flag)
sed -n '1,50p' docs/scripts/translate-qa.py && echo "..." && grep -n "ZAI_API_KEY\|ai.verify\|--ai" docs/scripts/translate-qa.py | head -10Repository: wailsapp/wails
Length of output: 6175
Do not auto-install requests via os.system().
Calling pip3 here mutates the Python environment, is non-reproducible, can target a different interpreter than the current process, and masks a missing dependency with side effects instead of a clear error. Since requests is only used with --ai-verify, keep it optional but import it lazily inside the function that uses it.
Suggested fix
-try:
- import requests
-except ImportError:
- os.system("pip3 install requests -q")
- import requestsThen in ai_verify_translation() at line 86, add a lazy import:
def ai_verify_translation(src_body: str, tgt_body: str, locale: str, lang_name: str, model: str) -> dict:
"""..."""
+ try:
+ import requests
+ except ImportError:
+ return {"score": None, "issues": ["requests library required for AI verification. Install with: pip install requests"], "ai_used": False}🧰 Tools
🪛 Ruff (0.15.12)
[error] 20-20: Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without shell
(S605)
[error] 20-20: Starting a process with a partial executable path
(S607)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/scripts/translate-qa.py` around lines 17 - 21, Remove the top-level
auto-install of requests and instead perform a lazy import inside the
ai_verify_translation() function: delete the try/except that runs
os.system("pip3 install requests -q") near the top of translate-qa.py, and add a
local import (with a clear ImportError message) at the start of
ai_verify_translation() so requests is only required when --ai-verify is used
and no environment mutation occurs.
| def extract_frontmatter(content: str) -> tuple[str, str]: | ||
| """Split content into frontmatter and body.""" | ||
| if not content.startswith("---"): | ||
| return "", content | ||
| end = content.find("---", 3) | ||
| if end == -1: | ||
| return "", content | ||
| return content[3:end].strip(), content[end+3:].strip() |
There was a problem hiding this comment.
Anchor the closing frontmatter delimiter to its own line.
content.find("---", 3) matches the next triple-dash anywhere in the file, so a --- inside a YAML value can split the document early. That cascades into false frontmatter/body parsing for several later checks.
Suggested fix
def extract_frontmatter(content: str) -> tuple[str, str]:
"""Split content into frontmatter and body."""
- if not content.startswith("---"):
+ if not content.startswith("---\n"):
return "", content
- end = content.find("---", 3)
- if end == -1:
+ match = re.match(r"^---\s*\n(.*?)\n---\s*\n?(.*)$", content, re.DOTALL)
+ if not match:
return "", content
- return content[3:end].strip(), content[end+3:].strip()
+ return match.group(1).strip(), match.group(2).strip()🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/scripts/translate-qa.py` around lines 61 - 68, The extract_frontmatter
function incorrectly finds any subsequent '---' anywhere; change the closing
delimiter detection to only match a triple-dash that is anchored to its own line
(i.e., preceded and followed by a line boundary) rather than using
content.find("---", 3); update the search to use a regex or line-aware search
that looks for a newline + '---' + newline (or start/end-of-line anchors) so
YAML values containing '---' won't prematurely terminate the frontmatter; ensure
the function still returns the trimmed frontmatter and body as before.
| if args.ai_verify and not ZAI_API_KEY: | ||
| print("⚠ --ai-verify requires ZAI_API_KEY environment variable to be set.", flush=True) | ||
| print(f" Set it with: export ZAI_API_KEY=<your-key>", flush=True) | ||
| print(f" Using heuristic scoring only.", flush=True) | ||
|
|
||
| if args.locale == "all": | ||
| locales = ALL_LOCALES | ||
| else: | ||
| locales = [l.strip() for l in args.locale.split(",")] | ||
|
|
||
| all_results = {} | ||
| ai_label = f" (+ z.ai/{args.ai_model})" if args.ai_verify and ZAI_API_KEY else "" | ||
| print(f"\nQA Scoring Results{ai_label}", flush=True) | ||
| print("="*70, flush=True) | ||
| print(f"{'Locale':<10} {'Avg Score':>10} {'Files':>7} {'Low Quality':>12}", flush=True) | ||
| print("-"*70, flush=True) | ||
|
|
||
| for locale in locales: | ||
| result = score_locale(locale, ai_verify=args.ai_verify, ai_model=args.ai_model) | ||
| all_results[locale] = result | ||
| if "error" in result: | ||
| print(f"{locale:<10} {'ERROR':>10} {result['error']}", flush=True) | ||
| else: | ||
| flag = " ⚠" if result["low_quality_count"] > 0 else " ✓" | ||
| print( | ||
| f"{locale:<10} {result['avg_score']:>10.3f} {result['files_scored']:>7} {result['low_quality_count']:>12}{flag}", | ||
| flush=True | ||
| ) | ||
| if result["low_quality"]: | ||
| for lq in result["low_quality"]: | ||
| ai_note = f" [AI: {lq['ai_score']:.3f}]" if lq.get("ai_score") is not None else "" | ||
| print(f" → LOW ({lq['score']:.3f}{ai_note}): {lq['file']}", flush=True) | ||
| for issue in lq["issues"]: | ||
| print(f" - {issue}", flush=True) | ||
|
|
||
| print("="*70, flush=True) | ||
|
|
||
| if args.json: | ||
| print(json.dumps(all_results, indent=2)) | ||
|
|
||
| import datetime | ||
| today = datetime.date.today().strftime("%Y-%m-%d") | ||
|
|
||
| # Save per-locale reports to .translation-qa/ | ||
| QA_DIR.mkdir(exist_ok=True) | ||
| for locale, result in all_results.items(): | ||
| per_locale_path = QA_DIR / f"{locale}-{today}.json" | ||
| per_locale_path.write_text(json.dumps(result, indent=2, ensure_ascii=False)) | ||
|
|
||
| # Save combined results | ||
| combined_path = QA_DIR / f"all-{today}.json" | ||
| combined_path.write_text(json.dumps(all_results, indent=2, ensure_ascii=False)) | ||
| print(f"\nQA results saved to: {QA_DIR}/", flush=True) |
There was a problem hiding this comment.
--json currently emits non-JSON data on stdout.
The warning, summary table, and final “saved to” line are always printed, so --json cannot be piped into jq or another parser reliably. Keep status output on stderr or suppress it when JSON mode is enabled.
Suggested fix
- if args.ai_verify and not ZAI_API_KEY:
- print("⚠ --ai-verify requires ZAI_API_KEY environment variable to be set.", flush=True)
- print(f" Set it with: export ZAI_API_KEY=<your-key>", flush=True)
- print(f" Using heuristic scoring only.", flush=True)
+ status_stream = sys.stderr if args.json else sys.stdout
+ if args.ai_verify and not ZAI_API_KEY:
+ print("⚠ --ai-verify requires ZAI_API_KEY environment variable to be set.", flush=True, file=status_stream)
+ print(" Set it with: export ZAI_API_KEY=<your-key>", flush=True, file=status_stream)
+ print(" Using heuristic scoring only.", flush=True, file=status_stream)
@@
- print(f"\nQA Scoring Results{ai_label}", flush=True)
- print("="*70, flush=True)
- print(f"{'Locale':<10} {'Avg Score':>10} {'Files':>7} {'Low Quality':>12}", flush=True)
- print("-"*70, flush=True)
+ print(f"\nQA Scoring Results{ai_label}", flush=True, file=status_stream)
+ print("="*70, flush=True, file=status_stream)
+ print(f"{'Locale':<10} {'Avg Score':>10} {'Files':>7} {'Low Quality':>12}", flush=True, file=status_stream)
+ print("-"*70, flush=True, file=status_stream)
@@
- print(f"{locale:<10} {'ERROR':>10} {result['error']}", flush=True)
+ print(f"{locale:<10} {'ERROR':>10} {result['error']}", flush=True, file=status_stream)
@@
- flush=True
+ flush=True,
+ file=status_stream
)
@@
- print(f" → LOW ({lq['score']:.3f}{ai_note}): {lq['file']}", flush=True)
+ print(f" → LOW ({lq['score']:.3f}{ai_note}): {lq['file']}", flush=True, file=status_stream)
for issue in lq["issues"]:
- print(f" - {issue}", flush=True)
+ print(f" - {issue}", flush=True, file=status_stream)
- print("="*70, flush=True)
+ print("="*70, flush=True, file=status_stream)
@@
- print(f"\nQA results saved to: {QA_DIR}/", flush=True)
+ print(f"\nQA results saved to: {QA_DIR}/", flush=True, file=status_stream)🧰 Tools
🪛 Ruff (0.15.12)
[error] 368-368: f-string without any placeholders
Remove extraneous f prefix
(F541)
[error] 369-369: f-string without any placeholders
Remove extraneous f prefix
(F541)
[error] 374-374: Ambiguous variable name: l
(E741)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/scripts/translate-qa.py` around lines 366 - 418, The script prints
status, warnings and the summary table to stdout even when args.json is set,
making --json output invalid; change those non-JSON prints to either go to
stderr (use file=sys.stderr on print calls) or wrap them behind "if not
args.json:" so only the json.dumps(all_results, ...) stays on stdout; update the
initial warning about ZAI_API_KEY, the QA Scoring Results header/table loop
prints, the final "QA results saved to" line, and any other prints surrounding
the JSON output (references: args.json, the various print(...) calls in the main
block, json.dumps(all_results, ...), QA_DIR, and combined_path write) so that
JSON mode emits only the JSON to stdout.
…ens for GLM-5-Turbo - Switch ZAI_BASE from general paas/v4 to the dedicated coding plan endpoint (https://api.z.ai/api/coding/paas/v4) as required by the Coding Plan subscription - Increase max_tokens from 512 to 2000 — GLM-5-Turbo is a thinking model that consumes ~300 reasoning tokens internally before producing output, causing empty responses with the previous limit Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
docs/scripts/translate-qa.py (1)
142-147: ⚡ Quick winBrittle code-fence stripping for AI responses.
raw.split("\n")followed bylines[1:-1]only works when the model returns the fenced JSON spread across at least three lines. Single-line responses like```json {"score":0.9} ```, or fences without a trailing newline, collapse to an empty payload and bubble up as a generic[AI] Verification error: ...via the broadexcept, so the failure mode looks like a network/API problem rather than a parser problem. A small regex-based strip is more robust and preserves diagnostics.♻️ Proposed refactor
raw = resp.json()["choices"][0]["message"]["content"].strip() - # Strip markdown fences if model wraps in ```json ... ``` - if raw.startswith("```"): - lines = raw.split("\n") - raw = "\n".join(lines[1:-1]) + # Strip markdown fences if model wraps in ```json ... ``` + fence = re.match(r"^```(?:json)?\s*(.*?)\s*```$", raw, re.DOTALL) + if fence: + raw = fence.group(1) result = json.loads(raw)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/scripts/translate-qa.py` around lines 142 - 147, The current brittle fence-stripping in translate-qa.py uses raw.split("\n") and lines[1:-1], which fails for single-line or no-newline fences; replace that block (where raw is assigned and before json.loads(raw)) with a regex-based extractor: use re.match(r"^```(?:json)?\s*(.*?)\s*```$", raw, re.DOTALL) to capture the inner payload and set raw = match.group(1) when matched, otherwise leave raw unchanged so json.loads(result) can produce useful parse errors; update the code path that produces result = json.loads(raw) to run after this robust strip.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/scripts/translate-qa.py`:
- Around line 330-338: Currently orphaned translated files are silently skipped;
change the loop that iterates tgt_path (using variables results, tgt_path, rel,
src_path and calling check_file_pair) so that when src_path.exists() is False
you do not continue but instead append a result entry for that rel marking it as
an orphaned translation (e.g., set result["file"]=str(rel), result["score"]=0.0
and add an issue like "orphaned translation" or similar metadata), so the locale
QA report includes per-file entries for removed/renamed English sources;
preserve the same result shape as check_file_pair outputs so downstream
consumers can handle it.
---
Nitpick comments:
In `@docs/scripts/translate-qa.py`:
- Around line 142-147: The current brittle fence-stripping in translate-qa.py
uses raw.split("\n") and lines[1:-1], which fails for single-line or no-newline
fences; replace that block (where raw is assigned and before json.loads(raw))
with a regex-based extractor: use re.match(r"^```(?:json)?\s*(.*?)\s*```$", raw,
re.DOTALL) to capture the inner payload and set raw = match.group(1) when
matched, otherwise leave raw unchanged so json.loads(result) can produce useful
parse errors; update the code path that produces result = json.loads(raw) to run
after this robust strip.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: cce2277f-4463-47a7-8a18-7964cefbc375
📒 Files selected for processing (1)
docs/scripts/translate-qa.py
| results = [] | ||
| for tgt_path in sorted(files): | ||
| rel = tgt_path.relative_to(locale_dir) | ||
| src_path = DOCS_ROOT / rel | ||
| if not src_path.exists(): | ||
| continue | ||
| result = check_file_pair(src_path, tgt_path, locale, ai_verify=ai_verify, ai_model=ai_model) | ||
| result["file"] = str(rel) | ||
| results.append(result) |
There was a problem hiding this comment.
Stale/orphaned translations are silently skipped instead of being reported.
When tgt_path (a translated file) exists but the corresponding English source has been removed or renamed, the file is dropped from the QA report with no warning. That's exactly the case a QA tool should surface — orphaned localized content drifts out of sync without anyone noticing. At minimum, record an issue for the locale (or attach a per-file entry with score: 0.0 and an "orphaned translation" issue) so the report reflects reality.
🛡️ Proposed fix
results = []
+ orphaned = []
for tgt_path in sorted(files):
rel = tgt_path.relative_to(locale_dir)
src_path = DOCS_ROOT / rel
if not src_path.exists():
+ orphaned.append(str(rel))
continue
result = check_file_pair(src_path, tgt_path, locale, ai_verify=ai_verify, ai_model=ai_model)
result["file"] = str(rel)
results.append(result)
+
+ for rel in orphaned:
+ results.append({
+ "file": rel,
+ "score": 0.0,
+ "heuristic_score": 0.0,
+ "ai_score": None,
+ "ai_used": False,
+ "issues": ["Orphaned translation: source file no longer exists"],
+ })🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/scripts/translate-qa.py` around lines 330 - 338, Currently orphaned
translated files are silently skipped; change the loop that iterates tgt_path
(using variables results, tgt_path, rel, src_path and calling check_file_pair)
so that when src_path.exists() is False you do not continue but instead append a
result entry for that rel marking it as an orphaned translation (e.g., set
result["file"]=str(rel), result["score"]=0.0 and add an issue like "orphaned
translation" or similar metadata), so the locale QA report includes per-file
entries for removed/renamed English sources; preserve the same result shape as
check_file_pair outputs so downstream consumers can handle it.
…iles Split documents > 8000 chars at paragraph/heading boundaries and translate each chunk independently. After each chunk, ask the model for a 2-3 sentence context summary (topics covered, terminology choices) and inject it into the next chunk's prompt, keeping terminology consistent across boundaries. This addresses two issues: - Hallucination at truncated boundaries: models were completing sentences/ paragraphs not in the source when they received a truncated view - Terminology drift: without context, later chunks sometimes used different terms for the same concept (e.g. alternating synonyms) Changes: - split_into_chunks(): split at \n\n## headings or \n\n paragraph breaks, never inside code blocks (tracks ``` fence parity) - summarize_chunk(): non-streaming Ollama call for a 2-3 sentence context note - strip_frontmatter(): removes spurious --- blocks from continuation chunks - translate_doc(): orchestrates split → translate → summarize → reassemble - Prompt: removed --- content delimiters (collided with MDX frontmatter syntax, causing model to include the closing --- in its output) - System prompt: added WRONG/CORRECT counter-example for bash comment translation - QA: fixed false positive in asset path check (../../../assets/ substring matched ../../assets/ pattern — now uses string replacement before checking) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (5)
docs/scripts/translate-qa.py (5)
17-21:⚠️ Potential issue | 🟠 Major | ⚡ Quick winDo not mutate the environment just to import
requests.This makes heuristic-only runs non-reproducible and can install into a different interpreter than the current process. Keep
requestsoptional by importing it lazily only when--ai-verifyis used.Suggested fix
-try: - import requests -except ImportError: - os.system("pip3 install requests -q") - import requests @@ def ai_verify_translation(src_body: str, tgt_body: str, locale: str, lang_name: str, model: str) -> dict: @@ + try: + import requests + except ImportError: + return { + "score": None, + "issues": ["requests is required for --ai-verify. Install it in the current environment first."], + "ai_used": False, + } + if not ZAI_API_KEY: return {"score": None, "issues": ["ZAI_API_KEY not set — skipping AI verification"], "ai_used": False}#!/bin/bash rg -n --type=py '^\s*(try:|import requests|os\.system|def ai_verify_translation|requests\.)' docs/scripts/translate-qa.pyAlso applies to: 86-93
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/scripts/translate-qa.py` around lines 17 - 21, Replace the top-level try/except that runs os.system to install requests (the block currently trying to import requests) with no environment mutation; remove the auto-install lines and instead perform a lazy import of requests inside the code path used only for AI verification (move the import into the ai_verify_translation function or the branch that runs when the --ai-verify flag is parsed), wrapping that local import in a try/except ImportError that raises a clear message asking the user to pip install requests; also apply the same change for the later occurrence around lines 86-93 so no code path auto-installs or mutates the interpreter environment.
61-68:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAnchor the closing frontmatter delimiter to its own line.
content.find("---", 3)will stop at the next triple-dash anywhere in the file, including inside a YAML value. That can poison the later frontmatter-based checks with a bad split.Suggested fix
def extract_frontmatter(content: str) -> tuple[str, str]: """Split content into frontmatter and body.""" - if not content.startswith("---"): - return "", content - end = content.find("---", 3) - if end == -1: + match = re.match(r'^---\s*\n(.*?)\n---\s*\n?(.*)$', content, re.DOTALL) + if not match: return "", content - return content[3:end].strip(), content[end+3:].strip() + return match.group(1).strip(), match.group(2).strip()🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/scripts/translate-qa.py` around lines 61 - 68, The current extract_frontmatter uses content.find("---", 3) which matches triple-dashes anywhere; instead locate the closing frontmatter delimiter that is on its own line. Update extract_frontmatter to use a regex search (e.g., re.search with the (?m) multiline flag for a line that matches only ^\s*---\s*$ starting after the opening '---') and use the match span to split the frontmatter and body; keep the same return shapes and strip() calls and handle CRLF by allowing optional surrounding whitespace.
332-340:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winSurface orphaned locale files instead of silently skipping them.
Skipping translated files whose English source no longer exists hides exactly the drift this QA script is supposed to report. Those should show up as failing entries in the locale report.
Suggested fix
results = [] for tgt_path in sorted(files): rel = tgt_path.relative_to(locale_dir) src_path = DOCS_ROOT / rel if not src_path.exists(): + results.append({ + "file": str(rel), + "score": 0.0, + "heuristic_score": 0.0, + "ai_score": None, + "ai_used": False, + "issues": ["Orphaned translation: source file no longer exists"], + }) continue result = check_file_pair(src_path, tgt_path, locale, ai_verify=ai_verify, ai_model=ai_model) result["file"] = str(rel) results.append(result)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/scripts/translate-qa.py` around lines 332 - 340, The loop currently skips translated files when their English source (src_path) is missing; change that so orphaned files produce a failing entry in results instead of being silently ignored: inside the for tgt_path in sorted(files) loop, replace the if not src_path.exists(): continue with construction of a result object (set result["file"]=str(rel) and add fields like result["status"]="orphaned" and result["error"]="source_missing" or similar) and append it to results so the locale report surfaces the orphan; keep using the same results list and the check_file_pair(result) pattern for non-orphaned files.
368-420:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
--jsonmode still writes non-JSON data to stdout.The warning, summary table, and final “saved to” line are printed even when
--jsonis enabled, so piping this output intojqor another parser will break. Route status output tostderror suppress it in JSON mode.Suggested fix
+ status_stream = sys.stderr if args.json else sys.stdout + if args.ai_verify and not ZAI_API_KEY: - print("⚠ --ai-verify requires ZAI_API_KEY environment variable to be set.", flush=True) - print(f" Set it with: export ZAI_API_KEY=<your-key>", flush=True) - print(f" Using heuristic scoring only.", flush=True) + print("⚠ --ai-verify requires ZAI_API_KEY environment variable to be set.", flush=True, file=status_stream) + print(" Set it with: export ZAI_API_KEY=<your-key>", flush=True, file=status_stream) + print(" Using heuristic scoring only.", flush=True, file=status_stream) @@ - print(f"\nQA Scoring Results{ai_label}", flush=True) - print("="*70, flush=True) - print(f"{'Locale':<10} {'Avg Score':>10} {'Files':>7} {'Low Quality':>12}", flush=True) - print("-"*70, flush=True) + print(f"\nQA Scoring Results{ai_label}", flush=True, file=status_stream) + print("="*70, flush=True, file=status_stream) + print(f"{'Locale':<10} {'Avg Score':>10} {'Files':>7} {'Low Quality':>12}", flush=True, file=status_stream) + print("-"*70, flush=True, file=status_stream) @@ - print(f"{locale:<10} {'ERROR':>10} {result['error']}", flush=True) + print(f"{locale:<10} {'ERROR':>10} {result['error']}", flush=True, file=status_stream) @@ - flush=True + flush=True, + file=status_stream ) @@ - print(f" → LOW ({lq['score']:.3f}{ai_note}): {lq['file']}", flush=True) + print(f" → LOW ({lq['score']:.3f}{ai_note}): {lq['file']}", flush=True, file=status_stream) for issue in lq["issues"]: - print(f" - {issue}", flush=True) + print(f" - {issue}", flush=True, file=status_stream) @@ - print("="*70, flush=True) + print("="*70, flush=True, file=status_stream) @@ - print(f"\nQA results saved to: {QA_DIR}/", flush=True) + print(f"\nQA results saved to: {QA_DIR}/", flush=True, file=status_stream)🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/scripts/translate-qa.py` around lines 368 - 420, The script currently always prints human-readable status and summary to stdout even when args.json is true; change those prints (the header/status/table prints, per-locale human lines, low-quality details, the final "QA results saved" line and the separator prints around them) to either be suppressed when args.json is set or redirected to stderr by adding file=sys.stderr to those print() calls (leave the json.dumps(all_results, indent=2) output on stdout). Locate prints in the main reporting block (the code that builds ai_label, prints "QA Scoring Results", the table rows inside the locales loop, the "=" separators, and the final saved-to print) and update them accordingly so only the JSON output is emitted to stdout when args.json is true.
280-290:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winThe d2-label check still misses add/remove/reorder cases.
zip(src_labels, tgt_labels)truncates to the shorter list, so extra or missing labels are invisible here. Reordered labels can also makesrc_labels != tgt_labelstrue while leavingchangedempty, which suppresses the issue entirely.Suggested fix
for i, (sd, td) in enumerate(zip(src_d2_blocks, tgt_d2_blocks)): src_labels = re.findall(r'"([^"]+)"', sd) tgt_labels = re.findall(r'"([^"]+)"', td) if src_labels != tgt_labels: changed = [(s, t) for s, t in zip(src_labels, tgt_labels) if s != t] - if changed: - issues.append( - f"d2 diagram string labels translated in block {i+1}: " - + ", ".join(f'"{s}" → "{t}"' for s, t in changed[:3]) - ) - score -= 0.15 + detail = [] + if changed: + detail.append(", ".join(f'"{s}" → "{t}"' for s, t in changed[:3])) + if len(src_labels) != len(tgt_labels): + detail.append(f"label count {len(src_labels)} → {len(tgt_labels)}") + if not detail: + detail.append("labels were reordered or replaced") + issues.append( + f"d2 diagram string labels modified in block {i+1}: " + "; ".join(detail) + ) + score -= 0.15🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/scripts/translate-qa.py` around lines 280 - 290, The current d2-label check uses zip(src_labels, tgt_labels) which truncates to the shorter list and misses added/removed labels and reorder-only cases; update the loop that compares src_labels and tgt_labels so it (1) iterates over the full range (max(len(src_labels), len(tgt_labels))) rather than zip to detect extra/missing labels, (2) build a list of per-index differences including added (present only in tgt) and removed (present only in src) entries, and (3) detect reorder-only changes by checking if set(src_labels) == set(tgt_labels) but src_labels != tgt_labels and append a specific "reordered labels" issue; update the issues.append message to include added/removed/reordered detail and keep the score penalty logic on these new detections (variables: src_d2_blocks, tgt_d2_blocks, src_labels, tgt_labels, changed, issues, score).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/scripts/translate-docs.py`:
- Around line 490-498: The cache currently keys only on file content
(file_hash(content)) so changes to SYSTEM_PROMPT, MODEL, or rewrite rules are
ignored; update the cache key/validation to include a fingerprint of the
translation configuration (e.g., SYSTEM_PROMPT, MODEL, and any
rewrite_rules/version) alongside file_hash(content) so cached entries are
invalidated when those change. Locate the cache check and write sites around
cache_key/file_hash(content) (same block using variables cache_key, h, and where
cache[...] is set — also the analogous logic at the second occurrence near line
515) and modify them to compute and persist a combined signature (content_hash +
config_hash) and compare that combined signature when deciding to return
"cached".
- Around line 290-353: The post-processing regexes (make_locale_path +
rewrite_markdown_link, rewrite_fm_link, rewrite_href) run over the whole MDX and
accidentally rewrite fenced code blocks and inline code; update the logic to
skip code by first locating and temporarily masking fenced code blocks
(```...```) and inline code spans (`...`) from content before running the three
re.sub calls, then restore the masked blocks afterwards so only
prose/frontmatter/JSX is rewritten; keep the existing regexes and functions
(make_locale_path, rewrite_markdown_link, rewrite_fm_link, rewrite_href) but
apply them to the masked content to prevent mutating examples and code snippets.
- Around line 433-436: The current cleanup unconditionally finds the first "---"
in result and discards everything before it (using result.index("---")), which
can drop legitimate translated content; change this so the trim only runs when
the original source chunk actually starts with frontmatter—i.e., guard the block
with a check like "if source_chunk.startswith('---') and not
result.startswith('---') and '---' in result:" (use whatever variable name holds
the original chunk) and then perform the index/trim on result; reference the
existing "result" variable and the original source chunk variable when making
the change.
- Around line 18-23: The current try/except that auto-installs requests at
import time should be removed; instead, keep a plain import requests and on
ImportError raise a clear, non-mutating error (e.g. re-raise ImportError with a
helpful message) rather than calling os.system to pip install. Locate the import
block that references requests and os.system("pip3 install requests -q") and
replace it with a simple import requests wrapped in an except ImportError that
raises a descriptive error telling the user to install requests (or add it to
requirements), so the script fails fast without mutating the caller's
environment.
- Around line 177-197: The current split logic can still split inside a fenced
code block when in_code is true; modify the branch that runs when in_code is
true to locate the end of the fenced block instead of falling through: after
computing in_code (using pre/count logic), search forward from lo for the next
fence delimiter (e.g. a line starting with ```), if no fence delimiter is found
set split_pos to len(content), otherwise advance split_pos to the first newline
after that fence delimiter (so the split occurs after the closing fence); keep
the existing fallback that handles split_pos is None (last newline before hi)
only for the non-code case.
---
Duplicate comments:
In `@docs/scripts/translate-qa.py`:
- Around line 17-21: Replace the top-level try/except that runs os.system to
install requests (the block currently trying to import requests) with no
environment mutation; remove the auto-install lines and instead perform a lazy
import of requests inside the code path used only for AI verification (move the
import into the ai_verify_translation function or the branch that runs when the
--ai-verify flag is parsed), wrapping that local import in a try/except
ImportError that raises a clear message asking the user to pip install requests;
also apply the same change for the later occurrence around lines 86-93 so no
code path auto-installs or mutates the interpreter environment.
- Around line 61-68: The current extract_frontmatter uses content.find("---", 3)
which matches triple-dashes anywhere; instead locate the closing frontmatter
delimiter that is on its own line. Update extract_frontmatter to use a regex
search (e.g., re.search with the (?m) multiline flag for a line that matches
only ^\s*---\s*$ starting after the opening '---') and use the match span to
split the frontmatter and body; keep the same return shapes and strip() calls
and handle CRLF by allowing optional surrounding whitespace.
- Around line 332-340: The loop currently skips translated files when their
English source (src_path) is missing; change that so orphaned files produce a
failing entry in results instead of being silently ignored: inside the for
tgt_path in sorted(files) loop, replace the if not src_path.exists(): continue
with construction of a result object (set result["file"]=str(rel) and add fields
like result["status"]="orphaned" and result["error"]="source_missing" or
similar) and append it to results so the locale report surfaces the orphan; keep
using the same results list and the check_file_pair(result) pattern for
non-orphaned files.
- Around line 368-420: The script currently always prints human-readable status
and summary to stdout even when args.json is true; change those prints (the
header/status/table prints, per-locale human lines, low-quality details, the
final "QA results saved" line and the separator prints around them) to either be
suppressed when args.json is set or redirected to stderr by adding
file=sys.stderr to those print() calls (leave the json.dumps(all_results,
indent=2) output on stdout). Locate prints in the main reporting block (the code
that builds ai_label, prints "QA Scoring Results", the table rows inside the
locales loop, the "=" separators, and the final saved-to print) and update them
accordingly so only the JSON output is emitted to stdout when args.json is true.
- Around line 280-290: The current d2-label check uses zip(src_labels,
tgt_labels) which truncates to the shorter list and misses added/removed labels
and reorder-only cases; update the loop that compares src_labels and tgt_labels
so it (1) iterates over the full range (max(len(src_labels), len(tgt_labels)))
rather than zip to detect extra/missing labels, (2) build a list of per-index
differences including added (present only in tgt) and removed (present only in
src) entries, and (3) detect reorder-only changes by checking if set(src_labels)
== set(tgt_labels) but src_labels != tgt_labels and append a specific "reordered
labels" issue; update the issues.append message to include
added/removed/reordered detail and keep the score penalty logic on these new
detections (variables: src_d2_blocks, tgt_d2_blocks, src_labels, tgt_labels,
changed, issues, score).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 0214403e-0893-4d97-a06a-963b228baeed
📒 Files selected for processing (2)
docs/scripts/translate-docs.pydocs/scripts/translate-qa.py
| try: | ||
| import requests | ||
| except ImportError: | ||
| print("Installing requests...") | ||
| os.system("pip3 install requests -q") | ||
| import requests |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
rg -n --type=py '^\s*(try:|import requests|os\.system|requests\.)' docs/scripts/translate-docs.pyRepository: wailsapp/wails
Length of output: 221
Do not auto-install requests at import time.
This mutates the caller's Python environment, can install into a different interpreter than the one running the script, and hides a missing dependency behind side effects. Fail fast with a clear error instead.
Suggested fix
try:
import requests
-except ImportError:
- print("Installing requests...")
- os.system("pip3 install requests -q")
- import requests
+except ImportError as exc:
+ raise SystemExit(
+ "This script requires the 'requests' package. "
+ "Install it in the current environment first, e.g. "
+ "`python3 -m pip install requests`."
+ ) from exc🧰 Tools
🪛 Ruff (0.15.12)
[error] 22-22: Starting a process with a shell: seems safe, but may be changed in the future; consider rewriting without shell
(S605)
[error] 22-22: Starting a process with a partial executable path
(S607)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/scripts/translate-docs.py` around lines 18 - 23, The current try/except
that auto-installs requests at import time should be removed; instead, keep a
plain import requests and on ImportError raise a clear, non-mutating error (e.g.
re-raise ImportError with a helpful message) rather than calling os.system to
pip install. Locate the import block that references requests and
os.system("pip3 install requests -q") and replace it with a simple import
requests wrapped in an except ImportError that raises a descriptive error
telling the user to install requests (or add it to requirements), so the script
fails fast without mutating the caller's environment.
| # Rough check: odd number of ``` lines before lo means we're inside a code block | ||
| pre = content[start:lo] | ||
| in_code = pre.count('\n```') % 2 == 1 | ||
|
|
||
| split_pos = None | ||
| if not in_code: | ||
| # Prefer last heading boundary (\n\n## ...) in window | ||
| for m in reversed(list(re.finditer(r'\n\n(?=#+\s)', window))): | ||
| split_pos = lo + m.end() | ||
| break | ||
|
|
||
| if split_pos is None: | ||
| # Fall back to last paragraph break | ||
| last = window.rfind('\n\n') | ||
| if last >= 0: | ||
| split_pos = lo + last + 2 | ||
|
|
||
| if split_pos is None: | ||
| # Last resort: last newline before hi | ||
| last_nl = content.rfind('\n', start, hi) | ||
| split_pos = (last_nl + 1) if last_nl > start else hi |
There was a problem hiding this comment.
The fallback path can still split inside fenced code blocks.
When in_code is true, this skips the heading/paragraph logic and falls through to “last newline before hi”, which is still inside the same fenced block. That can hand the model malformed code fences on large pages.
Suggested fix
- if split_pos is None:
- # Last resort: last newline before hi
- last_nl = content.rfind('\n', start, hi)
- split_pos = (last_nl + 1) if last_nl > start else hi
+ if split_pos is None and in_code:
+ # Never cut a chunk until the current fenced block closes.
+ fence_start = content.find('\n```', lo)
+ if fence_start == -1:
+ split_pos = len(content)
+ else:
+ next_nl = content.find('\n', fence_start + 1)
+ split_pos = len(content) if next_nl == -1 else next_nl + 1
+ elif split_pos is None:
+ # Last resort: last newline before hi
+ last_nl = content.rfind('\n', start, hi)
+ split_pos = (last_nl + 1) if last_nl > start else hi🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/scripts/translate-docs.py` around lines 177 - 197, The current split
logic can still split inside a fenced code block when in_code is true; modify
the branch that runs when in_code is true to locate the end of the fenced block
instead of falling through: after computing in_code (using pre/count logic),
search forward from lo for the next fence delimiter (e.g. a line starting with
```), if no fence delimiter is found set split_pos to len(content), otherwise
advance split_pos to the first newline after that fence delimiter (so the split
occurs after the closing fence); keep the existing fallback that handles
split_pos is None (last newline before hi) only for the non-code case.
| content = content.replace("../../assets/", "../../../assets/") | ||
|
|
||
| # 4 & 5. Link rewriting | ||
| # | ||
| # For each translated path, rewrite: | ||
| # - Markdown links: [text](/path) → [text](/<locale>/path) | ||
| # - Frontmatter link: values: link: /path → link: /<locale>/path | ||
| # - href="/path" in JSX | ||
| # | ||
| # Also apply PATH_REMAP before prefixing: | ||
| # /quick-start/installation → /<locale>/getting-started/installation | ||
| # (because we translate getting-started/installation, not quick-start/installation) | ||
| # | ||
| # ONLY rewrite paths that have a translated version. Leave other root-relative | ||
| # links as-is so they fall back to the English page via Starlight locale fallback. | ||
|
|
||
| def make_locale_path(src_path: str) -> str | None: | ||
| """Return locale-prefixed path if src_path should be rewritten, else None.""" | ||
| # Apply remap first | ||
| remapped = PATH_REMAP.get(src_path, src_path) | ||
| # Check if the remapped path (without extension) is in translated_paths | ||
| # translated_paths contains paths like /quick-start/next-steps (no extension) | ||
| if remapped in translated_paths: | ||
| return f"/{locale}{remapped}" | ||
| return None | ||
|
|
||
| def rewrite_markdown_link(m: re.Match) -> str: | ||
| text, path, suffix = m.group(1), m.group(2), m.group(3) | ||
| new_path = make_locale_path(path) | ||
| if new_path: | ||
| return f"[{text}]({new_path}{suffix if suffix != ')' else ''})" | ||
| return m.group(0) | ||
|
|
||
| # Markdown links: [text](/path) or [text](/path/sub) | ||
| content = re.sub( | ||
| r'\[([^\]]*)\]\((/[a-zA-Z0-9/_-]+)(/?)\)', | ||
| rewrite_markdown_link, | ||
| content | ||
| ) | ||
|
|
||
| # Frontmatter link: /path | ||
| def rewrite_fm_link(m: re.Match) -> str: | ||
| key, path, tail = m.group(1), m.group(2), m.group(3) | ||
| new_path = make_locale_path(path) | ||
| if new_path: | ||
| return f"{key}{new_path}{tail}" | ||
| return m.group(0) | ||
|
|
||
| content = re.sub( | ||
| r'(link:\s*)(/[a-zA-Z0-9/_-]+)(\s*$)', | ||
| rewrite_fm_link, | ||
| content, | ||
| flags=re.MULTILINE | ||
| ) | ||
|
|
||
| # href="/path" in JSX | ||
| def rewrite_href(m: re.Match) -> str: | ||
| path = m.group(1) | ||
| new_path = make_locale_path(path) | ||
| if new_path: | ||
| return f'href="{new_path}"' | ||
| return m.group(0) | ||
|
|
||
| content = re.sub(r'href="(/[a-zA-Z0-9/_-]+)"', rewrite_href, content) |
There was a problem hiding this comment.
Post-processing is rewriting code examples as if they were prose.
These replacements run over the entire MDX string, including fenced code and inline code. Any page that documents href="/...", markdown links, or ../../assets/ examples will have its verbatim code mutated after translation.
Safer direction
- content = content.replace("../../assets/", "../../../assets/")
+ # Rewrite only prose/frontmatter sections. Keep fenced code blocks verbatim.
+ # One straightforward approach is to temporarily mask fenced blocks, apply
+ # rewrites to the remaining text, then restore the original blocks.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/scripts/translate-docs.py` around lines 290 - 353, The post-processing
regexes (make_locale_path + rewrite_markdown_link, rewrite_fm_link,
rewrite_href) run over the whole MDX and accidentally rewrite fenced code blocks
and inline code; update the logic to skip code by first locating and temporarily
masking fenced code blocks (```...```) and inline code spans (`...`) from
content before running the three re.sub calls, then restore the masked blocks
afterwards so only prose/frontmatter/JSX is rewritten; keep the existing regexes
and functions (make_locale_path, rewrite_markdown_link, rewrite_fm_link,
rewrite_href) but apply them to the masked content to prevent mutating examples
and code snippets.
| # Strip any leading "---" explanatory text before the frontmatter | ||
| if not result.startswith("---") and "---" in result: | ||
| idx = result.index("---") | ||
| result = result[idx:] |
There was a problem hiding this comment.
Don’t drop everything before the first --- on non-frontmatter chunks.
For chunks that do not start with frontmatter, any legitimate --- later in the translated content will cause the already-translated prefix to be discarded. This cleanup should only run when the source chunk itself starts with frontmatter.
Suggested fix
- if not result.startswith("---") and "---" in result:
- idx = result.index("---")
- result = result[idx:]
+ if content.startswith("---\n") and not result.startswith("---\n"):
+ match = re.search(r'(?m)^---\s*$', result)
+ if match:
+ result = result[match.start():]🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/scripts/translate-docs.py` around lines 433 - 436, The current cleanup
unconditionally finds the first "---" in result and discards everything before
it (using result.index("---")), which can drop legitimate translated content;
change this so the trim only runs when the original source chunk actually starts
with frontmatter—i.e., guard the block with a check like "if
source_chunk.startswith('---') and not result.startswith('---') and '---' in
result:" (use whatever variable name holds the original chunk) and then perform
the index/trim on result; reference the existing "result" variable and the
original source chunk variable when making the change.
| h = file_hash(content) | ||
| cache_key = str(src_path.relative_to(DOCS_ROOT)) | ||
|
|
||
| # Check cache | ||
| if cache_key in cache and cache[cache_key].get("hash") == h: | ||
| rel = src_path.relative_to(DOCS_ROOT) | ||
| out_path = DOCS_ROOT / locale / rel | ||
| if out_path.exists(): | ||
| return True, "cached" |
There was a problem hiding this comment.
Cache invalidation ignores prompt/model changes.
The cache only keys on source content, so unchanged files stay “cached” even after SYSTEM_PROMPT, MODEL, or rewrite rules change. That defeats the prompt-improvement workflow because the new prompt never gets applied to previously translated files.
Suggested fix
h = file_hash(content)
cache_key = str(src_path.relative_to(DOCS_ROOT))
+ cache_version = file_hash(f"{MODEL}\n{SYSTEM_PROMPT}\n{sorted(PATH_REMAP.items())}")
# Check cache
- if cache_key in cache and cache[cache_key].get("hash") == h:
+ if (
+ cache_key in cache
+ and cache[cache_key].get("hash") == h
+ and cache[cache_key].get("version") == cache_version
+ ):
rel = src_path.relative_to(DOCS_ROOT)
out_path = DOCS_ROOT / locale / rel
if out_path.exists():
return True, "cached"
@@
- cache[cache_key] = {"hash": h, "translated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime())}
+ cache[cache_key] = {
+ "hash": h,
+ "version": cache_version,
+ "translated_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
+ }Also applies to: 515-515
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/scripts/translate-docs.py` around lines 490 - 498, The cache currently
keys only on file content (file_hash(content)) so changes to SYSTEM_PROMPT,
MODEL, or rewrite rules are ignored; update the cache key/validation to include
a fingerprint of the translation configuration (e.g., SYSTEM_PROMPT, MODEL, and
any rewrite_rules/version) alongside file_hash(content) so cached entries are
invalidated when those change. Locate the cache check and write sites around
cache_key/file_hash(content) (same block using variables cache_key, h, and where
cache[...] is set — also the analogous logic at the second occurrence near line
515) and modify them to compute and persist a combined signature (content_hash +
config_hash) and compare that combined signature when deciding to return
"cached".
Adds an alternative translation backend using the z.ai OpenAI-compatible API alongside the existing Ollama/qwen3.6:35b default. Usage: python3 translate-docs.py --locale de --zai-model glm-5.1 python3 translate-docs.py --locale all --zai-model glm-4.7 The z.ai backend reads ZAI_API_KEY and ZAI_BASE from the environment (defaults to the Coding Plan endpoint). GLM-5.1 and GLM-4.7 are both thinking models and work correctly with max_tokens=8000. The same chunking and context-passing logic applies to both backends. Comparison result (2 German test files): both qwen3.6:35b and GLM-5.1 score 1.000 on heuristic checks. GLM-5.1 is ~2x faster (cloud vs local 35B). Minor style difference: GLM-5.1 uses fully-localized German terms while qwen mixes in English loanwords more freely. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
docs/scripts/translate-docs.py (1)
510-513:⚠️ Potential issue | 🟠 Major | ⚡ Quick winSame
---prefix-stripping bug exists in the z.ai backend.This is the identical pattern previously flagged in
translate_with_ollama(lines 440‑443): when the source chunk does not start with frontmatter, any legitimate---later in the translated body causes the entire prefix to be discarded. Apply the same guard here (only strip when the source chunk itself started with frontmatter and the model's output lost it).🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/scripts/translate-docs.py` around lines 510 - 513, The current prefix-stripping logic unconditionally strips up to the first '---' in result, which can remove legitimate content; change it to only perform this repair when the original source chunk actually started with frontmatter and the model's output lost it (same guard used in translate_with_ollama). Concretely, before running the block that checks result.startswith("---") and result.index("---"), consult the original source chunk variable (the input passed to the translation routine) and only apply the idx/result = result[idx:] fix if that source started with '---' but result does not — otherwise leave result untouched.
🧹 Nitpick comments (1)
docs/scripts/translate-docs.py (1)
365-523: 💤 Low valueConsolidate the two backend functions to remove duplication.
translate_with_ollamaandtranslate_with_zaiduplicatesystem/context_block/user_msgconstruction and the post-strip logic (fences + leading----cleanup). Extracting a small helper for prompt assembly and another for trailing fence/---cleanup would (a) keep both backends in lock-step when the system prompt evolves, and (b) ensure that any future fix to the---cleanup (see related comment) only needs to be made once.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@docs/scripts/translate-docs.py` around lines 365 - 523, Both translate_with_ollama and translate_with_zai duplicate prompt construction and post-response cleanup; extract a helper assemble_translate_prompt(system_template: str, chunk_label: str, lang: str, content: str, context_summary: str) that returns (system, user_msg) using SYSTEM_PROMPT and CHUNK_TRANSLATE_PROMPT, and another helper clean_translated_text(result: str) that strips markdown fences and any leading preamble before the frontmatter ("---"). Replace the duplicated blocks in translate_with_ollama and translate_with_zai to call assemble_translate_prompt(...) to build system/user messages and call clean_translated_text(result) before returning; keep existing function names and _translate_chunk dispatch unchanged so integration is seamless.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@docs/scripts/translate-docs.py`:
- Around line 25-26: Replace the hardcoded Ollama endpoint and model by reading
them from environment variables (like ZAI_BASE/ZAI_MODEL) so the script is
usable outside the author's network: change the OLLAMA_BASE and MODEL constants
to default from os.environ.get("OLLAMA_BASE", "<current default>") and
os.environ.get("OLLAMA_MODEL", "<current default>") (or similar), and update any
code that references OLLAMA_BASE and MODEL to use these values; ensure the
environment variable names are documented in translate-docs.py README or
comments so CI and contributors can override them without editing code.
- Around line 482-488: Increase the z.ai request's "max_tokens" from 8000 to
16384 to match Ollama's num_predict and after parsing the response (where you
call resp.json() into data) detect truncation by inspecting data.get("choices",
[{}])[0] as choice and checking if choice.get("finish_reason") == "length"; if
truncated, print a warning referencing file_path (flush=True) and return None,
otherwise extract the translated text from choice.get("message",
{}).get("content", "").strip() as before.
---
Duplicate comments:
In `@docs/scripts/translate-docs.py`:
- Around line 510-513: The current prefix-stripping logic unconditionally strips
up to the first '---' in result, which can remove legitimate content; change it
to only perform this repair when the original source chunk actually started with
frontmatter and the model's output lost it (same guard used in
translate_with_ollama). Concretely, before running the block that checks
result.startswith("---") and result.index("---"), consult the original source
chunk variable (the input passed to the translation routine) and only apply the
idx/result = result[idx:] fix if that source started with '---' but result does
not — otherwise leave result untouched.
---
Nitpick comments:
In `@docs/scripts/translate-docs.py`:
- Around line 365-523: Both translate_with_ollama and translate_with_zai
duplicate prompt construction and post-response cleanup; extract a helper
assemble_translate_prompt(system_template: str, chunk_label: str, lang: str,
content: str, context_summary: str) that returns (system, user_msg) using
SYSTEM_PROMPT and CHUNK_TRANSLATE_PROMPT, and another helper
clean_translated_text(result: str) that strips markdown fences and any leading
preamble before the frontmatter ("---"). Replace the duplicated blocks in
translate_with_ollama and translate_with_zai to call
assemble_translate_prompt(...) to build system/user messages and call
clean_translated_text(result) before returning; keep existing function names and
_translate_chunk dispatch unchanged so integration is seamless.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: ba3805eb-6d7f-4992-b4c4-dc8550a91d4b
📒 Files selected for processing (1)
docs/scripts/translate-docs.py
| OLLAMA_BASE = "http://ai-master.taileaa27f.ts.net:11434" | ||
| MODEL = "qwen3.6:35b" |
There was a problem hiding this comment.
Hardcoded internal Ollama endpoint and model are not overridable.
OLLAMA_BASE points at a personal Tailscale hostname (ai-master.taileaa27f.ts.net) and MODEL is hardcoded. Anyone outside the author's network cannot run the default backend, and there's no CLI/env override (unlike ZAI_BASE/ZAI_MODEL). Make these env-configurable so this script is usable by other contributors and CI without a code edit.
♻️ Suggested change
-OLLAMA_BASE = "http://ai-master.taileaa27f.ts.net:11434"
-MODEL = "qwen3.6:35b"
+OLLAMA_BASE = os.environ.get("OLLAMA_BASE", "http://localhost:11434")
+MODEL = os.environ.get("OLLAMA_MODEL", "qwen3.6:35b")📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| OLLAMA_BASE = "http://ai-master.taileaa27f.ts.net:11434" | |
| MODEL = "qwen3.6:35b" | |
| OLLAMA_BASE = os.environ.get("OLLAMA_BASE", "http://localhost:11434") | |
| MODEL = os.environ.get("OLLAMA_MODEL", "qwen3.6:35b") |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/scripts/translate-docs.py` around lines 25 - 26, Replace the hardcoded
Ollama endpoint and model by reading them from environment variables (like
ZAI_BASE/ZAI_MODEL) so the script is usable outside the author's network: change
the OLLAMA_BASE and MODEL constants to default from
os.environ.get("OLLAMA_BASE", "<current default>") and
os.environ.get("OLLAMA_MODEL", "<current default>") (or similar), and update any
code that references OLLAMA_BASE and MODEL to use these values; ensure the
environment variable names are documented in translate-docs.py README or
comments so CI and contributors can override them without editing code.
| ], | ||
| "temperature": 0.2, | ||
| "max_tokens": 8000, | ||
| }, | ||
| timeout=180, | ||
| ) | ||
| resp.raise_for_status() |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
z.ai chat completions API max_tokens limit GLM-5.1 GLM-4.7
💡 Result:
For z.ai's chat completions API, the max_tokens limit (maximum output tokens) is 131072 for both GLM-5.1 and GLM-4.7 [1][2]. The default max_tokens is 65536 for both models [1]. These limits apply to the GLM-5.1, GLM-5, GLM-4.7, and GLM-4.6 series, which support up to 128K (131072 tokens) maximum output [2]. The API endpoint is https://api.z.ai/api/paas/v4/chat/completions, compatible with OpenAI-style requests [3][2][4]. Context windows are up to 200K tokens for GLM-5.1 [5][6], but max_tokens specifically limits generated output tokens [1].
Citations:
- 1: https://docs.z.ai/guides/overview/concept-param
- 2: https://docs.z.ai/api-reference/llm/chat-completion
- 3: https://docs.z.ai/guides/llm/glm-5.1
- 4: https://docs.z.ai/guides/llm/glm-4.7
- 5: https://runware.ai/docs/models/zai-glm-5-1
- 6: https://lambda.ai/inference-models/zai-org/glm-5.1
🏁 Script executed:
# First, let's examine the file and understand the context around lines 482-488
head -n 500 docs/scripts/translate-docs.py | tail -n 50Repository: wailsapp/wails
Length of output: 1667
🏁 Script executed:
# Search for CHUNK_SIZE definition
rg "CHUNK_SIZE\s*=" docs/scripts/translate-docs.pyRepository: wailsapp/wails
Length of output: 134
🏁 Script executed:
# Look for Ollama and z.ai API configurations to compare
rg -A 5 -B 5 "Ollama|z\.ai" docs/scripts/translate-docs.py | head -n 100Repository: wailsapp/wails
Length of output: 2711
🏁 Script executed:
# Find and examine the translate_with_ollama function
rg -A 30 "def translate_with_ollama" docs/scripts/translate-docs.pyRepository: wailsapp/wails
Length of output: 1084
🏁 Script executed:
# Search specifically for num_predict in the file
rg "num_predict" docs/scripts/translate-docs.pyRepository: wailsapp/wails
Length of output: 153
🏁 Script executed:
# Get more context around both num_predict occurrences
rg -B 5 -A 5 "num_predict" docs/scripts/translate-docs.pyRepository: wailsapp/wails
Length of output: 689
🏁 Script executed:
# Get the line count to understand file structure better
wc -l docs/scripts/translate-docs.pyRepository: wailsapp/wails
Length of output: 93
Increase max_tokens for z.ai and detect truncation to prevent silently incomplete translations.
The current max_tokens: 8000 is arbitrary (z.ai's API supports up to 131,072)—set it to match Ollama's num_predict: 16384. For CJK locales (zh-cn/zh-tw/ja/ko) and verbose languages like German, an 8000-character input chunk often exceeds 8000 output tokens once tokenized. z.ai silently truncates at this limit, returning finish_reason: "length" without raising an error, producing incomplete pages undetected.
Suggested change
"temperature": 0.2,
- "max_tokens": 8000,
+ "max_tokens": 16384,
},After data = resp.json(), add truncation detection:
choice = data.get("choices", [{}])[0]
if choice.get("finish_reason") == "length":
print(f" WARNING: z.ai response truncated (finish_reason=length) for {file_path}", flush=True)
return None
result = choice.get("message", {}).get("content", "").strip()🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@docs/scripts/translate-docs.py` around lines 482 - 488, Increase the z.ai
request's "max_tokens" from 8000 to 16384 to match Ollama's num_predict and
after parsing the response (where you call resp.json() into data) detect
truncation by inspecting data.get("choices", [{}])[0] as choice and checking if
choice.get("finish_reason") == "length"; if truncated, print a warning
referencing file_path (flush=True) and return None, otherwise extract the
translated text from choice.get("message", {}).get("content", "").strip() as
before.
Summary
Adds three Python scripts to
docs/scripts/that power the Wails v3 automated documentation translation pipeline.Scripts
translate-docs.py— Batch translatorqwen3.6:35b) to translate MDX/Markdown docs to 8 locales"Your UI" → "Ihre UI"in the German PR)post_process(): deterministic fixes applied to every translation output (frontmatter artifacts, asset paths, link rewrites)translate-qa.py— Quality scorer"Your UI" → "Ihre UI"style errors with-0.15penalty--ai-verifyflag: optional z.ai prose quality scoring (60% heuristic / 40% AI blend), requiresZAI_API_KEYimprove-prompts.py— Prompt improvement looptranslate-docs.pypython3 scripts/improve-prompts.py --locale de --iterations 3A/B Test Results
Test locale: German (
de), test files:why-wails.mdx+installation.mdxThe automated loop correctly identified that product name preservation and consistency needed strengthening. The d2 string-label fix required a human-added counter-example (
"Your UI" → wrong: "Ihre UI") that made the rule unambiguous to the model.🤖 Generated with Claude Code
Summary by CodeRabbit