| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -8,6 +8,10 @@ | |||
| 8 | 8 | be translated, matching Sphinx's own translation convention. | |
| 9 | 9 | - Literal/code spans (``...``), substitution refs (|...|), and %s/{name} | |
| 10 | 10 | placeholders — these must match verbatim, since they're not prose. | |
| 11 | + - Invalid C-string escape sequences in msgstr (a lone backslash followed | ||
| 12 | + by a character that isn't one of \\ " n t r f b a v) — these make | ||
| 13 | + `msgfmt` fail with "invalid control sequence" and must be found before | ||
| 14 | + they break a build. | ||
| 11 | 15 | ||
| 12 | 16 | Output is grouped by file, with a per-file mismatch count and a grand | |
| 13 | 17 | total at the end. | |
@@ -44,6 +48,68 @@ | |||
| 44 | 48 | ("brace placeholder", re.compile(r"\{[^{}\s]*\}")), | |
| 45 | 49 | ] | |
| 46 | 50 | ||
| 51 | + # Valid C-string escapes that gettext/msgfmt accept inside a quoted | ||
| 52 | + # string. A backslash followed by anything else is what msgfmt rejects | ||
| 53 | + # with "invalid control sequence". This mirrors the set the old | ||
| 54 | + # autofix bash script treated as "leave alone": \\ \" \n \t \r \f \b \a \v | ||
| 55 | + VALID_ESCAPE_CHARS = set('\\"ntrfbav') | ||
| 56 | + | ||
| 57 | + # One raw quoted-string line as it appears in the .po file. Matches | ||
| 58 | + # both the first line of an entry, which carries a keyword prefix | ||
| 59 | + # (msgid "..." / msgstr "..." / msgstr[0] "..."), and bare continuation | ||
| 60 | + # lines ("...") that follow it. Captured group is the RAW content | ||
| 61 | + # between the quotes - escapes are NOT decoded, which is required for | ||
| 62 | + # this check (see note below). | ||
| 63 | + RAW_STRING_LINE = re.compile( | ||
| 64 | + r'^(?:msgid|msgstr(?:\[\d+\])?|msgctxt)?\s*"((?:[^"\\]|\\.)*)"\s*$' | ||
| 65 | + ) | ||
| 66 | + | ||
| 67 | + | ||
| 68 | + def find_invalid_escapes_in_raw(raw: str): | ||
| 69 | + """Scan raw (undecoded) quoted-string content left-to-right the way | ||
| 70 | + a C-string tokenizer would, consuming two characters whenever a | ||
| 71 | + backslash is seen, and return the invalid `\\X` sequences found. | ||
| 72 | + | ||
| 73 | + MUST run on raw file text, not on polib's .msgid/.msgstr - polib | ||
| 74 | + decodes \\\\ into a single \\ before Claude ever sees it, so scanning | ||
| 75 | + decoded text turns safe, doubled backslashes (\\\\d in the file, | ||
| 76 | + meaning a literal backslash-d) into false positives that look like | ||
| 77 | + a lone backslash followed by an invalid character.""" | ||
| 78 | + invalid = [] | ||
| 79 | + i = 0 | ||
| 80 | + n = len(raw) | ||
| 81 | + while i < n: | ||
| 82 | + if raw[i] == "\\": | ||
| 83 | + if i + 1 < n: | ||
| 84 | + nxt = raw[i + 1] | ||
| 85 | + if nxt not in VALID_ESCAPE_CHARS: | ||
| 86 | + invalid.append("\\" + nxt) | ||
| 87 | + i += 2 | ||
| 88 | + continue | ||
| 89 | + else: | ||
| 90 | + invalid.append("\\<end-of-line>") | ||
| 91 | + i += 1 | ||
| 92 | + continue | ||
| 93 | + i += 1 | ||
| 94 | + return invalid | ||
| 95 | + | ||
| 96 | + | ||
| 97 | + def check_raw_escapes(path: Path): | ||
| 98 | + """Line-by-line scan of the raw file for invalid escape sequences, | ||
| 99 | + independent of polib. Returns a list of (line_no, line_text, | ||
| 100 | + invalid_list) for every quoted-string line with a problem.""" | ||
| 101 | + findings = [] | ||
| 102 | + for lineno, line in enumerate( | ||
| 103 | + path.read_text(encoding="utf-8").splitlines(), start=1 | ||
| 104 | + ): | ||
| 105 | + m = RAW_STRING_LINE.match(line.strip()) | ||
| 106 | + if not m: | ||
| 107 | + continue | ||
| 108 | + invalid = find_invalid_escapes_in_raw(m.group(1)) | ||
| 109 | + if invalid: | ||
| 110 | + findings.append((lineno, line.strip(), invalid)) | ||
| 111 | + return findings | ||
| 112 | + | ||
| 47 | 113 | ||
| 48 | 114 | def extract_role_targets(text: str): | |
| 49 | 115 | """For each Sphinx role, return its target: the <target> anchor if | |
@@ -60,6 +126,7 @@ def check_file(path: Path): | |||
| 60 | 126 | """Return a list of finding-dicts for this file (empty if none).""" | |
| 61 | 127 | results = [] | |
| 62 | 128 | po = polib.pofile(str(path)) | |
| 129 | + | ||
| 63 | 130 | for entry in po: | |
| 64 | 131 | if entry.obsolete or not entry.msgid or not entry.msgstr: | |
| 65 | 132 | continue # obsolete entry, header, or still untranslated | |
@@ -134,6 +201,8 @@ def main(): | |||
| 134 | 201 | ||
| 135 | 202 | total = 0 | |
| 136 | 203 | per_file_counts = [] | |
| 204 | + escape_total = 0 | ||
| 205 | + escape_files = 0 | ||
| 137 | 206 | ||
| 138 | 207 | for f in files: | |
| 139 | 208 | results = check_file(f) | |
@@ -142,17 +211,38 @@ def main(): | |||
| 142 | 211 | total += len(results) | |
| 143 | 212 | per_file_counts.append((f, len(results))) | |
| 144 | 213 | ||
| 214 | + escape_findings = check_raw_escapes(f) | ||
| 215 | + if escape_findings: | ||
| 216 | + print(f"\n{'=' * 70}") | ||
| 217 | + print(f"{f} ({len(escape_findings)} invalid escape sequence line(s))") | ||
| 218 | + print("=" * 70) | ||
| 219 | + for lineno, line, invalid in escape_findings: | ||
| 220 | + print(f" line {lineno}: invalid {invalid}") | ||
| 221 | + print(f" {line[:120]}") | ||
| 222 | + escape_total += len(escape_findings) | ||
| 223 | + escape_files += 1 | ||
| 224 | + | ||
| 145 | 225 | if total: | |
| 146 | 226 | print(f"\n{'=' * 70}") | |
| 147 | 227 | print("Summary by file (sorted by mismatch count, descending):") | |
| 148 | 228 | print("=" * 70) | |
| 149 | 229 | for f, count in sorted(per_file_counts, key=lambda x: -x[1]): | |
| 150 | 230 | print(f" {count:4d} {f}") | |
| 151 | 231 | print(f"\n{total} markup mismatch(es) found across {len(per_file_counts)} file(s).") | |
| 232 | + | ||
| 233 | + if escape_total: | ||
| 234 | + print( | ||
| 235 | + f"\n{escape_total} invalid escape sequence line(s) found across " | ||
| 236 | + f"{escape_files} file(s). These will make msgfmt fail with " | ||
| 237 | + f"'invalid control sequence' - fix the offending backslash " | ||
| 238 | + f"in each line above." | ||
| 239 | + ) | ||
| 240 | + | ||
| 241 | + if total or escape_total: | ||
| 152 | 242 | sys.exit(1) | |
| 153 | 243 | ||
| 154 | 244 | print("No markup mismatches found.") | |
| 155 | 245 | ||
| 156 | 246 | ||
| 157 | 247 | if __name__ == "__main__": | |
| 158 | - main() | ||
| 248 | + main() | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -2908,7 +2908,7 @@ msgstr "" | |||
| 2908 | 2908 | "پروندههای ورودی دادهشده تولید میکند.\n" | |
| 2909 | 2909 | "\n" | |
| 2910 | 2910 | "قالب دایرکتیو autosummary در ماژول پایتونی\n" | |
| 2911 | - "«sphinx.ext.autosummary» مستند شده و میتوان با استفاده از موارد زیر آن را خواند::\n" | ||
| 2911 | + "``sphinx.ext.autosummary`` مستند شده و میتوان با استفاده از موارد زیر آن را خواند::\n" | ||
| 2912 | 2912 | "\n" | |
| 2913 | 2913 | " pydoc sphinx.ext.autosummary\n" | |
| 2914 | 2914 | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments