trustgraph/generate_i18n_packs.py
Jenkins, Kenneth Alexander 1f1aaa24ae
second round of translation fixes
Signed-off-by: Jenkins, Kenneth Alexander <kjenkins60@gatech.edu>
2026-04-06 14:50:34 -04:00

233 lines
9 KiB
Python

#!/usr/bin/env python3
"""Generate TrustGraph pre-built language packs using the local Ollama translate model.
This script translates a curated set of UI/CLI strings (keys) from English into
all supported languages and writes JSON packs under:
trustgraph-base/trustgraph/i18n/packs/
Design goal: no runtime translation. Packs are generated ahead of time and
loaded via `trustgraph.i18n`.
Requires:
- Ollama running locally (default: http://localhost:11434)
- Model available (default: translategemma:12b)
Usage:
python generate_i18n_packs.py
You can override:
OLLAMA_URL, OLLAMA_MODEL
"""
from __future__ import annotations
import argparse
import json
import os
import re
from pathlib import Path
from typing import Dict, List, Tuple
# Reuse the hardened translation engine we already use for docs.
import translate_docs
DEFAULT_OUTPUT_DIR = Path("trustgraph-base/trustgraph/i18n/packs")
# 10 languages (including English)
LANGS: Dict[str, Tuple[str, str]] = {
"en": ("English", "en"),
"es": ("Spanish", "es"),
"sw": ("Swahili", "sw"),
"pt": ("Portuguese", "pt"),
"tr": ("Turkish", "tr"),
"hi": ("Hindi", "hi"),
"he": ("Hebrew", "he"),
"ar": ("Arabic", "ar"),
"zh-cn": ("Chinese (simplified)", "zh-cn"),
"ru": ("Russian", "ru"),
}
# Curated strings currently used by tg-verify-system-status.
# Keep keys stable.
EN_STRINGS: Dict[str, str] = {
"cli.verify_system_status.title": "TrustGraph System Status Verification",
"cli.verify_system_status.phase_1": "Phase 1: Infrastructure",
"cli.verify_system_status.phase_2": "Phase 2: Core Services",
"cli.verify_system_status.phase_3": "Phase 3: Data Services",
"cli.verify_system_status.phase_4": "Phase 4: User Interface",
"cli.verify_system_status.summary": "Summary",
"cli.verify_system_status.checking": "Checking {name}...",
"cli.verify_system_status.checking_attempt": "Checking {name}... (attempt {attempt})",
"cli.verify_system_status.failed_timeout": "{name}: Failed (timeout after {attempt} attempts)",
"cli.verify_system_status.pulsar_not_responding": "Pulsar is not responding - other checks may fail",
"cli.verify_system_status.checks_passed": "Checks passed: {passed}/{total}",
"cli.verify_system_status.checks_failed": "Checks failed: {failed}/{total}",
"cli.verify_system_status.total_time": "Total time: {elapsed}",
"cli.verify_system_status.system_healthy": "System is healthy!",
"cli.verify_system_status.system_failing": "System has {failed} failing check(s)",
"cli.verify_system_status.check_name.pulsar": "Pulsar",
"cli.verify_system_status.check_name.api_gateway": "API Gateway",
"cli.verify_system_status.check_name.processors": "Processors",
"cli.verify_system_status.check_name.flow_blueprints": "Flow Blueprints",
"cli.verify_system_status.check_name.flows": "Flows",
"cli.verify_system_status.check_name.prompts": "Prompts",
"cli.verify_system_status.check_name.library": "Library",
"cli.verify_system_status.check_name.workbench_ui": "Workbench UI",
"cli.verify_system_status.pulsar.healthy": "Pulsar healthy ({clusters} cluster(s))",
"cli.verify_system_status.pulsar.status": "Pulsar returned status {status_code}",
"cli.verify_system_status.pulsar.timeout": "Pulsar connection timeout",
"cli.verify_system_status.pulsar.cannot_connect": "Cannot connect to Pulsar",
"cli.verify_system_status.pulsar.error": "Pulsar error: {error}",
"cli.verify_system_status.api_gateway.responding": "API Gateway is responding",
"cli.verify_system_status.api_gateway.status": "API Gateway returned status {status_code}",
"cli.verify_system_status.api_gateway.timeout": "API Gateway connection timeout",
"cli.verify_system_status.api_gateway.cannot_connect": "Cannot connect to API Gateway",
"cli.verify_system_status.api_gateway.error": "API Gateway error: {error}",
"cli.verify_system_status.processors.found": "Found {count} processors (≥ {min})",
"cli.verify_system_status.processors.only": "Only {count} processors running (need {min})",
"cli.verify_system_status.processors.metrics_status": "Metrics returned status {status_code}",
"cli.verify_system_status.processors.error": "Processor check error: {error}",
"cli.verify_system_status.flow_blueprints.found": "Found {count} flow blueprint(s)",
"cli.verify_system_status.flow_blueprints.none": "No flow blueprints found",
"cli.verify_system_status.flow_blueprints.error": "Flow blueprints check error: {error}",
"cli.verify_system_status.flows.responding": "Flow manager responding ({count} flow(s))",
"cli.verify_system_status.flows.error": "Flow manager check error: {error}",
"cli.verify_system_status.prompts.found": "Found {count} prompt(s)",
"cli.verify_system_status.prompts.none": "No prompts found",
"cli.verify_system_status.prompts.error": "Prompts check error: {error}",
"cli.verify_system_status.library.responding": "Library responding ({count} document(s))",
"cli.verify_system_status.library.error": "Library check error: {error}",
"cli.verify_system_status.ui.responding": "Workbench UI is responding",
"cli.verify_system_status.ui.status": "UI returned status {status_code}",
"cli.verify_system_status.ui.timeout": "UI connection timeout",
"cli.verify_system_status.ui.cannot_connect": "Cannot connect to UI",
"cli.verify_system_status.ui.error": "UI error: {error}",
}
_FMT_RE = re.compile(r"\{[a-zA-Z0-9_]+\}")
def _protect_format_placeholders(text: str) -> Tuple[str, Dict[str, str]]:
"""Replace `{name}` placeholders with CODE-like placeholders that the doc translator preserves."""
mapping: Dict[str, str] = {}
def repl(m: re.Match[str]) -> str:
idx = len(mapping)
token = f"⟦CODE_{idx}"
mapping[token] = m.group(0)
return token
protected = _FMT_RE.sub(repl, text)
return protected, mapping
def _restore_format_placeholders(text: str, mapping: Dict[str, str]) -> str:
out = text
for token, original in mapping.items():
out = out.replace(token, original)
return out
def _validate_placeholders(restored: str, originals: List[str]) -> bool:
return all(ph in restored for ph in originals)
def translate_pack(client: translate_docs.OllamaClient, *, lang_name: str, lang_code: str) -> Dict[str, str]:
keys = list(EN_STRINGS.keys())
protected_lines: List[str] = []
placeholder_maps: List[Dict[str, str]] = []
placeholder_originals: List[List[str]] = []
for k in keys:
text = EN_STRINGS[k]
protected, mapping = _protect_format_placeholders(text)
protected_lines.append(protected + "\n")
placeholder_maps.append(mapping)
placeholder_originals.append(list(mapping.values()))
translated = translate_docs.translate_lines_resilient(
client,
lines=protected_lines,
target_language_name=lang_name,
target_language_code=lang_code,
retries=2,
)
if translated is None:
raise RuntimeError(f"Translation failed for {lang_code}")
out_lines = translated.splitlines()
if len(out_lines) != len(keys):
raise RuntimeError(f"Unexpected line count for {lang_code}: {len(out_lines)} != {len(keys)}")
pack: Dict[str, str] = {}
for i, k in enumerate(keys):
restored = _restore_format_placeholders(out_lines[i], placeholder_maps[i])
# Ensure python-format placeholders survived
if not _validate_placeholders(restored, placeholder_originals[i]):
raise RuntimeError(f"Placeholder lost for key {k} in {lang_code}")
pack[k] = restored
return pack
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--output-dir",
default=str(DEFAULT_OUTPUT_DIR),
help=f"Output directory for packs (default: {DEFAULT_OUTPUT_DIR})",
)
args = parser.parse_args()
output_dir = Path(args.output_dir)
output_dir.mkdir(parents=True, exist_ok=True)
client = translate_docs.OllamaClient(
url=os.getenv("OLLAMA_URL", translate_docs.DEFAULT_OLLAMA_URL),
model=os.getenv("OLLAMA_MODEL", translate_docs.DEFAULT_OLLAMA_MODEL),
num_ctx=translate_docs.DEFAULT_NUM_CTX,
num_predict=translate_docs.DEFAULT_NUM_PREDICT,
temperature=translate_docs.DEFAULT_TEMPERATURE,
top_p=translate_docs.DEFAULT_TOP_P,
repeat_penalty=translate_docs.DEFAULT_REPEAT_PENALTY,
keep_alive=translate_docs.DEFAULT_KEEP_ALIVE,
)
# Always write English as the source pack
(output_dir / "en.json").write_text(json.dumps(EN_STRINGS, ensure_ascii=False, indent=2) + "\n", encoding="utf-8")
for lang, (name, code) in LANGS.items():
if lang == "en":
continue
print(f"\n=== Generating pack: {lang} ({name}) ===")
pack = translate_pack(client, lang_name=name, lang_code=code)
(output_dir / f"{lang}.json").write_text(
json.dumps(pack, ensure_ascii=False, indent=2) + "\n",
encoding="utf-8",
)
print("\nDone.")
if __name__ == "__main__":
main()