diff --git a/hack/i18n/lib.py b/hack/i18n/lib.py index 9d3427fe..d65e3b93 100644 --- a/hack/i18n/lib.py +++ b/hack/i18n/lib.py @@ -47,6 +47,29 @@ # 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]+>') + +# 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+)') + def load_config() -> dict: with open(CONFIG_PATH, encoding="utf-8") as fh: @@ -208,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"])] @@ -217,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. @@ -385,6 +457,18 @@ 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. + # 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( + lambda m: m.group("label") + _stash(m.group("dest"), "URL"), text) return text, store @@ -430,6 +514,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/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) 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. diff --git a/hack/i18n/test_i18n.py b/hack/i18n/test_i18n.py index 056620ad..83e05fb6 100644 --- a/hack/i18n/test_i18n.py +++ b/hack/i18n/test_i18n.py @@ -600,5 +600,184 @@ 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 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( + 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..a01991f0 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"]))), @@ -347,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. @@ -561,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))