- en2ru.py: EN markdown -> Russian via Ollama , strips code-fence artifacts, prints jargon-review hints to stderr - DOCSTRAT.md: rule — EN canonical source, RU for comprehension only, never edit on drifted Russian (dedup/receive mistranslation guard) - ROADMAP.ru.md: sample RU generated from EN source - AGENTS.md: section 8 documenting the EN-first workflow
99 lines
3.7 KiB
Python
99 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
|
"""en2ru — English-source-of-truth -> Russian comprehension doc.
|
|
|
|
RULE (see DOCSTRAT.md): English is the CANONICAL source of truth. Russian is
|
|
only a comprehension aid; terminology may drift and MUST NEVER be used as the
|
|
editing basis. Always edit the `*.md` (EN), then re-run this to refresh
|
|
`<file>.ru.md`.
|
|
|
|
Usage:
|
|
python3 en2ru.py path/to/doc.md # writes path/to/doc.ru.md
|
|
cat doc.md | python3 en2ru.py - # stdin -> stdout + doc.ru.md
|
|
|
|
Model: translategemma:latest (Ollama, local @ 127.0.0.1:11434).
|
|
Jargon drift hints are printed to STDERR (never inside the RU output).
|
|
No external deps (urllib only) so it works offline after the model is pulled.
|
|
"""
|
|
import sys
|
|
import json
|
|
import urllib.request
|
|
|
|
OLLAMA_URL = "http://127.0.0.1:11434/v1/chat/completions"
|
|
MODEL = "translategemma:latest"
|
|
|
|
# Terms TranslateGemma-4b mishandles -> flag for human review (never auto-fix).
|
|
# Maps the EN token to the safe RU handling. JAPN is intentional: keep short.
|
|
JARGON_HINTS = [
|
|
("dedup", "дедупляция / удаление дублей"), # NOT «дублирование» (reversed!)
|
|
("receipt", "рецепт (паттерн действий)"), # NOT «результат»
|
|
("atom-level", "на уровне атомов"),
|
|
("stale", "устаревший (status=stale)"),
|
|
("gap", "пробел (gap)"),
|
|
("verify", "верификация / verify"),
|
|
("rerank", "реранг / reranking"),
|
|
("cache", "кэш"),
|
|
("inferred", "выведено / inferred"),
|
|
("web_search", "web_search (инструмент)"),
|
|
("ingest", "ингестия / загрузка / ingest"),
|
|
]
|
|
|
|
|
|
def translate(text: str) -> str:
|
|
prompt = (
|
|
"Translate the following Markdown technical text to Russian.\n"
|
|
"Rules:\n"
|
|
"- Output ONLY the Russian text. Preserve Markdown structure, code blocks,\n"
|
|
" backticks, code identifiers, URLs, command and tool names verbatim.\n"
|
|
"- Do NOT translate: code, identifiers, tool names, file extensions, URLs,\n"
|
|
" and product names (Memgraph, PostgreSQL, Qdrant, Memgraph).\n"
|
|
"- If unsure about a term, keep it as-is in Latin (do not guess).\n"
|
|
"- Words you must handle carefully: "
|
|
+ ", ".join(en for en, _ in JARGON_HINTS) + ".\n\n"
|
|
"TEXT:\n```\n" + text + "\n```"
|
|
)
|
|
payload = {
|
|
"model": MODEL,
|
|
"stream": False,
|
|
"messages": [{"role": "user", "content": prompt}],
|
|
}
|
|
req = urllib.request.Request(
|
|
OLLAMA_URL,
|
|
data=json.dumps(payload).encode("utf-8"),
|
|
headers={"Content-Type": "application/json"},
|
|
)
|
|
with urllib.request.urlopen(req, timeout=300) as resp:
|
|
return json.loads(resp.read())["choices"][0]["message"]["content"]
|
|
|
|
|
|
def main() -> None:
|
|
if len(sys.argv) < 2 or sys.argv[1] == "-":
|
|
src = sys.stdin.read()
|
|
base = "doc"
|
|
else:
|
|
with open(sys.argv[1], encoding="utf-8") as fh:
|
|
src = fh.read()
|
|
base = sys.argv[1].rsplit(".", 1)[0]
|
|
|
|
ru = translate(src)
|
|
# model echoes the prompt's ``` fences -> drop leading/trailing fence lines
|
|
lines = ru.splitlines()
|
|
if lines and lines[0].strip() == "```":
|
|
lines = lines[1:]
|
|
if lines and lines[-1].strip() == "```":
|
|
lines = lines[:-1]
|
|
ru = "\n".join(lines).strip()
|
|
out_path = base + ".ru.md"
|
|
with open(out_path, "w", encoding="utf-8") as fh:
|
|
fh.write(ru)
|
|
|
|
low = src.lower()
|
|
print("# en2ru -> " + out_path, file=sys.stderr)
|
|
for en, hint in JARGON_HINTS:
|
|
if en.lower() in low:
|
|
print(" review: '%s' -> ensure '%s'" % (en, hint), file=sys.stderr)
|
|
print("OK: " + out_path)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|