|
2 | 2 | """ |
3 | 3 | scripts/check_markup.py |
4 | 4 |
|
5 | | -Verify that Sphinx roles, inline literals, and format placeholders match |
6 | | -exactly between the English msgid and the Persian msgstr — nothing missing, |
7 | | -and nothing extra. Catches both the common review slip (translating or |
8 | | -dropping `:class:`int``-style markup, ``code`` spans, %s/{0} placeholders, |
9 | | -|substitution| refs) and the subtler mistake of adding a reference that |
10 | | -doesn't exist in the original (which Sphinx's own build treats as a hard |
11 | | -error under -W, e.g. "inconsistent term references in translated message"). |
| 5 | +Verify Sphinx markup consistency between msgid and msgstr: |
| 6 | + - Roles like :term:`text <target>` — the *target* (or the whole role, if |
| 7 | + it has no explicit target) must match; the display text is expected to |
| 8 | + be translated, matching Sphinx's own translation convention. |
| 9 | + - Literal/code spans (``...``), substitution refs (|...|), and %s/{name} |
| 10 | + placeholders — these must match verbatim, since they're not prose. |
12 | 11 |
|
13 | 12 | Requires: pip install polib |
14 | 13 |
|
|
24 | 23 |
|
25 | 24 | import polib |
26 | 25 |
|
27 | | -PATTERNS = [ |
28 | | - ("sphinx role", re.compile(r":(?:\w+:)?[\w.-]+:`.*?`")), |
| 26 | +ROLE_PATTERN = re.compile(r":(?:\w+:)?[\w.-]+:`([^`]+)`") |
| 27 | +TARGET_PATTERN = re.compile(r"^(.*)\s<([^<>]+)>$") |
| 28 | + |
| 29 | +LITERAL_PATTERNS = [ |
29 | 30 | ("literal/code span", re.compile(r"``.*?``")), |
30 | 31 | ("substitution ref", re.compile(r"\|[\w.-]+\|")), |
31 | 32 | ("percent placeholder", re.compile(r"%\(\w+\)[a-zA-Z]|%[a-zA-Z]")), |
32 | 33 | ("brace placeholder", re.compile(r"\{[^{}\s]*\}")), |
33 | 34 | ] |
34 | 35 |
|
35 | 36 |
|
| 37 | +def extract_role_targets(text: str): |
| 38 | + """For each Sphinx role, return its target: the <target> anchor if |
| 39 | + present, otherwise the role's full display text (which IS the target |
| 40 | + when there's no explicit anchor).""" |
| 41 | + targets = [] |
| 42 | + for body in ROLE_PATTERN.findall(text): |
| 43 | + m = TARGET_PATTERN.match(body) |
| 44 | + targets.append(m.group(2).strip() if m else body.strip()) |
| 45 | + return targets |
| 46 | + |
| 47 | + |
36 | 48 | def check_file(path: Path) -> int: |
37 | 49 | problems = 0 |
38 | 50 | po = polib.pofile(str(path)) |
39 | 51 | for entry in po: |
40 | 52 | if entry.obsolete or not entry.msgid or not entry.msgstr: |
41 | 53 | continue # obsolete entry, header, or still untranslated |
42 | | - for label, pattern in PATTERNS: |
| 54 | + |
| 55 | + findings = [] |
| 56 | + |
| 57 | + expected_targets = Counter(extract_role_targets(entry.msgid)) |
| 58 | + found_targets = Counter(extract_role_targets(entry.msgstr)) |
| 59 | + if expected_targets != found_targets: |
| 60 | + missing = list((expected_targets - found_targets).elements()) |
| 61 | + extra = list((found_targets - expected_targets).elements()) |
| 62 | + if missing: |
| 63 | + findings.append(f"missing role target(s): {missing}") |
| 64 | + if extra: |
| 65 | + findings.append(f"role target(s) not in source: {extra}") |
| 66 | + |
| 67 | + for label, pattern in LITERAL_PATTERNS: |
43 | 68 | expected = Counter(pattern.findall(entry.msgid)) |
44 | 69 | found = Counter(pattern.findall(entry.msgstr)) |
45 | | - if expected == found: |
46 | | - continue |
| 70 | + if expected != found: |
| 71 | + missing = list((expected - found).elements()) |
| 72 | + extra = list((found - expected).elements()) |
| 73 | + if missing: |
| 74 | + findings.append(f"missing {label}: {missing}") |
| 75 | + if extra: |
| 76 | + findings.append(f"extra {label} not in source: {extra}") |
47 | 77 |
|
48 | | - missing = list((expected - found).elements()) |
49 | | - extra = list((found - expected).elements()) |
| 78 | + if findings: |
50 | 79 | problems += 1 |
51 | 80 | loc = f" ({entry.occurrences[0][0]}:{entry.occurrences[0][1]})" if entry.occurrences else "" |
52 | 81 | tag = " [fuzzy]" if entry.fuzzy else "" |
53 | | - parts = [] |
54 | | - if missing: |
55 | | - parts.append(f"missing {label}: {missing}") |
56 | | - if extra: |
57 | | - parts.append(f"extra {label} not in source: {extra}") |
58 | | - print(f"{path}{loc}{tag}: {'; '.join(parts)}") |
| 82 | + print(f"{path}{loc}{tag}: {'; '.join(findings)}") |
59 | 83 | print(f" msgid : {entry.msgid[:100]}") |
60 | 84 | print(f" msgstr: {entry.msgstr[:100]}") |
61 | 85 | return problems |
|
0 commit comments