| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -14,23 +14,30 @@ on: | |||
| 14 | 14 | jobs: | |
| 15 | 15 | lint: | |
| 16 | 16 | runs-on: ubuntu-latest | |
| 17 | - strategy: | ||
| 18 | - fail-fast: false | ||
| 19 | - continue-on-error: true | ||
| 20 | 17 | steps: | |
| 21 | 18 | - name: Checkout code | |
| 22 | 19 | uses: actions/checkout@v4 | |
| 23 | - | ||
| 20 | + | ||
| 24 | 21 | - name: Set up Python | |
| 25 | 22 | uses: actions/setup-python@v4 | |
| 26 | 23 | with: | |
| 27 | 24 | python-version: '3.11' | |
| 28 | - | ||
| 25 | + | ||
| 29 | 26 | - name: Install sphinx-lint | |
| 30 | 27 | run: pip install sphinx-lint | |
| 31 | - | ||
| 28 | + | ||
| 29 | + - name: Install gettext tools | ||
| 30 | + run: sudo apt-get install -y gettext | ||
| 31 | + | ||
| 32 | 32 | - name: Setup problem matcher | |
| 33 | 33 | uses: rffontenelle/sphinx-lint-problem-matcher@v1.0.0 | |
| 34 | - | ||
| 34 | + | ||
| 35 | 35 | - name: Run sphinx-lint | |
| 36 | 36 | run: sphinx-lint | |
| 37 | + continue-on-error: true | ||
| 38 | + | ||
| 39 | + - name: Check PO file validity | ||
| 40 | + run: find . -name '*.po' -exec msgfmt --check {} \; | ||
| 41 | + | ||
| 42 | + - name: Check markup preservation | ||
| 43 | + run: python3 scripts/check_markup.py . | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -9,7 +9,27 @@ | |||
| 9 | 9 | ||
| 10 | 10 | ## راهنمای مشارکت 🌱 | |
| 11 | 11 | ||
| 12 | - تمام ترجمهها بر روی [Transifex](https://explore.transifex.com/python-doc/python-newest/) انجام میشود. فرآیند ترجمه شامل بازبینی و اصلاح متونی است که به صورت خودکار توسط ابزارهای ترجمه ماشینی ترجمه شدهاند. | ||
| 12 | + ترجمهها دیگر روی Transifex انجام نمیشوند و مستقیماً از طریق پولریکوئست در همین ریپازیتوری مدیریت میشوند. فایلهای `.po` هر کدام به بخشی از مستندات پایتون (مثل `tutorial/`، `library/` یا `c-api/`) مربوطاند و میتوانید هرکدام را جداگانه ویرایش و پولریکوئست بزنید. | ||
| 13 | + | ||
| 14 | + خلاصه فرایند مشارکت: | ||
| 15 | + | ||
| 16 | + 1. ریپازیتوری را فورک و کلون کنید. | ||
| 17 | + 2. فایل `.po` مورد نظر را با [Poedit](https://poedit.net/) باز کنید (رشتههای ترجمهنشده یا fuzzy را میتوانید از پنل فیلتر پیدا کنید). | ||
| 18 | + 3. متن انگلیسی (`msgid`) را ترجمه کنید و در بخش ترجمه (`msgstr`) وارد کنید. | ||
| 19 | + 4. نشانهگذاریهای Sphinx مثل `` :class:`int` ``، `` :func:`repr` ``، `` ``code`` ``، و جایگزینهایی مثل `%s` یا `{name}` را دقیقاً بدون تغییر نگه دارید؛ فقط متن اطراف آنها ترجمه میشود. | ||
| 20 | + 5. پیش از ارسال پولریکوئست، بررسی کنید فایلها مشکلی ندارند: | ||
| 21 | + ```bash | ||
| 22 | + msgfmt --check your_file.po | ||
| 23 | + python3 scripts/check_markup.py your_file.po | ||
| 24 | + ``` | ||
| 25 | + 6. پولریکوئست را باز کنید — روی هر پولریکوئست بهصورت خودکار بررسیهای لازم اجرا میشود. | ||
| 26 | + | ||
| 27 | + برای دیدن اینکه کدام فایلها هنوز نیاز به ترجمه دارند و چقدر از هرکدام باقی مانده: | ||
| 28 | + ```bash | ||
| 29 | + python3 scripts/translation_status.py --only-incomplete | ||
| 30 | + ``` | ||
| 31 | + | ||
| 32 | + راهنمای کاملتر مشارکت (شامل نحوهی همگامسازی با نسخههای جدید پایتون و اسکریپتهای موجود در `scripts/`) در [RESOURCE.md](RESOURCE.md) موجود است. | ||
| 13 | 33 | ||
| 14 | 34 | ## ارتباط و هماهنگی | |
| 15 | 35 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,127 @@ | |||
| 1 | + #!/usr/bin/env python3 | ||
| 2 | + """ | ||
| 3 | + scripts/check_markup.py | ||
| 4 | + | ||
| 5 | + Verify that Sphinx roles, inline literals, and format placeholders in the | ||
| 6 | + English msgid are preserved verbatim in the Persian msgstr. Catches the most | ||
| 7 | + common review slip: translating or mangling `:class:`int``-style markup, | ||
| 8 | + ``code`` spans, %s/{0} placeholders, or |substitution| refs. | ||
| 9 | + | ||
| 10 | + Usage: | ||
| 11 | + python3 scripts/check_markup.py library/functions.po | ||
| 12 | + python3 scripts/check_markup.py tutorial/*.po | ||
| 13 | + python3 scripts/check_markup.py . # recurse a whole directory | ||
| 14 | + """ | ||
| 15 | + import re | ||
| 16 | + import sys | ||
| 17 | + from pathlib import Path | ||
| 18 | + | ||
| 19 | + PATTERNS = [ | ||
| 20 | + ("sphinx role", re.compile(r":(?:\w+:)?[\w.-]+:`.*?`")), | ||
| 21 | + ("literal/code span", re.compile(r"``.*?``")), | ||
| 22 | + ("substitution ref", re.compile(r"\|[\w.-]+\|")), | ||
| 23 | + ("percent placeholder", re.compile(r"%\(\w+\)[a-zA-Z]|%[a-zA-Z]")), | ||
| 24 | + ("brace placeholder", re.compile(r"\{[^{}\s]*\}")), | ||
| 25 | + ] | ||
| 26 | + | ||
| 27 | + | ||
| 28 | + def unescape(raw: str) -> str: | ||
| 29 | + """Undo PO string escaping (raw includes the surrounding quotes).""" | ||
| 30 | + inner = raw[1:-1] | ||
| 31 | + out = [] | ||
| 32 | + i = 0 | ||
| 33 | + while i < len(inner): | ||
| 34 | + c = inner[i] | ||
| 35 | + if c == "\\" and i + 1 < len(inner): | ||
| 36 | + nxt = inner[i + 1] | ||
| 37 | + out.append({"n": "\n", "t": "\t", '"': '"', "\\": "\\"}.get(nxt, nxt)) | ||
| 38 | + i += 2 | ||
| 39 | + else: | ||
| 40 | + out.append(c) | ||
| 41 | + i += 1 | ||
| 42 | + return "".join(out) | ||
| 43 | + | ||
| 44 | + | ||
| 45 | + def parse_po(path: Path): | ||
| 46 | + """Yield (location, flags, msgid, msgstr) for each entry in a .po file.""" | ||
| 47 | + lines = path.read_text(encoding="utf-8").splitlines() | ||
| 48 | + i, n = 0, len(lines) | ||
| 49 | + | ||
| 50 | + def read_block(keyword_line): | ||
| 51 | + nonlocal i | ||
| 52 | + parts = [unescape(keyword_line.split(" ", 1)[1].strip())] | ||
| 53 | + i += 1 | ||
| 54 | + while i < n and lines[i].strip().startswith('"'): | ||
| 55 | + parts.append(unescape(lines[i].strip())) | ||
| 56 | + i += 1 | ||
| 57 | + return "".join(parts) | ||
| 58 | + | ||
| 59 | + while i < n: | ||
| 60 | + location, flags = "", [] | ||
| 61 | + while i < n and lines[i].startswith("#"): | ||
| 62 | + if lines[i].startswith("#:"): | ||
| 63 | + location = lines[i][2:].strip() | ||
| 64 | + elif lines[i].startswith("#,"): | ||
| 65 | + flags = [f.strip() for f in lines[i][2:].split(",")] | ||
| 66 | + i += 1 | ||
| 67 | + if i >= n or not lines[i].startswith("msgid"): | ||
| 68 | + i += 1 | ||
| 69 | + continue | ||
| 70 | + | ||
| 71 | + msgid = read_block(lines[i]) | ||
| 72 | + msgid_plural = read_block(lines[i]) if i < n and lines[i].startswith("msgid_plural") else None | ||
| 73 | + | ||
| 74 | + if i < n and lines[i].startswith("msgstr["): | ||
| 75 | + msgstrs = {} | ||
| 76 | + while i < n and lines[i].startswith("msgstr["): | ||
| 77 | + idx = int(lines[i][7:lines[i].index("]")]) | ||
| 78 | + msgstrs[idx] = read_block(lines[i]) | ||
| 79 | + yield location, flags, msgid, msgstrs.get(0, "") | ||
| 80 | + if msgid_plural is not None and 1 in msgstrs: | ||
| 81 | + yield location, flags, msgid_plural, msgstrs[1] | ||
| 82 | + continue | ||
| 83 | + | ||
| 84 | + msgstr = read_block(lines[i]) if i < n and lines[i].startswith("msgstr") else "" | ||
| 85 | + yield location, flags, msgid, msgstr | ||
| 86 | + | ||
| 87 | + | ||
| 88 | + def check_file(path: Path) -> int: | ||
| 89 | + problems = 0 | ||
| 90 | + for location, flags, msgid, msgstr in parse_po(path): | ||
| 91 | + if not msgid or not msgstr: | ||
| 92 | + continue # header entry or still untranslated | ||
| 93 | + for label, pattern in PATTERNS: | ||
| 94 | + expected = pattern.findall(msgid) | ||
| 95 | + if not expected: | ||
| 96 | + continue | ||
| 97 | + missing = [tok for tok in expected if tok not in msgstr] | ||
| 98 | + if missing: | ||
| 99 | + problems += 1 | ||
| 100 | + loc = f" ({location})" if location else "" | ||
| 101 | + tag = " [fuzzy]" if "fuzzy" in flags else "" | ||
| 102 | + print(f"{path}{loc}{tag}: missing {label}: {missing}") | ||
| 103 | + print(f" msgid : {msgid[:100]}") | ||
| 104 | + print(f" msgstr: {msgstr[:100]}") | ||
| 105 | + return problems | ||
| 106 | + | ||
| 107 | + | ||
| 108 | + def main(): | ||
| 109 | + if len(sys.argv) < 2: | ||
| 110 | + print(__doc__) | ||
| 111 | + sys.exit(1) | ||
| 112 | + | ||
| 113 | + files = [] | ||
| 114 | + for arg in sys.argv[1:]: | ||
| 115 | + p = Path(arg) | ||
| 116 | + files.extend(sorted(p.rglob("*.po"))) if p.is_dir() else files.append(p) | ||
| 117 | + | ||
| 118 | + total = sum(check_file(f) for f in files) | ||
| 119 | + | ||
| 120 | + if total: | ||
| 121 | + print(f"\n{total} markup mismatch(es) found.") | ||
| 122 | + sys.exit(1) | ||
| 123 | + print("No markup mismatches found.") | ||
| 124 | + | ||
| 125 | + | ||
| 126 | + if __name__ == "__main__": | ||
| 127 | + main() | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -0,0 +1,170 @@ | |||
| 1 | + #!/usr/bin/env python3 | ||
| 2 | + """ | ||
| 3 | + scripts/translation_status.py | ||
| 4 | + | ||
| 5 | + Report translation progress across .po files: how many strings are | ||
| 6 | + translated, fuzzy, or untranslated per file, plus overall totals. Meant to | ||
| 7 | + help a contributor quickly find files that need work. | ||
| 8 | + | ||
| 9 | + Usage: | ||
| 10 | + python3 scripts/translation_status.py # whole repo, least-translated first | ||
| 11 | + python3 scripts/translation_status.py tutorial/ # just one directory | ||
| 12 | + python3 scripts/translation_status.py --only-incomplete # hide fully-done files | ||
| 13 | + python3 scripts/translation_status.py --sort name | ||
| 14 | + python3 scripts/translation_status.py --format markdown > STATUS.md | ||
| 15 | + """ | ||
| 16 | + import argparse | ||
| 17 | + import sys | ||
| 18 | + from pathlib import Path | ||
| 19 | + | ||
| 20 | + | ||
| 21 | + def unescape(raw: str) -> str: | ||
| 22 | + """Undo PO string escaping (raw includes the surrounding quotes).""" | ||
| 23 | + inner = raw[1:-1] | ||
| 24 | + out = [] | ||
| 25 | + i = 0 | ||
| 26 | + while i < len(inner): | ||
| 27 | + c = inner[i] | ||
| 28 | + if c == "\\" and i + 1 < len(inner): | ||
| 29 | + nxt = inner[i + 1] | ||
| 30 | + out.append({"n": "\n", "t": "\t", '"': '"', "\\": "\\"}.get(nxt, nxt)) | ||
| 31 | + i += 2 | ||
| 32 | + else: | ||
| 33 | + out.append(c) | ||
| 34 | + i += 1 | ||
| 35 | + return "".join(out) | ||
| 36 | + | ||
| 37 | + | ||
| 38 | + def parse_po_entries(path: Path): | ||
| 39 | + """Yield (flags, msgid, msgstrs) for each entry in a .po file.""" | ||
| 40 | + lines = path.read_text(encoding="utf-8").splitlines() | ||
| 41 | + i, n = 0, len(lines) | ||
| 42 | + | ||
| 43 | + def read_block(keyword_line): | ||
| 44 | + nonlocal i | ||
| 45 | + parts = [unescape(keyword_line.split(" ", 1)[1].strip())] | ||
| 46 | + i += 1 | ||
| 47 | + while i < n and lines[i].strip().startswith('"'): | ||
| 48 | + parts.append(unescape(lines[i].strip())) | ||
| 49 | + i += 1 | ||
| 50 | + return "".join(parts) | ||
| 51 | + | ||
| 52 | + while i < n: | ||
| 53 | + flags = [] | ||
| 54 | + while i < n and lines[i].startswith("#"): | ||
| 55 | + if lines[i].startswith("#,"): | ||
| 56 | + flags = [f.strip() for f in lines[i][2:].split(",")] | ||
| 57 | + i += 1 | ||
| 58 | + if i >= n or not lines[i].startswith("msgid"): | ||
| 59 | + i += 1 | ||
| 60 | + continue | ||
| 61 | + | ||
| 62 | + msgid = read_block(lines[i]) | ||
| 63 | + if i < n and lines[i].startswith("msgid_plural"): | ||
| 64 | + read_block(lines[i]) # plural source not needed for counting | ||
| 65 | + | ||
| 66 | + msgstrs = [] | ||
| 67 | + if i < n and lines[i].startswith("msgstr["): | ||
| 68 | + while i < n and lines[i].startswith("msgstr["): | ||
| 69 | + msgstrs.append(read_block(lines[i])) | ||
| 70 | + elif i < n and lines[i].startswith("msgstr"): | ||
| 71 | + msgstrs.append(read_block(lines[i])) | ||
| 72 | + | ||
| 73 | + yield flags, msgid, msgstrs | ||
| 74 | + | ||
| 75 | + | ||
| 76 | + def file_stats(path: Path): | ||
| 77 | + translated = fuzzy = untranslated = 0 | ||
| 78 | + for flags, msgid, msgstrs in parse_po_entries(path): | ||
| 79 | + if not msgid: | ||
| 80 | + continue # header entry | ||
| 81 | + if "fuzzy" in flags: | ||
| 82 | + fuzzy += 1 | ||
| 83 | + elif any(m.strip() for m in msgstrs): | ||
| 84 | + translated += 1 | ||
| 85 | + else: | ||
| 86 | + untranslated += 1 | ||
| 87 | + return translated, fuzzy, untranslated | ||
| 88 | + | ||
| 89 | + | ||
| 90 | + def collect_files(paths): | ||
| 91 | + files = [] | ||
| 92 | + for arg in paths: | ||
| 93 | + p = Path(arg) | ||
| 94 | + if p.is_dir(): | ||
| 95 | + files.extend(sorted(p.rglob("*.po"))) | ||
| 96 | + elif p.suffix == ".po": | ||
| 97 | + files.append(p) | ||
| 98 | + return files | ||
| 99 | + | ||
| 100 | + | ||
| 101 | + def main(): | ||
| 102 | + parser = argparse.ArgumentParser( | ||
| 103 | + description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter | ||
| 104 | + ) | ||
| 105 | + parser.add_argument("paths", nargs="*", default=["."], | ||
| 106 | + help="Files or directories to scan (default: whole repo)") | ||
| 107 | + parser.add_argument("--sort", choices=["percent", "untranslated", "name"], default="percent", | ||
| 108 | + help="Sort order (default: percent, least-translated first)") | ||
| 109 | + parser.add_argument("--only-incomplete", action="store_true", | ||
| 110 | + help="Hide files that are already fully translated") | ||
| 111 | + parser.add_argument("--format", choices=["text", "markdown", "csv"], default="text") | ||
| 112 | + args = parser.parse_args() | ||
| 113 | + | ||
| 114 | + files = collect_files(args.paths) | ||
| 115 | + if not files: | ||
| 116 | + print("No .po files found.") | ||
| 117 | + sys.exit(1) | ||
| 118 | + | ||
| 119 | + rows = [] | ||
| 120 | + for f in files: | ||
| 121 | + t, fz, u = file_stats(f) | ||
| 122 | + total = t + fz + u | ||
| 123 | + percent = (t / total * 100) if total else 100.0 | ||
| 124 | + rows.append((str(f), t, fz, u, total, percent)) | ||
| 125 | + | ||
| 126 | + if args.only_incomplete: | ||
| 127 | + rows = [r for r in rows if r[2] > 0 or r[3] > 0] | ||
| 128 | + | ||
| 129 | + if args.sort == "percent": | ||
| 130 | + rows.sort(key=lambda r: r[5]) | ||
| 131 | + elif args.sort == "untranslated": | ||
| 132 | + rows.sort(key=lambda r: -r[3]) | ||
| 133 | + else: | ||
| 134 | + rows.sort(key=lambda r: r[0]) | ||
| 135 | + | ||
| 136 | + total_t = sum(r[1] for r in rows) | ||
| 137 | + total_fz = sum(r[2] for r in rows) | ||
| 138 | + total_u = sum(r[3] for r in rows) | ||
| 139 | + total_all = total_t + total_fz + total_u | ||
| 140 | + total_percent = (total_t / total_all * 100) if total_all else 100.0 | ||
| 141 | + | ||
| 142 | + if args.format == "csv": | ||
| 143 | + print("file,translated,fuzzy,untranslated,total,percent") | ||
| 144 | + for path, t, fz, u, total, percent in rows: | ||
| 145 | + print(f"{path},{t},{fz},{u},{total},{percent:.1f}") | ||
| 146 | + print(f"TOTAL,{total_t},{total_fz},{total_u},{total_all},{total_percent:.1f}") | ||
| 147 | + return | ||
| 148 | + | ||
| 149 | + if args.format == "markdown": | ||
| 150 | + print("| File | Translated | Fuzzy | Untranslated | % done |") | ||
| 151 | + print("|---|---:|---:|---:|---:|") | ||
| 152 | + for path, t, fz, u, total, percent in rows: | ||
| 153 | + print(f"| `{path}` | {t} | {fz} | {u} | {percent:.1f}% |") | ||
| 154 | + print(f"| **TOTAL** | **{total_t}** | **{total_fz}** | **{total_u}** | **{total_percent:.1f}%** |") | ||
| 155 | + return | ||
| 156 | + | ||
| 157 | + name_width = max((len(r[0]) for r in rows), default=4) | ||
| 158 | + header = f"{'File':<{name_width}} {'Translated':>10} {'Fuzzy':>6} {'Untranslated':>12} {'% done':>7}" | ||
| 159 | + print(header) | ||
| 160 | + print("-" * len(header)) | ||
| 161 | + for path, t, fz, u, total, percent in rows: | ||
| 162 | + print(f"{path:<{name_width}} {t:>10} {fz:>6} {u:>12} {percent:>6.1f}%") | ||
| 163 | + print("-" * len(header)) | ||
| 164 | + print(f"{'TOTAL':<{name_width}} {total_t:>10} {total_fz:>6} {total_u:>12} {total_percent:>6.1f}%") | ||
| 165 | + print(f"\n{len(rows)} file(s) shown. " | ||
| 166 | + f"{sum(1 for r in rows if r[5] < 100)} file(s) not fully translated.") | ||
| 167 | + | ||
| 168 | + | ||
| 169 | + if __name__ == "__main__": | ||
| 170 | + main() | ||
| Back | FazBrowse Home | New Git URL |
0 commit comments