[ Web Proxy ]
URL:
Viewing: https://raw.githubusercontent.com/python/python-docs-fa/3.15/scripts/review_report.py [Back]  [Original]

#!/usr/bin/env python3
"""
scripts/review_report.py

Builds the markdown body of the "translation review needed" issue from the
changes currently STAGED in git (run it after `git add --all` and
`unstage_cosmetic.py`). Writes an empty file if there is nothing to report.

Sections:
  - Summary
  - New PO files            (counts only; every string in them is untranslated)
  - Newly fuzzy strings     (old msgid -> new msgid, plus the current msgstr)
  - New untranslated strings (new msgids that matched nothing, not fuzzy)

"New" always means "not present in HEAD", so re-running never re-reports
strings from earlier syncs.

Requires: polib (pip install polib)
"""
from __future__ import annotations

import argparse
import subprocess
from pathlib import Path

import polib

# GitHub rejects issue bodies over 65,536 characters.
MAX_BODY = 60_000
FUZZY_PER_FILE = 10
UNTRANSLATED_PER_FILE = 25
MAX_LINE = 160


def git(*args: str) -> str:
    return subprocess.run(
        ["git", *args], capture_output=True, text=True, check=True
    ).stdout


def staged(diff_filter: str) -> list:
    out = git(
        "diff",
        "--staged",
        "--no-renames",
        "-z",
        "--name-only",
        f"--diff-filter={diff_filter}",
        "--",
        "*.po",
    )
    return [p for p in out.split("\0") if p]


def key(entry: polib.POEntry) -> tuple:
    return (entry.msgctxt, entry.msgid)


def live(po: polib.POFile) -> list:
    return [e for e in po if not e.obsolete]


def is_fuzzy(entry: polib.POEntry) -> bool:
    return "fuzzy" in entry.flags


def is_untranslated(entry: polib.POEntry) -> bool:
    if entry.msgid_plural:
        return not any(entry.msgstr_plural.values())
    return not entry.msgstr


def clip(text: str, limit: int = MAX_LINE) -> str:
    # Keep everything on one line and make sure we can't close a code fence.
    text = text.replace("\n", "\\n").replace("```", "'''")
    return text if len(text)  str:
    lines = [f"### `{path}` ({len(entries)} new fuzzy)", "", "```diff"]
    for e in entries[:FUZZY_PER_FILE]:
        if e.previous_msgid:
            lines.append("- " + clip(e.previous_msgid))
        else:
            lines.append("  (previous msgid unavailable)")
        lines.append("+ " + clip(e.msgid))
        if e.msgstr:
            lines.append("  msgstr: " + clip(e.msgstr))
        lines.append("")
    if len(entries) > FUZZY_PER_FILE:
        lines.append(f"   and {len(entries) - FUZZY_PER_FILE} more")
    lines.append("```")
    return "\n".join(lines) + "\n"


def untranslated_block(path: str, entries: list) -> str:
    lines = [f"### `{path}` ({len(entries)} new untranslated)", "", "```text"]
    lines += [clip(e.msgid) for e in entries[:UNTRANSLATED_PER_FILE]]
    if len(entries) > UNTRANSLATED_PER_FILE:
        lines.append(f" and {len(entries) - UNTRANSLATED_PER_FILE} more")
    lines.append("```")
    return "\n".join(lines) + "\n"


class Body:
    """Accumulates blocks until the size budget runs out."""

    def __init__(self, limit: int):
        self.parts: list = []
        self.size = 0
        self.limit = limit
        self.omitted = 0

    def add(self, text: str) -> None:
        if self.size + len(text) > self.limit:
            self.omitted += 1
            return
        self.parts.append(text)
        self.size += len(text) + 1


def render(new_files: list, changes: list) -> str:
    fuzzy_total = sum(len(f) for _, f, _ in changes)
    untr_total = sum(len(u) for _, _, u in changes)
    if not (new_files or fuzzy_total or untr_total):
        return ""

    body = Body(MAX_BODY)

    summary = ["## Summary", ""]
    if new_files:
        strings = sum(n for _, n in new_files)
        summary.append(
            f"-  **{len(new_files)}** new PO files ({strings} strings to translate)"
        )
    if fuzzy_total:
        n = sum(1 for _, f, _ in changes if f)
        summary.append(f"-  **{fuzzy_total}** newly fuzzy strings in {n} files")
    if untr_total:
        n = sum(1 for _, _, u in changes if u)
        summary.append(f"-  **{untr_total}** new untranslated strings in {n} files")
    body.add("\n".join(summary) + "\n")

    if new_files:
        lines = [f"##  New PO files ({len(new_files)})"]
        lines += [f"- `{p}` ({n} strings)" for p, n in new_files]
        body.add("\n".join(lines) + "\n")

    if fuzzy_total:
        body.add(
            "##  Newly fuzzy strings\n\n"
            "The source text changed slightly, so the existing translation "
            "(`msgstr`) may need updating. `-` is the old source, `+` the new.\n"
        )
        for path, fuzzy, _ in changes:
            if fuzzy:
                body.add(fuzzy_block(path, fuzzy))

    if untr_total:
        body.add(
            "##  New untranslated strings\n\n"
            "Brand-new source strings with no close match to an existing "
            "translation.\n"
        )
        for path, _, untr in changes:
            if untr:
                body.add(untranslated_block(path, untr))

    if body.omitted:
        body.parts.append(
            f"_{body.omitted} section(s) omitted to fit GitHub's issue size "
            f"limit  see the sync commit's diff for the full list._\n"
        )
    return "\n".join(body.parts)


def main() -> None:
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--output", required=True, type=Path)
    args = parser.parse_args()

    new_files, changes = collect()
    report = render(new_files, changes)
    args.output.write_text(report, encoding="utf-8")
    print(
        f"Report: {len(report)} chars, {len(new_files)} new files, "
        f"{len(changes)} changed files with new strings"
    )


if __name__ == "__main__":
    main()

Web Proxy Viewer  |  New URL  |  Original Page