1+ #!/usr/bin/env python3
2+ # /// script
3+ # requires-python = ">=3.13"
4+ # dependencies = [
5+ # "polib",
6+ # "rich",
7+ # ]
8+ # ///
9+
10+ """
11+ Checks correctness of translation PO files.
12+ """
13+
14+ import argparse
15+ import polib
16+ import subprocess
17+ import sys
18+ import glob
19+ from rich import print
20+
21+
22+
23+ def _matching_po_entry (po , lineno ):
24+ """
25+ Find the PO entry that matches the given line number.
26+ Note that entries are multiline and lineno may be in the middle of an entry.
27+ """
28+ last = None
29+ for entry in po :
30+ if entry .linenum > lineno :
31+ break
32+ last = entry
33+ return last
34+
35+
36+ def _print_error (fn , entry , lineno , text , github ):
37+ """
38+ Print the error message for the given entry.
39+ """
40+ if github :
41+ if entry :
42+ details = text + '\n ' + str (entry )
43+ details = details .replace ('\n ' , '%0A' )
44+ print (f'::error title="{ fn } :{ lineno } :{ text } "::{ details } ' )
45+ else :
46+ print (f'::error title="error in { fn } "::{ lineno } :{ text } ' )
47+ else :
48+ print (f'[red]{ fn } :{ lineno } :{ text } [/red]' )
49+ if entry :
50+ print (entry )
51+
52+
53+ def process_po (filename , stderr , github ):
54+ """
55+ Parse and pretty-print errors in the file.
56+ The error format is: <file>:<line>: <error>
57+ """
58+ po = polib .pofile (filename )
59+ # dict indexed with POEntry and containing all error messages for it
60+ errors = []
61+
62+ for line in stderr .split ('\n ' ):
63+ if not line :
64+ continue
65+ parts = line .split (':' )
66+ fn = parts [0 ]
67+ if fn != filename :
68+ continue
69+ lineno = int (parts [1 ])
70+ text = ':' .join (parts [2 :])
71+ entry = _matching_po_entry (po , lineno )
72+ _print_error (filename , entry , lineno , text , github )
73+ if entry :
74+ errors .append (entry )
75+
76+
77+ def check_translations (po_files , github = False ):
78+ status = True
79+ for po_file in po_files :
80+ result = subprocess .run (['msgfmt' , '-v' , '-c' , '-o' , '/dev/null' , po_file ], capture_output = True , text = True )
81+ if result .returncode != 0 :
82+ status = False
83+ process_po (po_file , result .stderr , github )
84+ return status
85+
86+
87+
88+ def main ():
89+ parser = argparse .ArgumentParser (description = 'Check correctness of translation PO files.' )
90+ parser .add_argument ('--github' , action = 'store_true' , help = 'Format output for GitHub Actions' )
91+ args = parser .parse_args ()
92+
93+ po_files = glob .glob ('locales/*.po' )
94+ status = check_translations (po_files , args .github )
95+ sys .exit (0 if status else 1 )
96+
97+
98+ if __name__ == "__main__" :
99+ main ()
0 commit comments