Skip to content

Commit 8b6c6f1

Browse files
committed
code
1 parent afbbc0d commit 8b6c6f1

10 files changed

Lines changed: 458 additions & 0 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
*.xml
2+
*.iml
3+
.idea/*
4+
*.pyc

.pre-commit-hooks.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
- id: missing-error-messages
2+
name: missing-error-messages-hook
3+
entry: missing-error-messages-hook
4+
description: pre-commit hooks for localization error messages
5+
language: python

poetry.lock

Lines changed: 154 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pre_commit_po_hooks/__init__.py

Whitespace-only changes.
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
"""Checks for missing in PO files error messages.
2+
3+
Returns an error code if a found an error message not included in PO file.
4+
"""
5+
6+
import argparse
7+
import os
8+
import sys
9+
from pathlib import Path
10+
from pre_commit_po_hooks import utils
11+
12+
13+
class Check:
14+
15+
quiet: bool
16+
po_dir: str
17+
repo_directory: str
18+
errors_patterns: list[str]
19+
20+
def __init__(self, quiet: bool, po_dir: str, repo_directory: str, errors_patterns: list[str]):
21+
self.quiet = quiet
22+
self.po_dir = po_dir
23+
self.repo_directory = repo_directory
24+
self.errors_patterns = errors_patterns
25+
26+
if not self.quiet:
27+
sys.stdout.write(
28+
f"Run with args: \n"
29+
f"`errors_patterns`: {self.errors_patterns},\n"
30+
f"`repo_directory`: {self.repo_directory},\n"
31+
f"`po_dir`: {self.po_dir}\n"
32+
)
33+
34+
def get_py_filenames(self) -> list[Path]:
35+
filenames = []
36+
try:
37+
for pattern in self.errors_patterns:
38+
filenames += [
39+
p for p in Path(self.repo_directory).rglob(pattern)
40+
if not any(x.startswith('.') or x.startswith('_') for x in p.parts)
41+
]
42+
except Exception:
43+
raise Exception("Incorrect error filename pattern.\n")
44+
45+
if not self.quiet:
46+
sys.stdout.write("Found error files: " + str(filenames) + '\n')
47+
48+
return filenames
49+
50+
def get_po_filenames(self, po_dir: str) -> list[Path]:
51+
return [p.parent / p.name for p in Path(po_dir).rglob("*.po") if os.path.isfile(p.parent / p.name)]
52+
53+
def update_en_po(self, poes_data: dict, py_objects: set[str]) -> int:
54+
(filename, catalog), *_ = [
55+
(filename, catalog) for filename, catalog in poes_data.items() if catalog.locale_identifier == 'en'
56+
]
57+
po_objects = set(message.id for message in catalog if message.id)
58+
59+
if sorted(list(py_objects)) != sorted(list(po_objects)):
60+
utils.update_po_file(
61+
errors=set((e, e) for e in py_objects), catalog=catalog, po_filepath=Path(self.po_dir) / filename
62+
)
63+
return 1
64+
65+
return 0
66+
67+
def update_non_en_po(self, poes_data: dict, py_objects: set[str]) -> int:
68+
res = 0
69+
70+
for filename, catalog in poes_data.items():
71+
if catalog.locale_identifier == 'en':
72+
continue
73+
74+
messages = {message.id: message.string for message in catalog if message.id}
75+
76+
if messages.keys() - py_objects:
77+
res = 1
78+
utils.update_po_file(
79+
catalog=catalog,
80+
po_filepath=Path(self.po_dir) / filename,
81+
errors=set(
82+
(message_id, message_text)
83+
for message_id, message_text in messages.items() if message_id in py_objects
84+
),
85+
)
86+
87+
return res
88+
89+
def _execute(self):
90+
"""Warns about all missed error messages found in filenames that not included in PO files.
91+
92+
Parameters
93+
----------
94+
errors_patterns : list[str]
95+
Pattern of errors.py filenames
96+
97+
repo_directory : str
98+
Path to repository containing errors.py files
99+
100+
po_dir : string
101+
Path to dir with .po files.
102+
103+
quiet : bool, optional
104+
Enabled, don't print output to stderr when an untranslated message is found.
105+
106+
Returns
107+
-------
108+
109+
int: 0 if no missed messages found, 1 otherwise.
110+
"""
111+
py_objects = utils.load_py(filenames=self.get_py_filenames())
112+
113+
poes_data = {f.name: utils.load_po(po_filepath=f) for f in self.get_po_filenames(self.po_dir)}
114+
115+
res1 = self.update_en_po(poes_data, py_objects=py_objects)
116+
res2 = 0 # self.update_non_en_po(poes_data, py_objects=py_objects)
117+
118+
if res1 and not self.quiet:
119+
raise Exception("File .po was updated.\n")
120+
121+
if res2 and not self.quiet:
122+
raise Exception("Non default .po files were updated.\n")
123+
124+
return min(sum([res1, res2]), 1)
125+
126+
def execute(self):
127+
try:
128+
return self._execute()
129+
except Exception as e:
130+
sys.stderr.write(str(e) or f"Hook error happened: {repr(e)}")
131+
return 1
132+
133+
134+
def main():
135+
parser = argparse.ArgumentParser()
136+
137+
parser.add_argument("filenames", nargs="*", help="Changed files")
138+
parser.add_argument("-q", "--quiet", required=False, default=False, help="Supress output")
139+
parser.add_argument("--repo_directory", required=True, help="Path to repository containing errors.py files")
140+
parser.add_argument("--po_dir", required=True, help="Path to dir with .po files")
141+
parser.add_argument(
142+
"--errors_patterns", required=False, nargs='*', default=["errors.py"], help="Pattern of errors.py filenames"
143+
)
144+
145+
args = parser.parse_args()
146+
147+
if not args.filenames:
148+
return 0
149+
150+
return Check(
151+
quiet=args.quiet,
152+
po_dir=args.po_dir,
153+
repo_directory=args.repo_directory,
154+
errors_patterns=args.errors_patterns,
155+
).execute()
156+
157+
158+
if __name__ == "__main__":
159+
exit(main())

pre_commit_po_hooks/utils.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import ast
2+
import os
3+
from pathlib import Path
4+
5+
from babel.messages import Catalog
6+
from babel.messages.pofile import read_po, write_po
7+
8+
9+
def load_po(po_filepath: Path) -> Catalog:
10+
if not os.path.isfile(po_filepath):
11+
raise Exception("File .po was not found.\n")
12+
13+
with open(po_filepath) as f:
14+
catalog = read_po(f)
15+
16+
return catalog
17+
18+
19+
def update_po_file(errors: set[(str, str)], catalog: Catalog, po_filepath: Path) -> None:
20+
new_catalog = Catalog(
21+
fuzzy=catalog.fuzzy,
22+
locale=catalog.locale,
23+
domain=catalog.domain,
24+
charset=catalog.charset,
25+
project=catalog.project,
26+
version=catalog.version,
27+
creation_date=catalog.creation_date,
28+
revision_date=catalog.revision_date,
29+
language_team=catalog.language_team,
30+
header_comment=catalog.header_comment,
31+
last_translator=catalog.last_translator,
32+
copyright_holder=catalog.copyright_holder,
33+
msgid_bugs_address=catalog.msgid_bugs_address,
34+
)
35+
for msgid, msgstr in errors:
36+
new_catalog.add(msgid, msgstr)
37+
38+
with open(po_filepath, 'wb') as outfile:
39+
write_po(outfile, catalog=new_catalog, width=100, sort_output=True)
40+
41+
42+
def load_py(filenames: list[Path]) -> set[str]:
43+
errors = []
44+
45+
for filename in filenames:
46+
if not os.path.isfile(filename):
47+
raise Exception(f"File {filename} was not found.\n")
48+
49+
with open(filename) as f:
50+
content = f.read()
51+
52+
content = ast.parse(content)
53+
54+
for node in content.body:
55+
if isinstance(node, ast.ClassDef):
56+
errors.extend(enum_field.value.value for enum_field in node.body)
57+
58+
if len(errors) != len(set(errors)):
59+
raise Exception(f"Error messages are not unique.\n")
60+
61+
return set(errors)

0 commit comments

Comments
 (0)