#!/usr/bin/env python3
"""
scripts/translation_status.py
Report translation progress across .po files: how many strings are
translated, fuzzy, or untranslated per file, plus overall totals. Meant to
help a contributor quickly find files that need work.
Requires: pip install polib
Usage:
python3 scripts/translation_status.py # whole repo, least-translated first
python3 scripts/translation_status.py tutorial/ # just one directory
python3 scripts/translation_status.py --only-incomplete # hide fully-done files
python3 scripts/translation_status.py --sort name
python3 scripts/translation_status.py --sort fuzzy
python3 scripts/translation_status.py --format markdown > STATUS.md
"""
import argparse
import sys
from pathlib import Path
import polib
def file_stats(path: Path):
po = polib.pofile(str(path))
return (
len(po.translated_entries()),
len(po.fuzzy_entries()),
len(po.untranslated_entries()),
)
def collect_files(paths, exclude=None):
exclude = {Path(e).resolve() for e in (exclude or [])}
files = []
def is_excluded(path):
resolved = path.resolve()
return resolved in exclude or any(
parent in exclude for parent in resolved.parents
)
for arg in paths:
p = Path(arg)
if p.is_dir():
if is_excluded(p):
continue
for f in p.rglob("*.po"):
if any(part.startswith(".") for part in f.parts):
continue
if is_excluded(f):
continue
files.append(f)
elif p.suffix == ".po":
if not is_excluded(p):
files.append(p)
return files
def main():
parser = argparse.ArgumentParser(
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter
)
parser.add_argument(
"paths",
nargs="*",
default=["."],
help="Files or directories to scan (default: whole repo)",
)
parser.add_argument(
"--sort",
choices=["percent", "untranslated", "fuzzy", "name"],
default="percent",
help="Sort order (default: percent, least-translated first)",
)
parser.add_argument(
"--only-incomplete",
action="store_true",
help="Hide files that are already fully translated",
)
parser.add_argument(
"--exclude",
nargs="*",
help="Exclude files or directories from scan",
)
parser.add_argument("--format", choices=["text", "markdown", "csv"], default="text")
args = parser.parse_args()
files = collect_files(args.paths, args.exclude)
if not files:
print("No .po files found.")
sys.exit(1)
rows = []
for f in files:
t, fz, u = file_stats(f)
total = t + fz + u
percent = (t / total * 100) if total else 100.0
rows.append((str(f), t, fz, u, total, percent))
if args.only_incomplete:
rows = [r for r in rows if r[2] > 0 or r[3] > 0]
if args.sort == "percent":
rows.sort(key=lambda r: r[5])
elif args.sort == "untranslated":
rows.sort(key=lambda r: -r[3])
elif args.sort == "fuzzy":
rows.sort(key=lambda r: -r[2])
else:
rows.sort(key=lambda r: r[0])
total_t = sum(r[1] for r in rows)
total_fz = sum(r[2] for r in rows)
total_u = sum(r[3] for r in rows)
total_all = total_t + total_fz + total_u
total_percent = (total_t / total_all * 100) if total_all else 100.0
if args.format == "csv":
print("file,translated,fuzzy,untranslated,total,percent")
for path, t, fz, u, total, percent in rows:
print(f"{path},{t},{fz},{u},{total},{percent:.1f}")
print(f"TOTAL,{total_t},{total_fz},{total_u},{total_all},{total_percent:.1f}")
return
if args.format == "markdown":
print("| File | Translated | Fuzzy | Untranslated | % done |")
print("|---|---:|---:|---:|---:|")
for path, t, fz, u, total, percent in rows:
print(f"| `{path}` | {t} | {fz} | {u} | {percent:.1f}% |")
print(
f"| **TOTAL** | **{total_t}** | **{total_fz}** | **{total_u}** | **{total_percent:.1f}%** |"
)
return
name_width = max((len(r[0]) for r in rows), default=4)
header = f"{'File':10} {'Fuzzy':>6} {'Untranslated':>12} {'% done':>7}"
print(header)
print("-" * len(header))
for path, t, fz, u, total, percent in rows:
print(f"{path:10} {fz:>6} {u:>12} {percent:>6.1f}%")
print("-" * len(header))
print(
f"{'TOTAL':10} {total_fz:>6} {total_u:>12} {total_percent:>6.1f}%"
)
print(
f"\n{len(rows)} file(s) shown. "
f"{sum(1 for r in rows if r[5] < 100)} file(s) not fully translated."
)
if __name__ == "__main__":
main()