From df696750e873fdb901f5e25394b4663931dea316 Mon Sep 17 00:00:00 2001 From: tym83 <6355522@gmail.com> Date: Fri, 24 Jul 2026 10:31:51 +0500 Subject: [PATCH 1/4] feat(i18n): guarantee link/identifier integrity and check typography Masking previously covered code, shortcodes and comments, so a URL, a bare CLI flag, a version number or a brand sitting in ordinary prose was defended by a prompt rule alone. Link destinations are now masked like any other protected span, and two deterministic checks feed the existing revise loop: - integrity_findings() compares versions, bare flags and do-not-translate terms between source and translation, catching a localized version separator or a transliterated brand. - check_typography() enforces the per-language rules the style guides state (Russian guillemets, German quotes, Spanish inverted marks, Chinese full-width punctuation, pt-PT vocabulary leaks, Devanagari digits). Both look at prose only; markup, code and link targets are exempt so the checks do not cry wolf on correct ASCII punctuation in an HTML attribute. lint_translations.py applies the same typography rules to already-published pages, where a hand edit is otherwise never re-checked. Co-Authored-By: Claude Signed-off-by: tym83 <6355522@gmail.com> --- hack/i18n/lib.py | 154 +++++++++++++++++++++++++++++++++ hack/i18n/lint_translations.py | 73 ++++++++++++++++ hack/i18n/test_i18n.py | 84 ++++++++++++++++++ hack/i18n/translate.py | 8 ++ 4 files changed, 319 insertions(+) create mode 100755 hack/i18n/lint_translations.py diff --git a/hack/i18n/lib.py b/hack/i18n/lib.py index 9d3427fe..b5e5d12e 100644 --- a/hack/i18n/lib.py +++ b/hack/i18n/lib.py @@ -47,6 +47,18 @@ # link, the delimiters, the param names) is structure and stays verbatim. _VISIBLE_ATTR_RE = re.compile(r'\b(?:caption|alt|title)="([^"]*)"') +# Link DESTINATIONS. Only backticked code was structurally protected before, so a +# URL sitting in ordinary prose (`[text](https://…)`) was defended by a prompt rule +# alone — the model could silently mutate, localize or "fix" it and nothing would +# catch it. Masking the destination makes URL integrity a guarantee instead of a +# hope, while leaving the link TEXT exposed so it still gets translated. +# inline: [text](/docs/install "Optional title") -> destination only +# autolink: +# refdef: [id]: https://example.com +_LINKDEST_RE = re.compile(r'(?<=\])\((?P<[^>]*>|[^)\s]*)(?P\s+"[^"]*")?\)') +_AUTOLINK_RE = re.compile(r'<(?:https?|mailto):[^>\s]+>') +_REFDEF_RE = re.compile(r'(?m)^(?P<label>\[[^\]]+\]:[ \t]*)(?P<dest>\S+)') + def load_config() -> dict: with open(CONFIG_PATH, encoding="utf-8") as fh: @@ -385,6 +397,15 @@ def _shortcode(m: "re.Match") -> str: # 3. Comments, then inline code. text = _HTMLCOMMENT_RE.sub(_sub("HTMLCOMMENT"), text) text = _INLINECODE_RE.sub(_sub("INLINECODE"), text) + + # 4. Link destinations LAST, so URLs already inside code/shortcodes are + # covered by their own placeholders and are not double-masked. Link TEXT + # stays exposed and still gets translated; only the target is opaque. + text = _AUTOLINK_RE.sub(_sub("URL"), text) + text = _LINKDEST_RE.sub( + lambda m: "(" + _stash(m.group("dest"), "URL") + (m.group("title") or "") + ")", text) + text = _REFDEF_RE.sub( + lambda m: m.group("label") + _stash(m.group("dest"), "URL"), text) return text, store @@ -430,6 +451,139 @@ def has_ref_shortcode(text: str) -> bool: return bool(_ANY_REF_SHORTCODE.search(text)) +# ---- deterministic integrity & typography checks ---------------------------- +# +# The masking in protect() guarantees that code, shortcodes and URLs survive +# byte-for-byte. What it CANNOT guarantee is the material that legitimately sits +# in ordinary prose: a bare `--flag`, a `kind: Tenant` written without backticks, +# a version number, a brand from do_not_translate. Those are defended only by +# prompt rules, which is to say softly. These checks turn "the model was told +# not to" into "we looked". They return FINDINGS (fed to the same revise loop as +# the reviewers) rather than raising: a false positive should cost one revise +# round, never a hard-failed page. + +_CODEISH_RE = re.compile( + r"```.*?```" # fenced code + r"|`[^`\n]+`" # inline code + r"|§§[A-Z]+_\d+§§" # pipeline placeholders + r"|\{\{[<%].*?[%>]\}\}" # Hugo shortcodes (params are not prose) + r"|<!--.*?-->" # HTML comments + r"|<[^>\n]+>" # HTML tags with their attributes + r"|\]\([^)\s]*\)" # markdown link/image destinations + r"|^\[[^\]]+\]:[ \t]*\S+", # reference-link definitions + re.DOTALL | re.MULTILINE) + + +def _prose_only(text: str) -> str: + """Drop everything that is markup or code, so the checks see prose alone. + + Without this the typography rules fire on things that are *correctly* written + with ASCII punctuation — an HTML attribute (`class="hero"`), a shortcode + param, a file path — and a check that cries wolf is a check reviewers learn + to ignore. + """ + return _CODEISH_RE.sub(" ", text) + + +# Version-ish tokens: v1.5, 1.2.3, v1.2.5. Localizing the separator (1,2,3) or +# bumping a digit changes documented behaviour, so counts must match the source. +_VERSION_RE = re.compile(r"\bv?\d+\.\d+(?:\.\d+)?\b") +# Bare long CLI flags in prose. +_FLAG_RE = re.compile(r"(?<![\w-])--[a-z][a-z0-9-]{2,}\b") + + +def integrity_findings(src_body: str, tr_body: str, dnt_terms: list[str] | None = None, + who: str = "integrity-check") -> list[dict]: + """Compare source and translation for material that must survive verbatim. + + Checks, all on prose only (masked spans are already guaranteed): + * version tokens — same multiset + * bare CLI flags — same multiset + * do_not_translate — a term present in the source must not vanish + """ + out: list[dict] = [] + src, tr = _prose_only(src_body), _prose_only(tr_body) + + def _counts(rx, s): + d: dict[str, int] = {} + for m in rx.finditer(s): + d[m.group(0)] = d.get(m.group(0), 0) + 1 + return d + + for label, rx in (("version", _VERSION_RE), ("CLI flag", _FLAG_RE)): + a, b = _counts(rx, src), _counts(rx, tr) + for tok, n in a.items(): + got = b.get(tok, 0) + if got < n: + out.append({"severity": "major", "from": who, + "issue": f"{label} '{tok}' appears {n}× in the English source but " + f"{got}× in the translation — it must be carried over " + f"unchanged"}) + for tok in b: + if tok not in a: + out.append({"severity": "minor", "from": who, + "issue": f"{label} '{tok}' appears in the translation but not in " + f"the source — do not invent versions or flags"}) + + for term in (dnt_terms or []): + n = src.count(term) + if n and tr.count(term) < n: + out.append({"severity": "major", "from": who, + "issue": f"do-not-translate term '{term}' appears {n}× in the source " + f"but only {tr.count(term)}× in the translation — it must be " + f"kept verbatim, not translated or transliterated"}) + return out + + +# Per-language typography. The style guides state these rules; nothing checked +# them, so a page could read fine and still be typeset like English. Findings are +# advisory-but-real: they are the cheapest quality signal we have per language. +_TYPO_RULES: dict[str, list[tuple[str, str]]] = { + "ru": [ + (r'"[^"\n]{1,80}"', 'ASCII double quotes in Russian prose — use «ёлочки»'), + (r'[“”][^\n]{0,80}[“”]', 'English curly quotes in Russian prose — use «ёлочки»'), + # A hyphen doing a dash's job — but NOT a list marker ("\n- item"), which + # is why a non-space character is required immediately before it. + (r'(?<=[^\s])[ ]-[ ](?=\S)', 'hyphen used as a dash — use the em dash «—»'), + ], + "de": [ + (r'"[^"\n]{1,80}"', 'ASCII double quotes in German prose — use „…“'), + (r'[“][^\n]{0,80}[”]', 'English curly quotes in German prose — use „…“'), + ], + "es": [ + (r'(?<![¿])\b[A-ZÁÉÍÓÚÑ][^.!?\n]{5,120}\?', 'question without an opening ¿'), + (r'(?<![¡])\b[A-ZÁÉÍÓÚÑ][^.!?\n]{5,120}!', 'exclamation without an opening ¡'), + ], + "pt-br": [ + (r'\b(ficheiro|utilizador|ecrã|rato|autocarro|telemóvel|casa de banho)\b', + 'European Portuguese vocabulary leaked into pt-BR'), + (r'\bestá a [a-zçãéêó]+r\b', 'European Portuguese "está a fazer" — pt-BR uses the gerund'), + ], + "zh-cn": [ + (r'[一-鿿]\s*[,;:!?]', 'half-width punctuation after a Chinese character — use ,;:!?'), + (r'[,;:!?]\s*[一-鿿]', 'half-width punctuation before a Chinese character — use ,;:!?'), + (r'[一-鿿]\.(?=\s|$)', 'half-width period ending a Chinese sentence — use 。'), + (r'[A-Za-z0-9]', 'full-width Latin letters or digits — use half-width'), + ], + "hi": [ + (r'[०-९]', 'Devanagari digits — use Western Arabic numerals in technical content'), + ], +} + + +def check_typography(text: str, lang: str, who: str = "typography-check") -> list[dict]: + """Per-language typography findings (prose only; code spans are exempt).""" + prose = _prose_only(text) + out: list[dict] = [] + for pattern, message in _TYPO_RULES.get(lang, []): + hits = re.findall(pattern, prose) + if hits: + sample = hits[0] if isinstance(hits[0], str) else str(hits[0]) + out.append({"severity": "minor", "from": who, + "issue": f"{message} ({len(hits)}×, e.g. {sample.strip()[:60]!r})"}) + return out + + def deref_shortcodes(text: str, page_rel: str) -> str: """Rewrite Hugo ref/relref shortcodes to plain absolute links. diff --git a/hack/i18n/lint_translations.py b/hack/i18n/lint_translations.py new file mode 100755 index 00000000..c906ee0c --- /dev/null +++ b/hack/i18n/lint_translations.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +"""Lint already-published translations for per-language typography. + +WHY: `check-i18n.sh` guards i18n key parity and translation freshness, and the +pipeline's gate checks a page at the moment it is generated. Neither looks at a +page again afterwards — so a hand edit (or a page translated before a style rule +existed) can sit in the tree indefinitely with English quotation marks in Russian +prose or half-width punctuation in Chinese. The style guides state these rules; +this makes them enforceable on every PR. + +Findings are advisory by default (exit 0) because typography rules are heuristic +and the tree has history. Pass --strict to fail the build instead, once a +language's backlog is clean. + +Usage: + python3 hack/i18n/lint_translations.py # all configured languages + python3 hack/i18n/lint_translations.py --lang ru # one language + python3 hack/i18n/lint_translations.py --strict # exit 1 on any finding +""" +from __future__ import annotations + +import argparse +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +import lib + + +def main() -> int: + ap = argparse.ArgumentParser() + ap.add_argument("--lang", help="only this language code") + ap.add_argument("--strict", action="store_true", + help="exit non-zero when findings exist (default: advisory)") + args = ap.parse_args() + + cfg = lib.load_config() + langs = [l["code"] for l in cfg["languages"] if args.lang in (None, l["code"])] + if args.lang and not langs: + print(f"::error::unknown --lang '{args.lang}'", file=sys.stderr) + return 2 + + total = 0 + for lang in langs: + root = os.path.join(lib.REPO_ROOT, cfg["content_dir"], lang) + if not os.path.isdir(root): + continue + for dirpath, _dirs, files in os.walk(root): + for name in sorted(files): + if not name.endswith((".md", ".html")): + continue + path = os.path.join(dirpath, name) + with open(path, encoding="utf-8") as fh: + _fm, body, _raw = lib.split_frontmatter(fh.read()) + findings = lib.check_typography(body, lang) + if findings: + rel = os.path.relpath(path, lib.REPO_ROOT) + print(f"{rel}") + for f in findings: + print(f" - {f['issue']}") + total += len(findings) + + if total: + print(f"\n{total} typography finding(s). These are the rules the per-language " + f"style guides state; fix them or refine the rule in lib._TYPO_RULES.") + else: + print("no typography findings") + return 1 if (total and args.strict) else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/hack/i18n/test_i18n.py b/hack/i18n/test_i18n.py index 056620ad..7cac827a 100644 --- a/hack/i18n/test_i18n.py +++ b/hack/i18n/test_i18n.py @@ -600,5 +600,89 @@ def test_empty_worklist_clears_a_prior_run_report(self): os.unlink(translate.REPORT_PATH) +class TestLinkDestinationMasking(unittest.TestCase): + """URLs used to be defended by a prompt rule only. They are masked now, so a + model that "fixes" or localizes a link cannot silently ship a broken one.""" + + def test_inline_link_destination_is_masked_but_text_is_not(self): + masked, store = lib.protect("See the [install guide](/docs/v1.5/install) for details.") + self.assertNotIn("/docs/v1.5/install", masked) + self.assertIn("install guide", masked) # link text stays translatable + self.assertEqual(lib.restore(masked, store), + "See the [install guide](/docs/v1.5/install) for details.") + + def test_autolink_and_refdef_are_masked(self): + text = "Visit <https://cozystack.io> now.\n\n[ref]: https://example.com/a?b=c\n" + masked, store = lib.protect(text) + self.assertNotIn("cozystack.io", masked) + self.assertNotIn("example.com", masked) + self.assertEqual(lib.restore(masked, store), text) + + def test_link_title_is_left_translatable(self): + masked, store = lib.protect('A [link](/a/b "Read this") here.') + self.assertIn('"Read this"', masked) + self.assertNotIn("/a/b", masked) + self.assertEqual(lib.restore(masked, store), 'A [link](/a/b "Read this") here.') + + +class TestIntegrityFindings(unittest.TestCase): + def test_clean_translation_has_no_findings(self): + self.assertEqual( + lib.integrity_findings("Cozystack v1.5 uses --dry-run.", + "Cozystack v1.5 verwendet --dry-run.", ["Cozystack"]), []) + + def test_dropped_version_is_major(self): + f = lib.integrity_findings("Upgrade to v1.5 now.", "Jetzt aktualisieren.") + self.assertTrue(any(x["severity"] == "major" and "v1.5" in x["issue"] for x in f)) + + def test_localized_decimal_in_a_version_is_caught(self): + # "v1.5" rewritten as "v1,5" — the token no longer matches the source. + f = lib.integrity_findings("Use v1.5.", "Nutze v1,5.") + self.assertTrue(any("v1.5" in x["issue"] for x in f)) + + def test_dropped_flag_is_major(self): + f = lib.integrity_findings("Pass --dry-run to preview.", "Zum Testen übergeben.") + self.assertTrue(any(x["severity"] == "major" and "--dry-run" in x["issue"] for x in f)) + + def test_translated_brand_is_caught(self): + f = lib.integrity_findings("Cozystack is a platform.", "Козистек — это платформа.", + ["Cozystack"]) + self.assertTrue(any("Cozystack" in x["issue"] for x in f)) + + def test_code_spans_are_exempt(self): + # A version that only lives inside code is already guaranteed by masking; + # it must not be double-reported here. + self.assertEqual(lib.integrity_findings("Run `helm install v1.5`.", "Führen Sie aus."), []) + + +class TestTypographyChecks(unittest.TestCase): + def test_russian_ascii_quotes_flagged(self): + f = lib.check_typography('Это "кластер" здесь.', "ru") + self.assertTrue(any("ёлочки" in x["issue"] for x in f)) + + def test_russian_guillemets_pass(self): + self.assertEqual(lib.check_typography("Это «кластер» здесь.", "ru"), []) + + def test_chinese_halfwidth_punctuation_flagged(self): + self.assertTrue(lib.check_typography("这是集群, 然后部署", "zh-cn")) + + def test_chinese_fullwidth_punctuation_passes(self): + self.assertEqual(lib.check_typography("这是集群,然后部署。", "zh-cn"), []) + + def test_pt_pt_vocabulary_leak_flagged(self): + f = lib.check_typography("Abra o ficheiro de configuração.", "pt-br") + self.assertTrue(any("European Portuguese" in x["issue"] for x in f)) + + def test_devanagari_digits_flagged(self): + self.assertTrue(lib.check_typography("क्लस्टर में ३ नोड हैं।", "hi")) + + def test_code_spans_are_exempt_from_typography(self): + # Straight quotes inside code are correct and must not be flagged. + self.assertEqual(lib.check_typography('Запустите `echo "hi"` сейчас.', "ru"), []) + + def test_unknown_language_is_a_no_op(self): + self.assertEqual(lib.check_typography('Anything "here".', "xx"), []) + + if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/hack/i18n/translate.py b/hack/i18n/translate.py index 369d9689..b3669aca 100644 --- a/hack/i18n/translate.py +++ b/hack/i18n/translate.py @@ -293,6 +293,14 @@ def translate_page(cfg, glossary, lang_cfg, rel) -> tuple[str, bool, list[dict]] or [{"severity": "major", "from": "back-translation", "issue": "revise verdict with no findings listed"}]) + # 2b. deterministic checks — cheap, objective, and not subject to the + # reviewers' mood. Masking already guarantees code/shortcodes/URLs; these + # cover what legitimately lives in prose (versions, bare flags, brands) + # and the per-language typography the style guides mandate but nothing + # previously verified. They feed the same revise loop as the reviewers. + findings += lib.integrity_findings(body, tr_body, glossary.get("do_not_translate")) + findings += lib.check_typography(tr_body, lang_cfg["code"]) + # 3. two native reviewers for reviewer in cfg["review"]["reviewers"]: sys_r = render(_read(os.path.join(PROMPT_DIR, os.path.basename(reviewer["prompt"]))), From 97dd5fc31bb851f3cea5824b2ad4bc4593edd152 Mon Sep 17 00:00:00 2001 From: tym83 <6355522@gmail.com> Date: Fri, 24 Jul 2026 10:31:51 +0500 Subject: [PATCH 2/4] docs(i18n): turn the per-language style guides into real contracts Each guide was 3-6 lines, yet the whole fluency and typography strategy rests on injecting them into the translate and both reviewer prompts. They are now 80-100 lines each and work as an instruction set and a review rubric: register and address form, heading conventions, a decision rule for terms outside the glossary, typography, number/date formatting, the grammar traps of translating from English into that language, calque patterns with fixes, false friends, and a reviewer checklist of the MT failure modes specific to the language. Every original decision is preserved; the guides expand around them. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: tym83 <6355522@gmail.com> --- hack/i18n/style-guides/de.md | 106 ++++++++++++++++++++++++++++++-- hack/i18n/style-guides/es.md | 94 ++++++++++++++++++++++++++-- hack/i18n/style-guides/hi.md | 103 ++++++++++++++++++++++++++++++- hack/i18n/style-guides/pt-br.md | 85 +++++++++++++++++++++++-- hack/i18n/style-guides/ru.md | 101 ++++++++++++++++++++++++++++-- hack/i18n/style-guides/zh-cn.md | 91 +++++++++++++++++++++++++++ 6 files changed, 557 insertions(+), 23 deletions(-) diff --git a/hack/i18n/style-guides/de.md b/hack/i18n/style-guides/de.md index 48dd72dc..f52c7b88 100644 --- a/hack/i18n/style-guides/de.md +++ b/hack/i18n/style-guides/de.md @@ -1,5 +1,101 @@ -- Formal register: address the reader with "Sie". -- Follow kubernetes.io German localization conventions. -- Compound nouns: keep them readable; hyphenate mixed English-German compounds (e.g. "Kubernetes-Cluster", "Bare-Metal-Server"). -- Keep established English infra loanwords where German has no natural equivalent (Deployment, Workload, Pod, Container). -- Use „German quotation marks". +Code blocks, inline code, Hugo shortcodes, URLs, front-matter keys and `§§NAME_N§§` placeholders are masked structurally by the pipeline — never translate, reorder or "fix" anything inside them. Everything below applies to prose only. + +## 1. Register and tone + +- Formal register: address the reader with **„Sie“** — docs, blog, landing pages, UI strings, notes and warnings alike. Never „du“, never a register switch inside a page. +- Docs: precise, neutral, instructional. Blog/landing: same „Sie“, but shorter sentences, active voice, and transcreated marketing claims instead of word-for-word renderings. +- Instructions use the Sie-imperative (verb first): "Create a namespace." → ✗ „Du erstellst einen Namespace.“ / ✗ „Erstellen ein Namespace.“ → ✓ „Erstellen Sie einen Namespace.“ +- In numbered step lists the Infinitiv is acceptable („Cluster anlegen“), but never mix Infinitiv and Sie-Imperativ within one list. +- "Please note" → ✓ „Beachten Sie“ (not „Bitte beachten Sie bitte“). "Let's create…" → ✓ „Erstellen Sie …“ / „Als Nächstes …“, never „Lassen Sie uns …“. + +## 2. Headings + +- German capitalization: nouns and the first word up, everything else down. English title case must not survive: "Installing the Cozystack Platform" → ✗ „Installieren Der Cozystack Plattform“ → ✓ „Cozystack-Plattform installieren“. +- Task headings: Infinitivkonstruktion. "Configure networking" → ✓ „Netzwerk konfigurieren“ (not „Konfigurieren Sie das Netzwerk“). Concept headings: nominal — "Storage architecture" → ✓ „Speicherarchitektur“. +- Idiomatic, not literal: "Getting started" → ✓ „Erste Schritte“; "Troubleshooting" → ✓ „Fehlerbehebung“; "Prerequisites" → ✓ „Voraussetzungen“; "What's next" → ✓ „Wie geht es weiter“. +- No trailing period; keep question headings as questions; keep heading levels exactly as in the source. + +## 3. Terminology, API objects, gender, plural + +Decision rule for a term **not** covered by the injected glossary, applied in this order: +1. Is it an identifier — API kind, CRD, field, flag, command, chart or package name? → keep verbatim English, uninflected internally, never translated. +2. Does kubernetes.io/de use an established German rendering? → use it. +3. Does the CNCF Cloud Native Glossary (de) have one? → use it. +4. Would a German platform engineer say the English word out loud (Deployment, Workload, Pod, Container, Ingress, Backup, Image)? → keep the English loanword. +5. Otherwise translate, and gloss once on first use: „Mandantentrennung (multi-tenancy)“. Never render the same term two ways on one page. + +- API objects stay English and capitalized in prose: „ein Pod“, „das Deployment“, „ein Secret“, „die VirtualMachine“. Do not translate an object kind (✗ „Geheimnis“ for `Secret`); use „Knoten“ for a cluster node as a general concept, `Node` when the API object is meant. +- Gender (a wrong-but-consistent article is a minor issue; a fluctuating one is major): **der** Cluster, der Pod, der Node, der Container, der Namespace, der Service, der Controller, der Ingress, der Server, der Speicher. **das** Deployment, das Secret, das Volume, das Image, das Manifest, das Backup — plus every `-ment` and every `-ing` noun (das Logging, das Monitoring, das Networking, das Scheduling). **die** Workload, die Ressource, die Instanz, die Pipeline. +- Acronyms take the gender of the German head noun: die API (Schnittstelle), die CRD (Definition), die CPU (Einheit), der RAM (Speicher), die IP-Adresse, das YAML (Format). +- Plural: English `-s` for most loanwords (die Pods, die Nodes, die Deployments, die Volumes, die Namespaces); **no** `-s` for `-er`/`-el` stems (der Cluster → die Cluster, der Container → die Container, der Server → die Server). Genitive singular takes `-s`: „des Pods“, „des Clusters“, „des Deployments“. +- Separate act from object: "the deployment of the app" → „die Bereitstellung der Anwendung“; the `Deployment` object → „das Deployment“. "to deploy" → „bereitstellen“/„ausrollen“, not „deployen“. + +## 4. Compound nouns (the biggest German MT failure mode) + +- All-German compounds are written as one word: ✗ „Speicher Klasse“ → ✓ „Speicherklasse“; ✗ „Netzwerk Richtlinie“ → ✓ „Netzwerkrichtlinie“; ✗ „Konfigurations Datei“ → ✓ „Konfigurationsdatei“. A space between the parts of a compound (Deppenleerzeichen) is always an error. +- Mixed English-German compounds are hyphenated at every joint: ✓ „Kubernetes-Cluster“, „Pod-Netzwerk“, „Container-Image“, „Cluster-Konfiguration“, „Node-Ausfall“, „Speicher-Backend“. +- Multiword English terms used as one noun get Durchkopplung (all joints hyphenated): ✗ „Control Plane Knoten“ → ✓ „Control-Plane-Knoten“; ✗ „Bare Metal Server“ → ✓ „Bare-Metal-Server“; ✓ „High-Availability-Setup“, „Multi-Tenant-Umgebung“, „Kubernetes-as-a-Service-Angebot“. +- Identifiers keep their exact original spelling inside a compound: „YAML-Datei“, „kubectl-Befehl“, „CSI-Treiber“, „VirtualMachine-Ressource“, „Helm-Chart“. +- Readability beats mechanical joining: with three or more elements, hyphenate the load-bearing joint or reformulate. ✗ „Kubernetesclusterkonfigurationsdatei“ → ✓ „Konfigurationsdatei des Kubernetes-Clusters“. +- Do not hyphenate a plain German compound that should be closed (✗ „Speicher-Klasse“ where „Speicherklasse“ exists), and never lowercase the resulting noun: ✓ „das Cluster-Upgrade“. + +## 5. Typography and punctuation + +- Quotation marks: „…“ (U+201E … U+201C); nested ‚…‘ (U+201A … U+2018). Never straight `"`, never English “…”. Do not put quotes around code — it is masked. +- Gedankenstrich: en dash **–** with spaces around it; never the em dash —. "Cozystack — a platform for…" → ✓ „Cozystack – eine Plattform für …“. Ranges use – without spaces: „3–5 Nodes“. The hyphen `-` is reserved for compounds. +- Ellipsis: the single character „…“, with a preceding space when it stands for omitted words. +- No Oxford comma: "A, B, and C" → ✓ „A, B und C“. +- Commas German requires and English does not: before every subordinate clause (dass, weil, wenn, ob, obwohl, relative clauses) and before extended infinitive groups with um/ohne/statt … zu. "We recommend that you upgrade first." → ✓ „Wir empfehlen, dass Sie zuerst aktualisieren.“; ✓ „…, um den Cluster zu aktualisieren.“ +- Lists: sentence fragments take no final period, full sentences do — consistently within one list. After a colon, continue lowercase unless a full sentence or a noun follows. + +## 6. Numbers, dates, units + +- Decimal **comma** in prose: "3.14" → ✓ „3,14“; "0.5 vCPU" → ✓ „0,5 vCPU“. CRITICAL: never convert inside code, version numbers (v1.5.0), image tags, IP addresses/CIDRs, ports, CLI output or YAML — those keep the source form byte-for-byte. +- Thousands separator: period, or a narrow space: "10,000 pods" → ✓ „10.000 Pods“. Prose only, same exception list as above. +- Dates: "07/24/2026" → ✓ „24.07.2026“ or „24. Juli 2026“. Never keep US month/day order. ISO dates in front matter and code stay ISO. Times: "3 PM" → ✓ „15:00 Uhr“. +- Units: space between number and unit, percent included — „4 GB“, „10 Gbit/s“, „50 %“, „25 °C“ (non-breaking space preferred). Kubernetes quantities (512Mi, 2000m) are code-like and stay unchanged. +- Ordinals take a period: „im 3. Schritt“. "billion" → „Milliarde“ (never „Billion“). + +## 7. Grammar traps when translating from English + +- Verbzweitstellung: the finite verb is the second element of a main clause. ✗ „Mit diesem Befehl Sie erstellen einen Cluster.“ → ✓ „Mit diesem Befehl erstellen Sie einen Cluster.“ +- Verbendstellung in subordinate clauses. ✗ „…, weil der Cluster hat nicht genug Speicher.“ → ✓ „…, weil der Cluster nicht genug Speicher hat.“ +- Modal verbs push the infinitive to the end. ✗ „Sie können erstellen eine virtuelle Maschine.“ → ✓ „Sie können eine virtuelle Maschine erstellen.“ +- Passive: prefer active or „lässt sich“; avoid stacked „wird … werden“ chains. "The cluster can be upgraded without downtime." → ✓ „Der Cluster lässt sich ohne Ausfallzeit aktualisieren.“ For a generic actor use the passive or man-Konstruktion, not „Sie“: "Backups are stored in S3." → ✓ „Backups werden in S3 abgelegt.“ +- Capability, not permission: "Cozystack allows you to run VMs" → ✗ „Cozystack erlaubt Ihnen, VMs auszuführen“ → ✓ „Mit Cozystack können Sie VMs betreiben“ / „Cozystack ermöglicht den Betrieb von VMs“. +- Gerunds are not present participles: "Using kubectl, you can…" → ✗ „Benutzend kubectl …“ → ✓ „Mit kubectl können Sie …“; "After installing…" → ✓ „Nach der Installation …“. +- Genitiv over „von“ in docs: ✓ „die Konfiguration des Clusters“; „von“ only without article or with bare plurals: ✓ „eine Liste von Nodes“. +- Restore articles English drops: "Create Deployment" → ✓ „Erstellen Sie ein Deployment“. Watch case after prepositions: „mit dem Cluster“, „für den Node“, „in der Umgebung“. +- No English noun stacking: ✗ „Cozystack Kubernetes Plattform“ → ✓ „die Kubernetes-Plattform Cozystack“. + +## 8. Translationese and calques to eliminate + +- ✗ „In diesem Artikel werden wir …“ → ✓ „Dieser Artikel zeigt, wie …“ / „Im Folgenden …“. +- ✗ filler „einfach“/„simpel“ ("simply run") → ✓ delete it: „Führen Sie … aus.“ +- ✗ „erlaubt Ihnen zu …“ → ✓ „ermöglicht …“ / „damit können Sie …“. +- ✗ dangling „basierend auf“ at the start of a clause → ✓ „auf Basis von“ / „anhand von“. +- ✗ „in Ordnung sein“ for "to be fine/OK" → ✓ „funktioniert“ / „ist zulässig“; ✗ „Sie wollen vielleicht …“ (you may want to) → ✓ „Optional können Sie …“. +- ✗ „Stellen Sie sicher, dass Sie X getan haben“ → ✓ „Vergewissern Sie sich, dass X …“ / „Voraussetzung: …“. +- ✗ „unterstützt“ for every sense of "support": feature support ✓ „unterstützt“, commercial support ✓ „Support“/„Betreuung“, "supported version" ✓ „unterstützte Version“. +- ✗ „adressieren“ (to address an issue) → ✓ „beheben“/„angehen“; ✗ „realisieren“ (to realize) → ✓ „umsetzen“ or „feststellen“. +- ✗ „Einmal der Cluster läuft, …“ → ✓ „Sobald der Cluster läuft, …“; ✗ over-nominalization „die Durchführung der Installation“ → ✓ „die Installation“; ✗ „Technologien wie z. B. …“ → ✓ „Technologien wie …“. + +## 9. False friends (EN ≠ DE) + +eventually ≠ eventuell → „schließlich/letztendlich“ (eventual consistency → „letztendliche Konsistenz“); actual/actually ≠ aktuell → „tatsächlich“ (current → „aktuell“); to control ≠ kontrollieren → „steuern/regeln“ (kontrollieren = to check); to become ≠ bekommen → „werden“; sensible ≠ sensibel → „sinnvoll/vernünftig“ (sensitive data → „sensible Daten“ ✓); provision ≠ Provision (= commission) → „bereitstellen/Bereitstellung“; note ≠ Note (= grade) → „Hinweis/Anmerkung“; billion ≠ Billion → „Milliarde“; consequent ≠ konsequent → „nachfolgend“; to handle ≠ handeln → „verarbeiten/behandeln“; map ≠ Mappe → „Zuordnung/Abbildung“; to spend ≠ spenden → „ausgeben“; brave ≠ brav → „mutig“; gift ≠ Gift (= poison) → „Geschenk“; chef ≠ Chef (= boss) → „Koch“. + +## 10. Reviewer checklist — German MT failure modes + +1. „du“/„dein“ anywhere, or a register switch mid-page. +2. English title case in headings, or a heading rendered as a full Sie-sentence where an Infinitiv belongs. +3. Deppenleerzeichen and missing Durchkopplung („Bare Metal Server“, „Control Plane Knoten“, „Kubernetes Cluster“). +4. Wrong or fluctuating gender for the same loanword; wrong plural („die Clusters“, „die Containers“); missing genitive `-s`. +5. Decimal points left in prose, or decimal commas wrongly injected into versions, IPs, image tags or code. +6. Em dash — or straight `"` instead of – and „…“. +7. Missing comma before dass/weil/wenn/relative clauses or before an extended infinitive group. +8. English word order surviving: verb not in second position, subordinate-clause verb not final, infinitive not at the end after a modal. +9. Untranslated leftovers — or the opposite: translated API kinds, flags, field names, or do_not_translate terms. +10. One source term rendered several ways on the same page; a glossary term overridden by an improvised synonym. +11. Calques from §8, especially „erlaubt Ihnen zu“, „in diesem Artikel werden wir“, filler „einfach“. +12. False friends from §9 — check every occurrence of „eventuell“, „aktuell“, „kontrollieren“, „sensibel“. +13. Sentences over ~25 words with several nested subordinate clauses: split them. German technical prose tolerates them worse than the English source suggests. diff --git a/hack/i18n/style-guides/es.md b/hack/i18n/style-guides/es.md index 1eb8d74f..1b2df25d 100644 --- a/hack/i18n/style-guides/es.md +++ b/hack/i18n/style-guides/es.md @@ -1,5 +1,89 @@ -- Neutral international Spanish (usable across Spain and Latin America); avoid region-specific slang. -- Address the reader with "tú" for docs/marketing (industry-accepted, friendly-professional) — consistent throughout. -- Follow kubernetes.io Spanish localization conventions. -- Keep English infra loanwords common in the ecosystem (pod, deployment, cluster→clúster, workload→carga de trabajo). -- Use inverted punctuation (¿ ¡) correctly. +Applies to both writing and reviewing: every rule below is also a review checkpoint. Code blocks, inline `code`, Hugo shortcodes, URLs, file paths and front-matter keys are masked by the pipeline (`§§NAME_N§§`) — never translate, reorder or "fix" them; the punctuation and number rules below apply to PROSE ONLY. + +## 1. Register and variety + +- Neutral international Spanish, usable in Spain and Latin America: no region-specific slang, no `vosotros`, no `vos`, and avoid the `ordenador`/`computadora` split (say `equipo`, `servidor`, `nodo`). +- Address the reader with **tú**, consistently on every page. Imperatives in `tú`: ✓ `Ejecuta`, `Crea`, `Consulta` — ✗ `Ejecute`, `Realicen`. A single `Consulte la guía` inside a `tú` page is a major finding. +- Docs: precise, imperative, short sentences, no first person except `En esta guía…`. Blog/landing: may transcreate, may use `nosotros` for the Cozystack team (`hemos publicado`), rhetorical questions allowed. +- Drop English politeness padding: `Please run…` → ✓ `Ejecuta…` — ✗ `Por favor, ejecuta…`. +- Gender-neutral wording by rephrasing (`el equipo`, `quienes administran el clúster`), never `@`, `x`, `e` or `usuarios/as`. + +## 2. Headings and capitalization — CRITICAL + +- Spanish uses **sentence case**, never English title case. `Getting Started with Cozystack` → ✓ `Primeros pasos con Cozystack` — ✗ `Primeros Pasos Con Cozystack`. +- Only the first word plus proper nouns, brands and API kinds are capitalized: ✓ `Cómo funciona el almacenamiento en LINSTOR`. +- `-ing` headings become a noun or an infinitive, as on kubernetes.io/es: `Installing Cozystack` → ✓ `Instalación de Cozystack` / `Instalar Cozystack` — ✗ `Instalando Cozystack`. Keep one pattern per page. +- Question headings take both marks: `How does it work?` → ✓ `¿Cómo funciona?`. +- No capital after a colon: ✓ `Nota: el clúster debe estar en ejecución.` — ✗ `Nota: El clúster…`. Capitalize after a colon only before a full quotation. +- Lowercase months, days, languages and nationalities: ✓ `el lunes`, `julio`, `en inglés`. +- Table headers and UI labels also follow sentence case. Do not drop the article for headline-ese: ✓ `Instalar el clúster` — ✗ `Instalar clúster`. + +## 3. Terminology policy (a do_not_translate + preferred list is injected separately — apply it first) + +Decision rule for a term in NEITHER list, in this order: +1. Is it an identifier the reader must match in a manifest, CLI or UI (`kubectl`, `namespace` inside a command, flags, field names, API kinds)? → keep verbatim, unchanged. +2. Does kubernetes.io/es or the CNCF Cloud Native Glossary already have a Spanish rendering? → use it. +3. Is there a natural, unambiguous Spanish word an engineer actually says? → translate (`rendimiento`, `red`, `almacenamiento`, `copia de seguridad`, `registros`). +4. Otherwise keep the English term bare and gloss it once, on first use: ✓ `sidecar (contenedor auxiliar)`, then `sidecar` alone. Never invent a translation nobody uses. +- Pick one rendering per term and keep it identical across the whole page. +- **API kinds in prose** keep the English name, the capital and no accent: ✓ `el Pod`, `el Deployment`, `los Secrets`, `el Node`, `el Tenant`. The same word as an ordinary concept is lowercase and Spanish: ✓ `un despliegue continuo`, `los nodos del clúster`, `el inquilino (tenant)`. Never inflect a kept term: ✗ `Deploymentos`, ✗ `kuberneteses`, ✗ `el Helm's chart`. +- **Gender** = gender of the implicit Spanish hypernym: el pod, el clúster, el nodo, el contenedor, el chart, el endpoint, el backend, el volumen, el despliegue, el almacenamiento, el espacio de nombres; la imagen, la red, la carga de trabajo, la API, la caché, la instancia, la máquina virtual. Agreement must follow: ✓ `una imagen firmada`, `los pods pendientes` — ✗ `la pod`, `el imagen`, `los clúster`. +- **Plurals**: adapted words take Spanish plurals (`clústeres`, `contenedores`, `servidores`); bare loanwords take `-s` (`los pods`, `los charts`, `los endpoints`). Acronyms are invariable: `las API`, `los CRD`. +- **Accent only when the adaptation is established** (RAE plus real ecosystem use): ✓ `clúster`, `búfer`, `caché`, `módulo`. Otherwise leave the loanword bare: ✓ `pod`, `chart`, `sidecar` — ✗ `pód`, ✗ `sídecar`. Do not italicize ecosystem loanwords. + +## 4. Typography and punctuation + +- Inverted marks are mandatory and go **where the question or exclamation actually starts**, not at the start of the sentence: ✓ `Si el nodo falla, ¿qué pasa con los datos?`, ✓ `Listo, ¡ya tienes un clúster!` — ✗ `¿Si el nodo falla, qué pasa con los datos?`, ✗ `Qué pasa con los datos?`. +- Quotes: use **"comillas inglesas"** (straight double quotes), not «comillas latinas». Rationale: kubernetes.io/es and the wider Latin American tech web use them, `«»` reads as Spain-specific and is inconsistent with the English source and with Markdown tooling. Nest as `"… 'anidado' …"`. Prefer **bold** over quotes for UI labels. +- **Raya (—)** for parentheticals, with a space outside and none inside: ✓ `El clúster —ya en ejecución— acepta cargas nuevas.` Never use `-` or `–` for this, and never leave a stray closing raya. In blog dialogue the raya opens the line with no space after it: `—Empezamos con tres nodos.` +- **No Oxford comma**: Spanish takes no comma before the `y`/`o` that closes a list. ✓ `nodos, discos y redes` — ✗ `nodos, discos, y redes`. (Exception only to prevent a real ambiguity.) +- Comma before an adversative or after a fronted subordinate clause: ✓ `Si el pod no arranca, revisa los eventos.`, ✓ `Funciona, pero requiere más memoria.` Never between subject and verb: ✗ `El operador de almacenamiento, crea el volumen.` +- Suspension points are always three, followed by a space. Use `:` rather than `;` to introduce lists and code blocks. + +## 5. Numbers, dates, units + +- **Decimal comma in prose**: `3.14 seconds` → ✓ `3,14 segundos` — ✗ `3.14 segundos`. NEVER convert inside version numbers, code, CLI output, YAML values, file names or CIDR notation: ✓ `Cozystack v1.5.0`, `10.0.0.1/24`, `cpu: 0.5`. +- Thousands: never a comma. `10,000 pods` → ✓ `10 000 pods` (non-breaking space) or `10000`; four digits stay unseparated (`1024`). +- Dates: `July 24, 2026` → ✓ `24 de julio de 2026` (lowercase month, `de` twice); numeric form `24/07/2026`. Never `Julio 24, 2026`. Front-matter `date:` values are masked — leave them. +- Space between number and unit, and before `%`: ✓ `16 GB`, `4 vCPU`, `500 ms`, `25 %`. Keep unit symbols in English/SI (`GB`, `TB`, `GiB`, `ms`). +- Abbreviations follow Spanish forms: `1.º`, `n.º`, `EE. UU.`. Spell out `segundos`, `minutos` in prose unless quoting a metric. + +## 6. Grammar traps when translating from English + +- **Gerund** — the top calque. Never gerund-as-adjective or gerund-of-consequence: ✗ `un archivo conteniendo la configuración` → ✓ `un archivo que contiene la configuración`; ✗ `El nodo falló, provocando un reinicio` → ✓ `El nodo falló y provocó un reinicio`. `Using X, you can…` → ✓ `Con X puedes…`. +- **Passive → pasiva refleja**, which Spanish strongly prefers: ✗ `El clúster es creado por el operador` → ✓ `El operador crea el clúster` / `El clúster se crea automáticamente`; ✗ `Los datos son almacenados en…` → ✓ `Los datos se almacenan en…`. +- **`you can`**: `puedes` when it is a real option, `es posible` in neutral docs — and often just drop it: `You can find the logs in /var/log` → ✓ `Los registros están en /var/log`. +- **Possessives → definite article**: `your cluster` → ✓ `el clúster` (`tu clúster` only when contrasting with someone else's); `Deploy your application` → ✓ `Despliega la aplicación`; `its configuration` → ✓ `la configuración`. +- **Noun stacking**: never a chain of four `de`. `Kubernetes cluster node pool autoscaling configuration` → ✗ `configuración de autoescalado de grupos de nodos de clúster de Kubernetes` → ✓ `configuración del autoescalado de los grupos de nodos en Kubernetes`. Break chains with an adjective (`red virtual`, `disco local`), a preposition (`en`, `para`) or a relative clause. Two consecutive `de` maximum. +- **ser vs estar**: identity or definition → `ser` (`Cozystack es una plataforma`); state, result, availability, location → `estar` (`el pod está listo`, `el servicio está disponible`, `el clúster está en ejecución`). ✗ `el pod es listo`. +- **Subjunctive** after `cuando` with future reference and after `asegúrate de que`: ✓ `Cuando el pod esté listo, ejecuta…` — ✗ `Cuando el pod está listo…`; ✓ `Asegúrate de que el nodo tenga suficiente memoria.` But `si` + present takes the indicative: ✓ `Si quieres aislar la carga…` — ✗ `Si quieras…`. +- Avoid the bureaucratic anaphoric `el mismo/los mismos`: ✗ `Descarga el chart y aplica el mismo` → ✓ `Descarga el chart y aplícalo`. + +## 7. Translationese and calques to fix + +- Filler openers: ✗ `En este artículo vamos a ver cómo…` → ✓ `Esta guía explica cómo…`, or start straight with the content. Delete `simplemente`, `básicamente`, `de hecho`, `es importante notar que` unless the English carries real emphasis. +- `support` → ✓ `admitir`, `ser compatible con`, `permitir` — ✗ `soportar` (that means "to endure"). `supported versions` → ✓ `versiones compatibles`; `unsupported` → ✓ `no compatible`. +- `apply`: ✓ `aplicar un manifiesto`, ✓ `esto se aplica a…`; but `apply a role` → ✓ `asignar un rol` and `apply for` → ✓ `solicitar` — ✗ `aplicar para`. +- `remove` → ✓ `eliminar`, `quitar` — ✗ `remover` (= to stir). `encrypt` → ✓ `cifrar`, `el cifrado` — ✗ `encriptar`. `library` → ✓ `biblioteca` — ✗ `librería`. `customize` → ✓ `personalizar` — ✗ `customizar`. +- Others: `deprecated` → `obsoleto`/`en desuso`; `performance` → `rendimiento`; `issue` → `problema`/`incidencia`; `feature` → `funcionalidad`; `release` → `versión`/`lanzamiento`; `setup` (n.) → `configuración`; `reset` → `restablecer`; `access` (v.) → `acceder a` (✗ `accesar`); `assume` → `suponer` (✗ `asumir`); `in order to` → `para` (✗ `en orden de`); `make sense` → `tener sentido` (✗ `hacer sentido`); `based on` → `según`/`a partir de` when `basado en` piles up; `additionally` → `además`. +- No English-style hyphenated compounds: ✗ `alta-disponibilidad`, ✗ `multi-inquilino` → ✓ `alta disponibilidad`, `multiinquilino`. No English possessive `'s`. +- Link text: never `haz clic aquí` → ✓ `Consulta la guía de instalación`. + +## 8. False friends (tech and business) + +`actually` → `en realidad`/`de hecho` (✗ `actualmente` = currently) · `eventually` → `con el tiempo`/`finalmente` (✗ `eventualmente` = occasionally) · `library` → `biblioteca` · `support` → `admitir` · `realize` → `darse cuenta` (✗ `realizar` = to carry out) · `sensible` → `sensato`/`razonable` (but `sensitive data` → `datos confidenciales`) · `assist` → `ayudar` (✗ `asistir` = to attend) · `consistent` → `coherente`/`uniforme` (✗ `consistente` = thick, solid) · `resume` → `reanudar` (✗ `resumir` = to summarize) · `topic` → `tema` (✗ `tópico` = cliché) · `large` → `grande` (✗ `largo` = long) · `ultimately` → `en última instancia` (✗ `últimamente` = lately) · `commodity hardware` → `hardware estándar` · `abort` → `cancelar`/`interrumpir` · `exit` → `salir` (✗ `éxito`) · `discrete` → `discreto` vs `discreet` → `prudente`. `argument`, `instance` and `policy` are fine as `argumento`, `instancia`, `política` in technical senses. + +## 9. Reviewer checklist — frequent MT failures in Spanish + +1. English Title Case leaking into headings, table headers or bold labels. +2. Missing opening `¿`/`¡`, or the mark placed at the sentence start instead of where the question begins. +3. Decimal point left as `.` in prose — or, worse, a decimal comma injected into a version, IP, CIDR or YAML value. +4. `usted`/`vosotros` drift, or mixed imperatives (`ejecuta` … `ejecute`) on one page. +5. Agreement drift around loanwords: `la pod`, `el imagen`, `los clúster`, `las nodos`. +6. Gerund calques (`conteniendo`, `permitiendo`, `resultando en`) and English passives left as `es/son` + participle. +7. `soportar`, `librería`, `encriptar`, `remover`, `customizar`, `accesar`, `en orden de`. +8. Missing article (`Instalar clúster`) or `de`-chains of four or more. +9. Capital after a colon; capitalized months or days; `Por favor` retained. +10. Terminology inconsistency: one English term rendered two ways on the same page, or a glossary/do_not_translate term inflected or translated. +11. Placeholders `§§NAME_N§§` lost, duplicated or reordered; code, URLs or YAML keys translated. +12. English clause order preserved in over-long sentences, comma splices, `el cual`/`el mismo` overuse, literal `aquí` link text. diff --git a/hack/i18n/style-guides/hi.md b/hack/i18n/style-guides/hi.md index 0ffcc103..93de8db1 100644 --- a/hack/i18n/style-guides/hi.md +++ b/hack/i18n/style-guides/hi.md @@ -1,3 +1,100 @@ -- Modern standard Hindi (Devanagari) as used in Indian tech writing; Hinglish is common — keep widely-used English technical terms in Latin script rather than forcing Sanskritized coinages. -- Keep ALL technical/product terms and most infra vocabulary in English/Latin (Kubernetes, cluster, pod, deployment, storage) — translate connective prose, not jargon. -- Professional, clear register. +# Hindi (hi) — style guide + +Modern standard Hindi (Devanagari) as written by Indian tech authors. Hinglish is normal and expected: keep widely-used English technical terms as English terms rather than forcing Sanskritized coinages. Translate the connective prose, not the jargon. Register: professional, clear, never bureaucratic (सरकारी हिंदी), never slangy. + +## 1. Register and address + +- Address the reader as **आप** only. Never तू/तुम. Verbs take the आप form: करें, चलाएँ, देखें, कर सकते हैं. +- Imperative style: use the **-एँ** form consistently (चलाएँ, बनाएँ, जोड़ें). Do not mix in कीजिए/कीजिये or करो within a page. +- Docs: terse, instructional, present tense. Blog: same person, slightly warmer; the project may speak as हम. +- No exclamation marks, no rhetorical questions, no emoji, no filler (आइए जानते हैं, तो चलिए शुरू करते हैं). +- EN "You can now access the dashboard." → ✗ "अब आप डैशबोर्ड को एक्सेस कर सकते हैं!" → ✓ "अब आप dashboard खोल सकते हैं।" + +## 2. How much English to keep — the central rule + +Test each term with one question: **would an Indian DevOps engineer say this word in English while speaking Hindi?** If yes, it stays an English word. Then decide the script by bucket: + +1. **Latin script, verbatim** — anything the reader must type, match, or click: product and project names (Cozystack, Kubernetes, Talos, Flux), CLI names and flags (kubectl, --namespace), API kinds and fields (Pod, Deployment, Secret, Node, ConfigMap, StorageClass, spec.replicas), file names, env vars, UI labels, acronyms (API, CNI, CSI, RBAC, TLS, YAML, GPU). +2. **Devanagari transliteration of the English word** — only for the general infra nouns the injected glossary assigns a Hindi rendering (e.g. cluster/node/storage as ordinary nouns). Use the glossary form exactly; never invent new transliterations for terms it does not list. +3. **Real Hindi** — everything else: verbs, adjectives, connectors, and ordinary nouns (उपलब्ध, कॉन्फ़िगर करें, इसके बाद, ध्यान दें, अनुमति, आवश्यकता). + +Default for a term not covered by the glossary: **keep it in Latin.** Never coin a Devanagari equivalent for infra jargon. + +- ✗ संगणक, कुंजीपटल, अभिकलन, स्मृति, संचिका, जालक्रम, कूटशब्द, आभासी संगणित्र → ✓ computer, keyboard, compute, memory, file, network, password, VM +- EN "Configure the ingress controller and the load balancer." → ✗ "प्रवेश नियंत्रक और भार संतुलक को विन्यस्त करें।" → ✓ "ingress controller और load balancer कॉन्फ़िगर करें।" +- EN "Kubernetes schedules the pod on a node." → ✗ "कुबेरनेट्स पॉड को एक नोड पर शेड्यूल करता है।" (brand transliterated) → ✓ "Kubernetes पॉड को नोड पर schedule करता है।" +- Over-translation is as bad as under-translation: EN "high availability" → ✗ "उच्च उपलब्धता" is acceptable only if the glossary says so; otherwise keep "high availability". + +## 3. Headings + +- Devanagari has no letter case — there is nothing to title-case. Do not capitalize or restyle; just write a natural phrase. +- No terminal । in headings. No trailing colon. +- English terms inside a heading keep exactly the form used in the body — same script, same capitalization. ✗ heading "क्लस्टर इंस्टॉल करना" + body "cluster" (script drift). +- Prefer verbal nouns or short noun phrases, consistently within a page: EN "Installing Cozystack" → ✓ "Cozystack इंस्टॉल करना"; EN "Prerequisites" → ✓ "आवश्यक शर्तें"; EN "Troubleshooting" → ✓ "समस्या निवारण". + +## 4. Terminology policy (terms outside the glossary) + +Decision order: glossary `do_not_translate` → verbatim Latin; glossary `preferred` → that exact rendering, every occurrence; otherwise apply §2 and default to Latin. + +- **API kinds stay Latin and singular in prose**: "Pod को delete करें", "Deployment में replicas बढ़ाएँ", "यह Secret namespace में बनता है". Never transliterate a kind (✗ डिप्लॉयमेंट ऑब्जेक्ट for `kind: Deployment`). +- Mirror the glossary's Tenant/tenant split: capitalized API kind (`Node`, `Tenant`) → Latin; the same word as an ordinary noun (a node, a tenant) → the glossary's Devanagari form. +- **First mention**: when a Devanagari-rendered term first appears on a page, gloss it once — "टेनेंट (tenant)" — then use the plain form. Do not repeat the gloss. +- One term = one rendering per page and per docs version. If you chose Latin once, it is Latin everywhere on that page. +- Never attach Devanagari inflection to a Latin word: ✗ Podों, ✗ Deploymentस, ✗ नोडnode. Express plurality with a quantifier: ✓ "कई Pod", "सभी Node", "दो PVC". + +## 5. Script and typography + +- End full sentences in prose with **पूर्ण विराम ।**, including sentences that end on a Latin word: ✓ "इसके बाद `kubectl apply` चलाएँ।" Never write ".।". Never place । inside code, headings, or list fragments that are not sentences. +- Keep the period for version numbers, decimals, abbreviations and anything inside code (v1.2.5). +- Use straight double quotes " " for quoted UI strings and values; keep the quoted English string in Latin. Avoid mixed ‘ ’/“ ” pairs. +- One space between a Devanagari run and an adjacent Latin/number run; no space before ।, comma, or a closing bracket. Postpositions after Latin words are separated by a space: "Node पर", "namespace में". +- Never mix scripts inside a single word, and never transliterate identifiers, flags, paths, or commands. + +## 6. Numbers, dates, units + +- **Western Arabic digits only** (1, 2, 3). Never Devanagari digits (१, २, ३). +- Use **international grouping** — 1,000 / 1,000,000. Do **not** use Indian grouping (✗ 10,00,000) and never convert to lakh/crore in technical content. +- Decimal point is `.` — ✓ 1.5 GB. +- Dates: "24 जुलाई 2026" in prose; ISO `2026-07-24` when the source is ISO. Never MM/DD/YYYY. +- Units keep Latin symbols with a space: 16 GB, 500 ms, 4 vCPU, 10 Gbit/s. Percent has no space: 50%. Ranges: "2–4 GB" or "2 से 4 GB". + +## 7. Grammar traps (EN → HI) + +- **SOV order.** EN "Cozystack deploys the application to the cluster." → ✗ "Cozystack डिप्लॉय करता है application को क्लस्टर में।" → ✓ "Cozystack application को क्लस्टर में deploy करता है।" The verb goes last. +- **Gender of English loanwords.** Consonant-final loans are **masculine**: क्लस्टर, नोड, पॉड, सर्वर, नेटवर्क, स्टोरेज, बैकअप, प्लेटफ़ॉर्म. Feminine: फ़ाइल, मशीन, सर्विस, इमेज, and -ी endings (मेमोरी, डायरेक्टरी, लाइब्रेरी). Keep gender consistent across the whole page: ✗ "क्लस्टर बनाई गई" → ✓ "क्लस्टर बनाया गया"; ✗ "फ़ाइल बनाया गया" → ✓ "फ़ाइल बनाई गई". +- **का/की/के agreement** follows the *possessed* noun: ✓ "क्लस्टर की स्थिति", "नोड का नाम", "Pod के logs". +- **Postpositions.** in → में, on → पर, from → से, for → के लिए. ✗ "क्लस्टर के लिए deploy करें" for "deploy to the cluster" → ✓ "क्लस्टर में deploy करें". +- **Ergative ने** only with perfective transitive verbs: ✓ "हमने क्लस्टर बनाया"; ✗ "Cozystack ने क्लस्टर बनाता है" (imperfective — no ने); ✗ ने with intransitives ("Pod ने शुरू हुआ"). +- **Compound verbs** read naturally; do not strip them: ✓ "config फ़ाइल लिख दें", "logs देख लें" where the source implies completion. Do not add them where the source is neutral. +- Honorific verb forms must stay consistent — do not switch between "करें" and "करते हैं" for the same instructional voice. + +## 8. Translationese and calque patterns to fix + +- **"आप ... कर सकते हैं" everywhere.** In step-by-step docs use the imperative. EN "You can create a tenant with kubectl." → ✗ "आप kubectl का उपयोग करके एक टेनेंट बना सकते हैं।" → ✓ "kubectl से टेनेंट बनाएँ।" +- **एक as an article.** Hindi has no indefinite article. ✗ "एक क्लस्टर बनाएँ" → ✓ "क्लस्टर बनाएँ". Keep एक only when the count matters. +- **Literal passive.** EN "The cluster is created by the operator." → ✗ "क्लस्टर ऑपरेटर के द्वारा बनाया जाता है।" → ✓ "operator क्लस्टर बनाता है।" +- **English-order relative clauses.** EN "the node that runs the workload" → ✗ "नोड जो वर्कलोड चलाता है" → ✓ "वर्कलोड चलाने वाला नोड" (or जो … वह with correct correlative). +- **"का उपयोग करके" / "के माध्यम से" stacked in every sentence** → prefer "से": ✓ "Helm से इंस्टॉल करें". +- **"यह सुनिश्चित करें कि" / "कृपया"** in every step — drop them; docs are already polite via आप. +- Word-by-word renderings of English idiom ("out of the box" → ✗ "डिब्बे से बाहर") → ✓ "बिना अतिरिक्त कॉन्फ़िगरेशन के" or keep "out of the box". + +## 9. Reviewer checklist (MT failure modes) + +1. Sanskritized coinage anywhere (संगणक, कुंजीपटल, अभिकलन, स्मृति, संचिका) → reject. +2. Brand/product transliterated (कुबेरनेट्स, कोज़ीस्टैक, कुबेक्टल) → must be Latin. +3. Same term in two scripts or two spellings on one page (cluster vs क्लस्टर, स्टोरेज vs भंडारण). +4. Gender drift on loanwords across paragraphs; का/की/के disagreement. +5. English SVO order preserved; verb not final. +6. Wrong postposition, or में/पर/के लिए swapped. +7. ने inserted with imperfective or intransitive verbs. +8. Passive constructions copied from English. +9. "आप … कर सकते हैं" or "एक" repeated in nearly every sentence. +10. Devanagari digits, Indian digit grouping, lakh/crore, MM/DD dates. +11. Missing or doubled ।; । after a heading; । inside code. +12. Devanagari suffixes glued to Latin words (Podों), or a translated flag/identifier (`--नाम`). +13. Mixed imperative registers (चलाएँ vs चलाइए vs चलाओ) in one page. +14. Terminology diverging from the injected glossary. + +## 10. Pipeline note + +Code blocks, inline code, Hugo shortcodes, URLs, HTML attributes and front-matter keys are masked structurally by the pipeline. Never translate, reorder, transliterate or re-space anything inside a mask; front-matter `title` and `description` values are translated, their keys are not. diff --git a/hack/i18n/style-guides/pt-br.md b/hack/i18n/style-guides/pt-br.md index 1cff6cc6..d988e996 100644 --- a/hack/i18n/style-guides/pt-br.md +++ b/hack/i18n/style-guides/pt-br.md @@ -1,5 +1,80 @@ -- Brazilian Portuguese (pt-BR), not European Portuguese. -- Address the reader with "você"; friendly-professional register. -- Follow kubernetes.io Brazilian Portuguese localization conventions. -- Keep English infra loanwords common in the BR tech community (pod, deploy/deployment, cluster, workload→carga de trabalho). -- Natural BR phrasing over literal calques. +Brazilian Portuguese (pt-BR), never European Portuguese. Follow kubernetes.io pt-BR localization conventions and the CNCF Cloud Native Glossary. Natural BR phrasing beats literal calques: if a sentence can only be parsed by reconstructing the English, rewrite it. Code blocks, inline `code`, Hugo shortcodes, URLs, front-matter keys and §§PLACEHOLDERS§§ are masked by the pipeline — never translate or reorder them. + +## 1. Register and address + +- Always **você** (explicit or implicit), consistently within a page. Never *tu*, *vós*, *o senhor*, and never mix conjugations: ✗ "se tu quiseres", ✗ "podes ver" → ✓ "se você quiser", "você pode ver". +- Friendly-professional. Docs: imperative, short sentences, one instruction per sentence ("Execute o comando", "Verifique o status"). Drop English politeness: "Please run" → ✗ "Por favor, execute" → ✓ "Execute". +- Blog/landing: transcreate. First person plural is fine ("lançamos", "nossa equipe"); avoid hype adjectives — *powerful* → ✗ "poderoso" → ✓ "robusto", "eficiente", or drop. +- The product takes an article in running prose: ✓ "o Cozystack instala…", "no Kubernetes"; omit it in labels and titles. + +## 2. Headings and capitalization (critical) + +- Portuguese uses **sentence case**, never English title case: capitalize only the first word and proper nouns. "Installing the Storage Backend" → ✗ "Instalando o Backend de Armazenamento" → ✓ "Instalação do backend de armazenamento". +- Prefer a noun phrase (or infinitive) over the English gerund heading, consistently within a page: "Configuring Networking" → ✗ "Configurando a Rede" → ✓ "Configuração de rede" / "Configurar a rede". +- Do not capitalize common nouns mid-sentence because English did: "the Cluster and the Node" → ✓ "o cluster e o nó". +- Same rule for `title`/`description` front matter, table headers, alt text, button and menu labels. + +## 3. Terminology policy (a glossary is injected separately — do not restate it) + +- Decision rule for terms **not** in the glossary, in order: (1) kubernetes.io pt-BR translates it → use that; (2) the CNCF Glossary pt-BR has it → use that; (3) a BR platform engineer would say the English word out loud in a standup → keep English; (4) otherwise translate, with the English in parentheses on first use only. +- Kept in English (real BR usage): pod, cluster, deploy, container, namespace, commit, pull request, backup, snapshot, endpoint, cache, log, bucket, overhead, hypervisor, bare metal, load balancer. +- Translated: workload → carga de trabalho, node → nó, storage → armazenamento, network → rede, image → imagem, file → arquivo, feature → recurso, wizard → assistente, release → versão/lançamento. +- **API objects**: when the word names a Kubernetes/Cozystack API kind, keep it capitalized and in English — "o Pod", "um Deployment", "o Secret", "o objeto Node", "o Tenant" (must match `kind:` in the manifest). When it is the everyday concept, translate and lowercase: "os pods do cluster", "os nós do cluster", "os segredos da aplicação". +- **Gender**: masculine by default for English loanwords (o pod, o cluster, o deploy, o container, o backup, o node, o namespace, o endpoint); feminine when the underlying PT head noun is feminine (a imagem, a API, a VM, a URL, a tag, a role). ✗ "a cluster", ✗ "o VM", ✗ "a pod". +- **Plural**: inflect with -s, no apostrophe — os pods, os clusters, os deployments, os namespaces, as VMs, as APIs, os PVCs. ✗ "os pod", ✗ "as VM's". Adjectives agree: "pods prontos", "cargas de trabalho críticas". + +## 4. Typography and punctuation + +- Quotation marks: **straight double quotes** `"…"`, nested `'…'`. Do not use «guillemets» (pt-PT/literary) or curly quotes — the docs are code-adjacent and quoted strings get copied into shells. +- **Travessão (—)** with spaces for parenthetical asides; keep the source's em dashes, never downgrade them to a hyphen: ✓ "O tenant — a unidade de isolamento — recebe seu próprio namespace." +- **No Oxford comma.** Portuguese forbids a comma before *e*/*ou* closing an enumeration: "storage, networking, and compute" → ✗ "armazenamento, rede, e computação" → ✓ "armazenamento, rede e computação". +- Comma after a fronted adverbial or subordinate clause: ✓ "Depois de instalar o Cozystack, aplique o manifesto." Never a comma between subject and verb, however long the subject: ✗ "O cluster com três nós de controle, requer…". +- **Crase (à)** — verify every occurrence. Use it when preposition *a* + feminine article *a* merge: ✓ "acesso à API", "conecte-se à rede", "à medida que", "devido à latência", "graças à replicação". No crase before masculine nouns, verbs or article-less plurals: ✗ "à partir de" → ✓ "a partir de"; ✗ "à fim de" → ✓ "a fim de"; ✗ "acesso à documentos" → ✓ "acesso a documentos"; ✓ "referente ao cluster". +- Use "e", never "&". Keep `%`, `/` and CLI punctuation exactly as in the source. + +## 5. Numbers, dates, units + +- **Decimal comma in prose**: "3.14" → ✓ "3,14"; "0.5 vCPU of overhead" → ✓ "0,5 vCPU de overhead". Thousands separator is a period: "1,000 nodes" → ✓ "1.000 nós"; "12,500" → ✓ "12.500". +- **Never** convert inside code, CLI output, YAML values, resource quantities, CIDRs or version numbers: `v1.2.3`, `0.5`, `500m`, `10.0.0.0/8` stay exactly as written — including when quoted in prose. +- Dates: "July 24, 2026" → ✓ "24 de julho de 2026" (month lowercase); numeric 24/07/2026; ranges "de 2024 a 2026". 24-hour clock: "9 AM UTC" → ✓ "9h UTC" / "09:00 UTC". +- Units: space between number and unit (10 GB, 500 ms, 2 vCPU, 4 GiB); no space before `%` (50%); keep byte/rate units in standard casing (GiB, TiB, Mbps). + +## 6. Grammar traps translating from English + +- **Gerundismo / -ing calque** — the notorious BR error. English -ing → infinitive or noun; never "estar + gerúndio" for a future or habitual action: ✗ "vamos estar enviando o manifesto" → ✓ "vamos enviar o manifesto"; "Before installing, check the requirements" → ✗ "Antes de instalando" → ✓ "Antes de instalar, verifique os requisitos". Genuine progressives are fine: ✓ "o pod está iniciando". +- **Passive**: agentless English passive → active imperative or synthetic passive with *se*: "The manifest is applied automatically" → ✓ "Aplica-se o manifesto automaticamente" / ✓ "O Cozystack aplica o manifesto automaticamente". In procedures, prefer the imperative. +- "you can" → alternate "você pode" with impersonal "é possível" so a page does not repeat "você pode" ten times. +- **Possessives → definite article**: "Open your terminal and check your cluster" → ✗ "Abra o seu terminal e verifique o seu cluster" → ✓ "Abra o terminal e verifique o cluster". +- **Noun stacking → de-chains, max two links**: "Kubernetes cluster node pool autoscaling configuration" → ✗ "configuração do autoscaling do pool de nós do cluster Kubernetes" → ✓ "configuração do autoscaling para pools de nós (cluster Kubernetes)". +- **ser vs estar**: definitional → *ser* ("O Cozystack é uma plataforma"); state/condition → *estar* ("O cluster está pronto", "o pod está em execução"). ✗ "O cluster é pronto". +- **Future subjunctive after quando/se/assim que**: "When you create the tenant" → ✗ "Quando você cria o tenant" → ✓ "Quando você criar o tenant"; "If the pod fails" → ✓ "Se o pod falhar"; "as soon as it is ready" → ✓ "assim que estiver pronto". +- **Colocação pronominal (BR = próclise)**: ✗ "Não aplica-se a clusters legados" → ✓ "Não se aplica a clusters legados". Never open a sentence with an unstressed pronoun; avoid pt-PT mesoclisis ✗ "permitir-lhe-á" → ✓ "vai permitir que você". +- **Future tense**: BR prefers "vai + infinitivo" or the simple present: "This will create a namespace" → ✗ "Isto irá criar um namespace" → ✓ "Isso cria um namespace". + +## 7. Translationese and calques to eliminate + +- "In this article, we will…" → ✗ "Neste artigo, nós vamos…" → ✓ "Veja como…" / "Este guia mostra…". +- Delete filler — *simply*, *just*, *easily*: ✗ "simplesmente execute" → ✓ "execute". +- support → ✗ "suportar" (= to endure) → ✓ "oferecer suporte a", "ser compatível com", "aceitar": "Cozystack supports GPU passthrough" → ✓ "O Cozystack oferece suporte a GPU passthrough". +- application → ✓ "aplicação" (server-side workloads); "aplicativo" only for mobile/desktop apps. +- encrypt → ✗ "encriptar" → ✓ "criptografar" / "criptografia". library → ✗ "livraria" → ✓ "biblioteca". customize → ✗ "customizar" → ✓ "personalizar". delete → ✗ "deletar" → ✓ "excluir"/"remover". set → ✗ "setar" → ✓ "definir"/"configurar". address (an issue) → ✗ "endereçar" → ✓ "tratar"/"resolver"/"corrigir". +- requirement → ✗ "requerimento" (= a petition) → ✓ "requisito". performance → ✓ "desempenho". check → ✓ "verificar" (not "checar"). issue → ✓ "problema" (keep "issue" only for a GitHub issue). +- "Once you have installed…" → ✗ "Uma vez que você instalou" (= *because*) → ✓ "Depois de instalar" / "Assim que instalar". "Assuming that…" → ✓ "Supondo que". "in order to" → ✓ "para" (not "de forma a"). "make sure" → ✓ "certifique-se de que"; "note that" → ✓ "observe que"; "keep in mind" → ✓ "lembre-se de que". + +## 8. False friends (EN → PT) + +actually → ✓ "na verdade" (✗ atualmente = *currently*) · eventually → ✓ "por fim", "com o tempo" (✗ eventualmente = *occasionally*) · library → ✓ biblioteca · support → ✓ oferecer suporte a · realize → ✓ "perceber", "notar" (✗ realizar = *to carry out*) · pretend → ✓ "fingir" (✗ pretender = *to intend*) · push (git) → keep "push" or ✓ "enviar" (✗ "empurrar") · assume → ✓ "supor", "presumir" (✗ assumir = *to take on*) · resume → ✓ "retomar" (✗ resumir = *to summarize*) · comprehensive → ✓ "abrangente" (✗ compreensivo = *understanding*) · sensible → ✓ "sensato" (✗ sensível = *sensitive*) · attend → ✓ "participar de" (✗ atender = *to serve*) · exit → ✓ "sair" (✗ êxito = *success*) · policy → ✓ "política" (never "polícia") · parent (resource) → ✓ "pai"/"superior". + +## 9. Reviewer checklist — pt-BR machine-translation failure modes + +1. **European Portuguese leaking in** — reject on sight: ficheiro→arquivo, utilizador→usuário, ecrã→tela, rato→mouse, aceder a→acessar, guardar→salvar, apagar→excluir, equipa→equipe, registo→registro, facto→fato, contacto→contato, "está a executar"→"está executando", connosco→conosco, gestor→gerenciador, telemóvel→celular. +2. English **title case** surviving in headings, `title`/`description`, table headers or link text. +3. Decimal points left as "3.14"/"1,000" in prose — or commas wrongly injected into code, versions, CIDRs. +4. Missing or hypercorrect **crase** ("à partir de", "acesso a API"). +5. Comma before *e*/*ou*, or a comma splitting subject from verb. +6. Gender/number disagreement on loanwords ("a cluster", "os pod", "VM's"). +7. Gerundismo, "irá + infinitivo" future, enclisis after negation ("não aplica-se"). +8. Literal "suportar", "customizar", "deletar", "encriptar", "endereçar", "livraria", "uma vez que", "simplesmente". +9. Register drift: *tu*/*você* mixing, "por favor" in imperatives, marketing hype in docs, stiff formality in blog. +10. Untranslated or dropped sentences; translated API kinds, CLI flags, YAML keys or URLs — all hard failures. +11. Glossary terms rendered inconsistently across a page (e.g. "nó" in one paragraph, "node" in the next). diff --git a/hack/i18n/style-guides/ru.md b/hack/i18n/style-guides/ru.md index 237450d1..df4fd6c3 100644 --- a/hack/i18n/style-guides/ru.md +++ b/hack/i18n/style-guides/ru.md @@ -1,5 +1,96 @@ -- Обращение к читателю — на «вы» со строчной буквы (нейтрально-профессионально). -- Инфинитивные заголовки («Установить кластер»), как в kubernetes.io/ru. -- Англицизмы, укоренившиеся в инфре, не переводить силой: «под» (pod — можно оставлять pod в коде), «нода»/«узел» — «узел» в прозе, «деплой»/«развёртывание» — предпочитать «развёртывание». -- Кавычки — «ёлочки». Тире — длинное (—). -- Не переводить: kubectl, namespace (в командах), CLI-флаги. +Этот файл — одновременно инструкция переводчику и рубрика для ревью. Каждый пункт проверяем. +Код-блоки, инлайн-код, шорткоды Hugo, URL и ключи front matter замаскированы пайплайном — их не переводят и не трогают ни при каких условиях; правила ниже касаются только прозы. + +## 1. Регистр и обращение + +- Обращение к читателю — на «вы» со строчной буквы (нейтрально-профессионально). «Вы» с прописной — ошибка. «Ты» — ошибка. +- Документация: безличный/императивный тон, без «мы», без эмоций. EN «We recommend using X» → ✗ «Мы рекомендуем использовать X» → ✓ «Рекомендуется использовать X». +- Блог и лендинг: «мы» допустимо, если это команда проекта; можно транскреация, но без восклицательных знаков, превосходных степеней и рекламных клише («революционный», «мощнейший»). +- EN «Please note that…» → ✓ «Обратите внимание:…» (никогда «пожалуйста»). EN «Let's create…» → ✓ «Создайте…». + +## 2. Заголовки + +- Инфинитивные заголовки, как в kubernetes.io/ru: EN «Installing a cluster» / «Install a cluster» → ✗ «Установка кластера» → ✓ «Установить кластер». +- Заголовки-существительные оставляем существительными: «Architecture» → «Архитектура», «Requirements» → «Требования». +- Заголовки-вопросы сохраняем: «Why Cozystack?» → «Почему Cozystack?». +- Английский Title Case → русская прописная только в первом слове и в именах собственных: «Configure Storage Backend» → ✗ «Настроить Хранилище Данных» → ✓ «Настроить бэкенд хранилища». +- Точка в конце заголовка не ставится. То же правило применяется к `title` и `description` во front matter. + +## 3. Терминология: правило решения (для терминов вне глоссария) + +Глоссарий (do_not_translate + preferred) инжектится отдельно и имеет приоритет. Для всего остального — по порядку: +1. Есть устоявшийся перевод в kubernetes.io/ru или CNCF Cloud Native Glossary → берём его. +2. Нет перевода, но термин читатель видит в CLI, UI, логах или манифестах → оставить латиницей, не склонять, добавить родовое слово: «в объекте PersistentVolumeClaim», «через контроллер Flux». +3. Есть точный русский аналог, не мешающий поиску → перевести, при первом упоминании на странице дать оригинал в скобках: «отказоустойчивость (fault tolerance)». Дальше — только русский вариант. +4. Транслитерация допустима только для уже укоренившихся форм: под, тенант, кластер, бэкенд, деплой. Не изобретать новых («кубелет», «инстанс» в доке — ✗, «экземпляр» — ✓). +5. Один термин — один перевод на всей странице и на всём сайте. Синонимическое разнообразие здесь — дефект, а не стиль. + +Объекты API Kubernetes: +- Как kind в манифесте — латиницей, с прописной, без склонения, с родовым словом: «создайте ресурс Deployment», «в поле spec объекта Secret». +- В обычной прозе, как нарицательное — по-русски и склоняется: pods → «поды», nodes → «узлы» (не «ноды»), secrets → «секреты», namespaces → «пространства имён» (но в командах и в описании флагов — namespace как есть). +- Устоявшиеся англицизмы инфраструктуры не переводить силой: «под» (в коде — pod), «узел» вместо «нода» в прозе, «развёртывание» предпочтительнее «деплоя». +- Не переводить и не склонять: kubectl, namespace (в командах), CLI-флаги, имена полей YAML, названия продуктов. + +## 4. Типографика и пунктуация + +- Кавычки — «ёлочки»; вложенные — „лапки“: «параметр „replicas“ в манифесте». ASCII-кавычки (" ') и “…” в прозе запрещены; внутри инлайн-кода остаются как есть. +- Тире — длинное (—), отбивается пробелами, слева — неразрывным. Дефис (-) — только внутри слов: «бэкап-политика», «SLA-требования». Диапазоны — короткое тире без пробелов: «3–5 узлов», «2024–2026». +- Пропуск глагола-связки оформляется тире: «Cozystack — платформа на базе Kubernetes». +- Многоточие — один символ «…», не «...». +- Буква ё обязательна: «развёртывание», «учётные данные», «объём», «затем». +- Неразрывный пробел: между числом и единицей («16 ГиБ», «3 узла»), внутри сокращений («и т. д.», «т. е.»), после коротких предлогов и союзов в начале синтагмы («в кластере», «с помощью»), перед длинным тире. +- Знаки препинания не отбиваются пробелом слева; двойных пробелов не бывает. +- Списки: если элементы продолжают вводную фразу — со строчной буквы и «;» на конце, последний — «.». Если элементы самостоятельные предложения — с прописной и точкой. Английский стиль (Title Case в пунктах, точки через один) не переносить. + +## 5. Числа, даты, единицы + +- Десятичный разделитель в прозе — запятая: «3.14» → «3,14»; «0.5 CPU» → «0,5 CPU». КРИТИЧНО: внутри кода, версий, тегов, значений YAML, размеров в командах разделитель НЕ меняется никогда: `v1.2.5`, `cpu: 0.5`, «версия 1.2.5» остаются с точкой. +- Разряды тысяч — неразрывный пробел, не запятая: «10,000 pods» → «10 000 подов». +- Проценты — слитно с числом: «50%». Диапазон — «10–15%». +- Единицы измерения в техническом контексте — латиницей и в оригинальной форме (GiB, Mi, vCPU, IOPS, ms), так как совпадают с тем, что читатель видит в манифестах. В блоге и маркетинге допустима кириллица (ГБ, ТБ) — но единообразно в пределах страницы. +- Даты в прозе — по-русски: «July 24, 2026» → «24 июля 2026 года». В командах, тегах, front matter и changelog формат ISO не трогать. +- Числа и существительные согласуются: «1 узел», «2 узла», «5 узлов». MT часто оставляет «5 узла» — проверять. + +## 6. Грамматические ловушки при переводе с английского + +- Артикли исчезают: «a cluster», «the node» → «кластер», «узел». Не превращать «a» в «один», «the» в «данный». +- Английский пассив → русский актив или безличная конструкция: «The volume is mounted by kubelet» → ✗ «Том монтируется kubelet-ом» → ✓ «Том монтирует kubelet» / ✓ «Том монтируется автоматически». +- Герундий в начале фразы → предлог или деепричастие, но не цепочка «используя»: «Using Flux, you can…» → ✗ «Используя Flux, вы можете…» → ✓ «С помощью Flux можно…». +- «You can / you need to» в документации → безлично: «You can override defaults» → ✓ «Значения по умолчанию можно переопределить». «Вы можете» оставляем только там, где важно противопоставление ролей (что может пользователь, а что — администратор). +- Цепочки существительных разворачиваются в родительный падеж, но не длиннее двух звеньев подряд: «Kubernetes cluster node pool configuration» → ✗ «конфигурация пула узлов кластера Kubernetes» → ✓ «настройка пулов узлов в кластере Kubernetes». Третий родительный подряд — разбить предлогом или прилагательным. +- Порядок слов: известное — в начало, новое — в конец. «A new tenant is created by the administrator» → ✓ «Новый тенант создаёт администратор» (если рема — кто) / ✓ «Администратор создаёт новый тенант» (если рема — что). +- Вид глагола: инструкция — совершенный вид («Создайте секрет», «Примените манифест»); описание повторяющегося поведения — несовершенный («Контроллер проверяет состояние каждые 30 секунд»). MT часто ставит несовершенный в шагах — ошибка. +- Согласование с аббревиатурами — по роду опорного слова: «API возвращает» (интерфейс), «ОС поддерживает» (система), «СУБД настроена» (система). Латинские аббревиатуры не склоняем: «через API», не «через APIшку». +- Английские имена собственные из списка do_not_translate не склоняются; если фраза становится неуклюжей — добавить родовое слово: ✗ «в Талосе» → ✓ «в Talos Linux», ✗ «Cozystack'ом» → ✓ «средствами Cozystack». +- Отрицание не терять: «must not be empty» → «не должно быть пустым», а не «должно быть пустым». + +## 7. Переводческие штампы и кальки (вычищать) + +- «В данной статье мы рассмотрим…» → «Здесь описано…» / «В статье описано…». «Данный» → «этот». +- «является» вместо тире: «Cozystack является платформой» → ✓ «Cozystack — платформа». +- «позволяет вам», «предоставляет возможность», «дает возможность» → «позволяет», «даёт», а лучше прямое действие: «Flux allows you to sync manifests» → ✓ «Flux синхронизирует манифесты». +- «просто», «легко», «всего лишь» как калька от «simply/easily» — удалять: «Simply run the command» → ✓ «Выполните команду». +- «поддерживается» в каждом абзаце (support): «X supports Y» → «X работает с Y» / «в X есть Y» / «X совместим с Y» — варьировать по смыслу. +- «для того чтобы» (от «in order to») → «чтобы»; «в случае, если» → «если»; «в настоящее время» → «сейчас»; «осуществлять настройку» → «настраивать». +- «Обратите внимание, что» — только там, где в оригинале Note/Warning. Цепочки «который… который…» — разбивать на предложения. +- Буквальные «пожалуйста», «наслаждайтесь», «мы рады сообщить» — не переносить. + +## 8. Ложные друзья + +actual → «фактический», не «актуальный» · consistent → «согласованный», «единообразный», не «консистентный» · control (control plane, access control) → «управление», не «контроль» · validate → «проверить», не «валидировать» (кроме валидации схемы API) · accurate → «точный» · dramatic(ally) → «резко», «значительно», не «драматически» · eventually → «в итоге», «в конечном счёте» · transparent → «незаметный для пользователя», редко «прозрачный» · native → «встроенный», «собственный» (но cloud native — не переводим) · performance → «производительность», не «перформанс» · capacity → «ёмкость», «мощность» · executive → «руководитель» · application → «приложение», но в бизнес-контексте может быть «заявка» · commit (git) → не переводим; commitment → «обязательство» · legacy → «унаследованный», «устаревший» · instance → «экземпляр» · provision → «выделить», «подготовить» · resolution → «разрешение» (DNS) или «решение проблемы» — различать · scale → «масштабировать», не «шкалировать» · security → «безопасность», не «секьюрити». + +## 9. Чеклист ревьюера: типовые сбои машинного перевода на русский + +- Заголовок не в инфинитиве или скопирован Title Case. +- «Вы» с прописной; смена регистра обращения внутри страницы. +- ASCII-кавычки или английские “…” в прозе; дефис вместо тире; «...» вместо «…»; пропущенная ё; двойные пробелы; отсутствие неразрывных пробелов в «16 ГиБ», «и т. д.». +- Десятичная точка в прозе — или, наоборот, запятая, вкравшаяся в версию/код/значение YAML; английский разделитель тысяч (10,000). +- Один и тот же термин переведён по-разному в разных абзацах; термин из глоссария проигнорирован. +- Переведено то, что переводить нельзя: имя kind, флаг, поле YAML, часть URL, текст внутри плейсхолдера §§…§§. +- Склонение или транслитерация латинских имён собственных («Кубернетесе», «Козистэк»). +- Рассогласование числительного и существительного, рода при аббревиатуре. +- Английская синтаксическая рамка: пассив, «вы можете», «используя», цепочки из трёх и более родительных падежей. +- Штампы из раздела 7 («является», «в данной статье», «позволяет вам», «просто»). +- Потерянное отрицание, потерянные условия («only if», «unless»), перепутанные «may/must/should». +- Незакрытая логика списка: пункты с разной пунктуацией и разным регистром первой буквы. +- Нарушена структура Markdown: съеден уровень заголовка, потеряна колонка таблицы, переведён URL внутри ссылки. diff --git a/hack/i18n/style-guides/zh-cn.md b/hack/i18n/style-guides/zh-cn.md index 78654b3a..fd7d71e1 100644 --- a/hack/i18n/style-guides/zh-cn.md +++ b/hack/i18n/style-guides/zh-cn.md @@ -2,3 +2,94 @@ - No spaces between Chinese characters; put a space between Chinese and Latin/number runs. - Use full-width Chinese punctuation (,。:;"")for Chinese text; keep half-width for code/commands. - Keep product/brand/CLI terms in Latin; translate prose. Common infra terms per k8s zh glossary (集群=cluster, 节点=node, 存储=storage, 工作负载=workload). + +### 1. Register and address + +- Docs: 书面语 — precise, impersonal, imperative for steps. Blog/landing: livelier, shorter sentences, rhetorical questions allowed; never 网络流行语 or exclamation spam. +- Address the reader as **你**, never 您. This is the kubernetes.io/zh-cn convention: 您 reads deferential/sales-y and is impossible to keep consistent across a large doc set. Better still, drop the pronoun. EN "You can install it with Helm" → ✗ 您可以使用 Helm 来安装它 → ✓ 使用 Helm 安装即可。 +- Mixing 你 and 您 on one page is a defect even if each sentence reads fine. +- First person plural only in signed blog posts. Docs: ✗ 我们建议启用备份 → ✓ 建议启用备份。 + +### 2. Headings + +- No trailing 。 in any heading. ?and !are allowed when the source is a question/exclamation: "What is Cozystack?" → ✓ 什么是 Cozystack? +- Concept/reference headings → noun phrase: "Storage Architecture" → ✓ 存储架构(✗ 存储是如何架构的)。 +- Task headings → 动宾 verb phrase, no 正在 / 如何进行 padding: "Installing Cozystack" → ✗ 正在安装 Cozystack、✗ 如何进行 Cozystack 的安装 → ✓ 安装 Cozystack。 +- Conventional renderings: "Getting Started" → 快速开始;"Prerequisites" → 前置条件;"Troubleshooting" → 故障排查;"See also" → 参见;"Next steps" → 后续步骤。 +- Do not add numbering the source lacks; keep heading levels unchanged. + +### 3. Terminology policy (terms NOT in the injected lists) + +Decision order: (1) kubernetes.io/zh-cn glossary → (2) CNCF Cloud Native Glossary 中文版 → (3) rules below. Never coin a new Chinese term for a concept the Chinese k8s community discusses in English. +- **API kinds, CRD kinds, field names, flags, CLIs stay Latin and uninflected**: Pod、Deployment、StatefulSet、DaemonSet、Secret、ConfigMap、Ingress、kubelet、kubectl。✗ 豆荚 / 入口 / 秘密 / 配置映射。 +- **The same word as an ordinary noun is translated** per k8s zh: node→节点、namespace→命名空间、volume→卷、label→标签、annotation→注解、container→容器、image→镜像、replica→副本、control plane→控制平面、service→服务。If the sentence means `kind: Node` / `kind: Service`, keep Node / Service in Latin. +- Generic concepts with stable renderings are translated: 高可用、可观测性、编排、调度、快照、备份、故障转移、隔离、租户。 +- Unknown/new term with no stable rendering: keep Latin, or use 中文(English)on **first mention per page**, Chinese only afterwards — ✓ 单节点控制平面(single-node control plane)。Never repeat the bracketed English on every occurrence. +- One page = one rendering per term. ✗ 租户 … 承租方 … 租客 in the same file. + +### 4. Punctuation + +- Full-width only in Chinese prose: ,。:;!?()【】、 — ASCII `,` `.` `;` `:` in Chinese prose is always a defect. +- **顿号 、 separates coordinate items inside a sentence** (English has no equivalent — do not copy the source commas): "CPU, memory, and storage" → ✗ CPU,内存,和存储 → ✓ CPU、内存和存储。 +- Quotes: “ ” (inner ‘ ’) for quoted prose and UI strings; 《》 for titles of docs/books/specs: ✓ 详见《Cozystack 安装指南》。✗ "Cozystack 安装指南"。 +- Ellipsis is ……(two full-width chars), dash is ——, never `...` or ` - `. Ranges: ✓ 3~5 个节点 / 3 到 5 个节点。 +- Half-width is correct and required inside code, inline code, versions (v1.5.0), paths, URLs, decimals (99.9), flags (`--dry-run`), and inside a fully English quoted string. +- No doubled punctuation: ✗ 部署完成。)→ ✓ 部署完成)。 Bullets and table cells: no 。 on fragments; 。 on full sentences, consistently within one list. +- Parentheses inside a Chinese sentence are full-width () even when the content is Latin: ✓ 控制平面(control plane)。 + +### 5. Spacing (pangu) + +- One space between a Chinese run and an adjacent Latin/digit run: ✓ 使用 Helm 部署 3 个副本。 ✗ 使用Helm部署3个副本。 +- **No** space between Chinese/Latin and full-width punctuation: ✗ 安装 Cozystack ,然后… → ✓ 安装 Cozystack,然后…;✗ 存储 、 网络 → ✓ 存储、网络。 +- Keep the space around masked inline-code spans too: ✓ 运行 `kubectl get pods` 查看状态。 +- Never insert pangu spaces inside code, paths, URLs, or masked placeholders. +- Never use full-width Latin letters or digits (✗ Kubernetes、✗ 10); no double spaces; no space before a line-final 。 + +### 6. Numbers, dates, units + +- Arabic numerals for all quantities, versions, dates, ports, sizes. Chinese numerals only in fixed expressions (一致、一次性、第三方). Procedures: ✓ 第 1 步 / 步骤 1。 +- 万/亿 are fine in blog prose for round numbers(✓ 10 万个 Pod); in docs keep the source's exact figures and separators (1,000 QPS stays 1,000 QPS). Never rescale magnitudes silently. +- English "billion" = 10 亿, "trillion" = 万亿 — a classic MT slip; verify every large number against the source. +- Dates: ✓ 2026年7月24日(no spaces around 年月日); ✗ 7月24日,2026。Times: ✓ 14:30。 +- Units: space between number and Latin unit — ✓ 16 GiB、100 ms、10 Gbit/s;no space for % or Chinese measure words — ✓ 99.9%、3 个节点。 + +### 7. Grammar traps translating from English + +- **量词 are mandatory and must be right**: "three nodes" → ✗ 3 节点 → ✓ 3 个节点。一台虚拟机、一块 GPU、一份清单、一条规则、一组副本、一次请求、一个 Pod、一套集群。 +- **Attributive chains before 的**: at most one 的 per noun phrase; move the rest into a following clause. "a highly available, self-healing Kubernetes control plane deployed on bare-metal nodes" → ✗ 一个部署在裸金属节点上的高可用的能够自我修复的 Kubernetes 控制平面 → ✓ 一个高可用的 Kubernetes 控制平面,部署在裸金属节点上,并可自我修复。 +- **Articles and plurals go to zero**: ✗ Pod们被调度 → ✓ Pod 会被调度;✗ 这些的集群 → ✓ 这些集群。 +- **被 is overused by MT.** Named agent → 由: ✗ 卷被 kubelet 挂载 → ✓ 卷由 kubelet 挂载。No agent → topic-comment: ✗ 备份被存储在 S3 中 → ✓ 备份存储在 S3 中。Reserve 被 for adverse/unexpected events. +- **Split long sentences**: one English sentence with 2+ subordinate clauses → 2–3 Chinese clauses ended by 。, ideally ≤ 40 汉字 each. Do not chain them with ;. +- **Topic first (主题-述题)**: "You can configure the number of replicas in values.yaml." → ✓ 副本数量在 `values.yaml` 中配置。 +- **Condition before result**: "Restart the Pod if the probe fails." → ✓ 如果探针失败,则重启 Pod。 +- 的/地/得: 快速的部署(attributive)、快速地部署(adverbial)、部署得很快(complement)— MT confuses these constantly. + +### 8. 翻译腔 patterns to eliminate + +- 进行/做出/实现 + nominalization: ✗ 对集群进行备份操作 → ✓ 备份集群;✗ 做出配置更改 → ✓ 修改配置。 +- 一个 for every English "a": ✗ 这是一个常见的模式 → ✓ 这是常见模式。 +- 们 on generic/inanimate nouns: ✗ 用户们、开发者们、节点们 → ✓ 用户、开发者、节点。 +- 关于/对于 padding: ✗ 关于更多关于 X 的信息,请参见 Y → ✓ 更多信息参见 Y;✗ 对 X 的支持 → ✓ 支持 X。 +- "simply / just / easily": ✗ 简单地运行以下命令 → ✓ 运行以下命令(或 只需运行以下命令)。 +- "you can" on every sentence: ✗ 你可以使用 Helm 来安装它 → ✓ 使用 Helm 安装。 +- 来 as filler infinitive: ✗ 使用 kubectl 来查看日志 → ✓ 使用 kubectl 查看日志。 +- "allows/enables you to": ✗ 允许你创建租户 → ✓ 支持创建租户。 +- "It is worth noting that…": ✗ 值得注意的是,… → ✓ 注意:… +- 请 only where the source is a genuine pointer/request(✓ 请参见), not on every imperative. + +### 9. Reviewer checklist — Chinese-specific MT failure modes + +1. 繁体字或台港用词泄漏: ✗ 記憶體/軟體/網路/程式/預設/伺服器/映像檔/叢集 → ✓ 内存/软件/网络/程序/默认/服务器/镜像/集群;✗ 结点 → ✓ 节点。 +2. ASCII punctuation in Chinese prose; missing 顿号; `...` instead of ……; half-width () wrapping Chinese. +3. Missing or spurious pangu spaces; full-width Latin/digits; space before 。,、. +4. Over-translated identifiers (Pod/Ingress/Secret/kubectl rendered in Chinese) or under-translated prose (cluster/node left in English). +5. Term drift within a page; first-mention 中文(English)repeated on every occurrence. +6. 被-heavy passives; 的-chains with more than one 的; missing 量词; 的/地/得 confusion. +7. English word order in Chinese characters — read the sentence aloud; if it needs a second pass to parse, split it. +8. 你/您 mixing; register drift (marketing tone inside reference docs, or stiff 书面语 inside a blog post). +9. Number/magnitude errors, altered versions, flags, resource names, or YAML keys. +10. Untranslated leftovers, duplicated clauses, and **dropped negation**(未/不/无 lost reverses the meaning); lost admonition labels — note→说明、warning→警告、caution→注意。 + +### 10. Structural (pipeline-enforced) + +Code blocks, inline code, Hugo shortcodes(`{{< >}}` / `{{% %}}`), URLs, file paths, HTML comments and front-matter **keys** are masked as `§§NAME_N§§` placeholders: copy them through verbatim, never translate, reorder, or space-fix their contents. Translate link **text**, never the URL; keep heading levels, list markers, and table column counts identical to the source. From c2ba45e880c05e5ef0ef18ebc84b88fc9d1f8549 Mon Sep 17 00:00:00 2001 From: tym83 <6355522@gmail.com> Date: Fri, 24 Jul 2026 10:31:51 +0500 Subject: [PATCH 3/4] fix(i18n): resolve the number-formatting contradiction in the translate prompt Hard rule 5 said to keep numbers unchanged, while several style guides require a decimal comma, a different thousands separator or a different date order in prose. The model was reading two incompatible instructions. The rule now separates the two ideas it was conflating: a number's VALUE and any version or identifier are literal and must be reproduced exactly, while formatting in ordinary prose follows the language's style guide. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: tym83 <6355522@gmail.com> --- hack/i18n/prompts/translate.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/hack/i18n/prompts/translate.md b/hack/i18n/prompts/translate.md index 7fffc9ea..d7e80a0f 100644 --- a/hack/i18n/prompts/translate.md +++ b/hack/i18n/prompts/translate.md @@ -24,7 +24,15 @@ reads well in {{LANGUAGE}}; when it is documentation, stay precise and faithful. 4. Preserve all Markdown/HTML structure exactly: heading levels, list markers, tables (same number of columns and delimiter cells), links (translate link TEXT, keep the URL), bold/italic, and blank lines. -5. Keep numbers, versions, dates, and units unchanged. +5. Never change the VALUE of a number, version, or measurement — a figure in the + translation must mean exactly what it meant in English. Version strings, + numbers inside code, YAML values, CIDRs, resource quantities (`512Mi`, + `0.5` CPU), ports, and tags are literal identifiers: reproduce them + character-for-character, never localizing their separators. + FORMATTING of numbers, dates and units in ordinary PROSE follows the style + guide below — several languages require a decimal comma, a different + thousands separator, or a different date order. Reformatting prose is + expected; changing a value, or touching a version/identifier, is not. ## Preferred terminology (use consistently) From 41255d05a993b59d53b96e386383f076583d423f Mon Sep 17 00:00:00 2001 From: tym83 <6355522@gmail.com> Date: Fri, 24 Jul 2026 11:16:49 +0500 Subject: [PATCH 4/4] fix(i18n): mask raw HTML and never regenerate hand-localized pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two gaps found in review by @lexfrei. protect() masked fences, shortcodes, comments and inline code, but not raw HTML tags — and .html pages are mostly markup (Goldmark also runs with unsafe: true, so Markdown pages carry raw HTML). content/en/docs/v1.5/ roadmap.html produced zero protected spans: its links, target="_blank" and <br /> all reached the model verbatim. Worse, the placeholder guard in _split_payload_response counts §§…§§ tokens, so a page with no tokens had nothing to fail closed on — a translated class= or a dropped </div> would ship silently, since the gate does not inspect markup, check-i18n.sh only compares digests, and Hugo renders broken HTML without complaint. Tags are now masked; the text between them stays exposed for translation. Autolinks are masked first so <https://…> is not swallowed by the tag pattern. translate_page also stamped l10n: mt unconditionally, after merge_target_only_keys — clobbering the very marker the README names as the human triage signal. Eight pages carry l10n: transcreate, including the four localized _index.html homepages, which are in translate_globs; one edit to the English homepage would have replaced a hand-written transcreation with machine output, and the banner is wired into the docs layout only, so the homepage would have carried it with no disclosure. Such pages are now kept out of the worklist, l10n is never downgraded, and pages whose English source has drifted are reported in the weekly PR so a human can refresh them deliberately. Co-Authored-By: Claude <noreply@anthropic.com> Signed-off-by: tym83 <6355522@gmail.com> --- hack/i18n/lib.py | 63 ++++++++++++++++++++++++++++ hack/i18n/test_i18n.py | 95 ++++++++++++++++++++++++++++++++++++++++++ hack/i18n/translate.py | 24 ++++++++++- 3 files changed, 180 insertions(+), 2 deletions(-) diff --git a/hack/i18n/lib.py b/hack/i18n/lib.py index b5e5d12e..d65e3b93 100644 --- a/hack/i18n/lib.py +++ b/hack/i18n/lib.py @@ -57,6 +57,17 @@ # refdef: [id]: https://example.com _LINKDEST_RE = re.compile(r'(?<=\])\((?P<dest><[^>]*>|[^)\s]*)(?P<title>\s+"[^"]*")?\)') _AUTOLINK_RE = re.compile(r'<(?:https?|mailto):[^>\s]+>') + +# Raw HTML tags. `.html` pages are mostly markup, and Goldmark runs with +# `unsafe: true`, so Markdown pages carry raw HTML too. Nothing masked it before: +# `docs/*/roadmap.html` produced ZERO protected spans, so every `<a href>`, +# `target="_blank"` and `<br />` reached the model verbatim — and because the +# placeholder guard in translate.py counts `§§…§§` tokens, a page with no tokens +# had nothing to fail closed on. A translated `class=` or a dropped `</div>` +# would ship silently: the gate does not inspect markup, check-i18n.sh only +# compares digests, and Hugo renders broken HTML without complaint. +# Only the TAG is masked; the text between tags stays exposed for translation. +_HTMLTAG_RE = re.compile(r'</?[a-zA-Z][^>\n]*/?>') _REFDEF_RE = re.compile(r'(?m)^(?P<label>\[[^\]]+\]:[ \t]*)(?P<dest>\S+)') @@ -220,6 +231,35 @@ def recorded_digest(path: str) -> str | None: return m.group(1) if m else None +# `l10n` values that mark a page as HAND-localized. The pipeline must never +# regenerate such a page: `translate_page` sets `l10n: mt` unconditionally, so a +# single edit to the English source would turn a hand-crafted transcreation into +# literal machine translation — and the disclaimer banner is wired into the docs +# layout only, so a localized homepage would carry that output with no disclosure +# at all. Today this protects the four localized `_index.html` homepages and four +# blog posts from the proof of concept. +PRESERVED_L10N_VALUES = ("transcreate",) + +_L10N_RE = re.compile(r'^l10n:\s*"?([A-Za-z-]+)"?', re.MULTILINE) + + +def recorded_l10n(path: str) -> str | None: + """Read the `l10n` marker from a translated page's front matter, if present.""" + if not os.path.exists(path): + return None + with open(path, encoding="utf-8") as fh: + head = fh.read() + fm_end = head.find("\n---", 4) + if fm_end != -1: + head = head[:fm_end] + m = _L10N_RE.search(head) + return m.group(1) if m else None + + +def is_hand_localized(path: str) -> bool: + return recorded_l10n(path) in PRESERVED_L10N_VALUES + + def build_worklist(cfg: dict, only_lang: str | None = None) -> list[WorkItem]: items: list[WorkItem] = [] langs = [l["code"] for l in cfg["languages"] if only_lang in (None, l["code"])] @@ -229,11 +269,31 @@ def build_worklist(cfg: dict, only_lang: str | None = None) -> list[WorkItem]: tp = target_path(cfg, lang, rel) if not os.path.exists(tp): items.append(WorkItem(lang, rel, "missing")) + elif is_hand_localized(tp): + continue # hand-crafted: never regenerate (see PRESERVED_L10N_VALUES) elif recorded_digest(tp) != cur: items.append(WorkItem(lang, rel, "stale")) return items +def find_hand_localized(cfg: dict, only_lang: str | None = None) -> list[tuple[str, str]]: + """(lang, rel) for pages skipped because a human localized them by hand. + + Skipping them silently would be its own failure: the page then drifts from an + English source nobody is tracking. They are reported in the weekly PR so a + human can refresh the transcreation deliberately. + """ + out: list[tuple[str, str]] = [] + langs = [l["code"] for l in cfg["languages"] if only_lang in (None, l["code"])] + for rel in iter_source_files(cfg): + cur = sha256_file(source_path(cfg, rel)) + for lang in langs: + tp = target_path(cfg, lang, rel) + if os.path.exists(tp) and is_hand_localized(tp) and recorded_digest(tp) != cur: + out.append((lang, rel)) + return sorted(out) + + def find_orphan_translations(cfg: dict, only_lang: str | None = None) -> list[str]: """Translated pages whose English source no longer exists. @@ -401,7 +461,10 @@ def _shortcode(m: "re.Match") -> str: # 4. Link destinations LAST, so URLs already inside code/shortcodes are # covered by their own placeholders and are not double-masked. Link TEXT # stays exposed and still gets translated; only the target is opaque. + # Autolinks go before raw HTML tags: `<https://…>` starts with a letter and + # would otherwise be swallowed by the tag pattern. text = _AUTOLINK_RE.sub(_sub("URL"), text) + text = _HTMLTAG_RE.sub(_sub("HTMLTAG"), text) text = _LINKDEST_RE.sub( lambda m: "(" + _stash(m.group("dest"), "URL") + (m.group("title") or "") + ")", text) text = _REFDEF_RE.sub( diff --git a/hack/i18n/test_i18n.py b/hack/i18n/test_i18n.py index 7cac827a..83e05fb6 100644 --- a/hack/i18n/test_i18n.py +++ b/hack/i18n/test_i18n.py @@ -625,6 +625,101 @@ def test_link_title_is_left_translatable(self): self.assertEqual(lib.restore(masked, store), 'A [link](/a/b "Read this") here.') +class TestRawHtmlMasking(unittest.TestCase): + """`.html` pages produced zero protected spans, so the placeholder guard had + nothing to fail closed on and markup reached the model verbatim.""" + + def test_html_tags_are_masked_but_their_text_is_not(self): + masked, store = lib.protect('<h2 class="section-label">Our roadmap</h2>') + self.assertNotIn("section-label", masked) + self.assertIn("Our roadmap", masked) # visible text stays translatable + self.assertEqual(lib.restore(masked, store), + '<h2 class="section-label">Our roadmap</h2>') + + def test_anchor_attributes_are_masked(self): + text = '<a href="/docs/install" target="_blank">Install guide</a>' + masked, store = lib.protect(text) + self.assertNotIn("/docs/install", masked) + self.assertNotIn("_blank", masked) + self.assertIn("Install guide", masked) + self.assertEqual(lib.restore(masked, store), text) + + def test_self_closing_and_closing_tags(self): + text = "line one<br />line two</div>" + masked, store = lib.protect(text) + self.assertNotIn("<br />", masked) + self.assertNotIn("</div>", masked) + self.assertEqual(lib.restore(masked, store), text) + + def test_html_page_now_produces_protected_spans(self): + # The regression lexfrei reported: this used to be 0. + _masked, store = lib.protect('<div class="hero">\n<p>Hello</p>\n</div>') + self.assertGreater(len(store), 0) + + def test_autolink_is_not_swallowed_by_the_tag_pattern(self): + text = "See <https://cozystack.io> for more." + masked, store = lib.protect(text) + self.assertEqual(lib.restore(masked, store), text) + + def test_a_less_than_sign_in_prose_is_not_a_tag(self): + text = "Use a value < 10 for this." + masked, store = lib.protect(text) + self.assertEqual(masked, text) + self.assertEqual(store, {}) + + +class TestHandLocalizedPagesArePreserved(unittest.TestCase): + """`l10n: transcreate` marks a page a human wrote by hand. Regenerating it + would overwrite that with machine output, and `translate_page` used to stamp + `l10n: mt` unconditionally.""" + + def _page(self, tmp, l10n, digest="deadbeef"): + p = os.path.join(tmp, "page.md") + with open(p, "w", encoding="utf-8") as fh: + fh.write(f'---\ntitle: T\nl10n: {l10n}\nsource_digest: "sha256:{digest}"\n---\nbody\n') + return p + + def test_recorded_l10n_reads_the_marker(self): + with tempfile.TemporaryDirectory() as tmp: + self.assertEqual(lib.recorded_l10n(self._page(tmp, "transcreate")), "transcreate") + + def test_is_hand_localized(self): + with tempfile.TemporaryDirectory() as tmp: + self.assertTrue(lib.is_hand_localized(self._page(tmp, "transcreate"))) + with tempfile.TemporaryDirectory() as tmp: + self.assertFalse(lib.is_hand_localized(self._page(tmp, "mt"))) + + def test_missing_marker_is_none(self): + with tempfile.TemporaryDirectory() as tmp: + p = os.path.join(tmp, "p.md") + with open(p, "w", encoding="utf-8") as fh: + fh.write("---\ntitle: T\n---\nbody\n") + self.assertIsNone(lib.recorded_l10n(p)) + self.assertFalse(lib.is_hand_localized(p)) + + def test_transcreated_page_is_kept_out_of_the_worklist(self): + with tempfile.TemporaryDirectory() as tmp: + en = os.path.join(tmp, "content", "en") + ru = os.path.join(tmp, "content", "ru") + os.makedirs(en); os.makedirs(ru) + with open(os.path.join(en, "_index.html"), "w", encoding="utf-8") as fh: + fh.write("---\ntitle: Home\n---\n<div>hi</div>\n") + # Target exists, is hand-localized, and its digest is deliberately stale. + with open(os.path.join(ru, "_index.html"), "w", encoding="utf-8") as fh: + fh.write('---\ntitle: Главная\nl10n: transcreate\nsource_digest: "sha256:stale"\n---\n<div>привет</div>\n') + cfg = {"content_dir": "content", "source_lang": "en", + "languages": [{"code": "ru"}], "translate_globs": ["_index.html"], + "exclude_globs": [], "blog_since": ""} + orig_root, orig_latest = lib.REPO_ROOT, lib.latest_docs_version + try: + lib.REPO_ROOT = tmp + lib.latest_docs_version = lambda _cfg: "v1.5" + self.assertEqual(lib.build_worklist(cfg), []) + self.assertEqual(lib.find_hand_localized(cfg), [("ru", "_index.html")]) + finally: + lib.REPO_ROOT, lib.latest_docs_version = orig_root, orig_latest + + class TestIntegrityFindings(unittest.TestCase): def test_clean_translation_has_no_findings(self): self.assertEqual( diff --git a/hack/i18n/translate.py b/hack/i18n/translate.py index b3669aca..a01991f0 100644 --- a/hack/i18n/translate.py +++ b/hack/i18n/translate.py @@ -355,7 +355,12 @@ def translate_page(cfg, glossary, lang_cfg, rel) -> tuple[str, bool, list[dict]] # (mt | transcreate). Whatever the page was before, this pipeline just # machine-translated it, so say so — the disclaimer banner and any future # native-review triage read this. - out_fm["l10n"] = "mt" + # Belt and braces: build_worklist already skips hand-localized pages, but this + # assignment used to run unconditionally AFTER merge_target_only_keys, so it + # was exactly what clobbered the marker a human set. Never downgrade a + # transcreation to `mt` — if we somehow got here, keep what the human wrote. + existing_l10n = lib.recorded_l10n(dest) + out_fm["l10n"] = existing_l10n if existing_l10n in lib.PRESERVED_L10N_VALUES else "mt" # Stamp honestly: a page that ran out of revise rounds with findings still # open is NOT the same as one that cleared the gate. Neither is "ratified" — # only a human sets that, and only that value drops the disclaimer banner. @@ -569,7 +574,22 @@ def main() -> int: # `-with-findings` stamp or a stalled page unactionable. status_md = _format_run_status(stopped_early, rate_limit_reason, clean + with_findings, skipped, PROTOCOL_ATTEMPTS) - sections = [s for s in (status_md, _format_report(report) if report else "") if s] + # Hand-localized pages whose English source moved on. The pipeline refuses to + # regenerate them (it would overwrite a transcreation with machine output), so + # the only way they get refreshed is a human deciding to — which requires + # telling the human they have drifted. + hand = lib.find_hand_localized(cfg, only_lang=args.lang) + hand_md = "" + if hand: + hand_md = "\n".join( + [f"### Hand-localized pages that drifted ({len(hand)})", "", + "These carry `l10n: transcreate`, so the pipeline leaves them alone — " + "regenerating would replace a human transcreation with machine output. " + "Their English source has changed since they were written; refresh them " + "by hand if the change matters.", ""] + + [f"- `{lang}: {rel}`" for lang, rel in hand] + [""]) + sections = [s for s in (status_md, hand_md, + _format_report(report) if report else "") if s] if sections: with open(REPORT_PATH, "w", encoding="utf-8") as fh: fh.write("\n".join(sections))