Skip to content

Commit 72bcc17

Browse files
committed
feat: Add translation manager for extracting and updating PO files using Excel
1 parent 77d5ab5 commit 72bcc17

1 file changed

Lines changed: 205 additions & 0 deletions

File tree

scripts/translation_manager.py

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import os
2+
import re
3+
import polib
4+
import itertools
5+
import argparse
6+
from typing import Dict, Tuple
7+
from openpyxl import Workbook, load_workbook
8+
9+
_patterns = [
10+
":c:func:`[^`]+`",
11+
":c:type:`[^`]+`",
12+
":c:macro:`[^`]+`",
13+
":c:member:`[^`]+`",
14+
":c:data:`[^`]+`",
15+
":py:data:`[^`]+`",
16+
":py:mod:`[^`]+`",
17+
":func:`[^`]+`",
18+
":mod:`[^`]+`",
19+
":ref:`[^`]+`",
20+
":class:`[^`]+`",
21+
":pep:`[^`]+`",
22+
":data:`[^`]+`",
23+
":exc:`[^`]+`",
24+
":term:`[^`]+`",
25+
":meth:`[^`]+`",
26+
":envvar:`[^`]+`",
27+
":file:`[^`]+`",
28+
":attr:`[^`]+`",
29+
":const:`[^`]+`",
30+
":issue:`[^`]+`",
31+
":opcode:`[^`]+`",
32+
":option:`[^`]+`",
33+
":program:`[^`]+`",
34+
":keyword:`[^`]+`",
35+
":RFC:`[^`]+`",
36+
":rfc:`[^`]+`",
37+
":doc:`[^`]+`",
38+
":source:`[^`]+`",
39+
":manpage:`[^`]+`",
40+
":mimetype:`[^`]+`",
41+
":sup:`[^`]+`",
42+
":kbd:`[^`]+`",
43+
":const:`[^`]+`",
44+
"``[^`]+``",
45+
"`[^`]+`__",
46+
"`[^`]+`_",
47+
r"\*\*[^\*]+\*\*", # bold text between **
48+
r"\*[^\*]+\*", # italic text between *
49+
]
50+
_exps = [re.compile(e) for e in _patterns]
51+
52+
53+
class Normalizer:
54+
@staticmethod
55+
def protect_sphinx_directives(s: str) -> Tuple[Dict[str, str], str]:
56+
"""
57+
Replace Sphinx directives in the input string with placeholders.
58+
59+
Parameters:
60+
s: The original string containing Sphinx directives.
61+
Returns:
62+
A tuple (placeholders, new_s) where 'placeholders' maps placeholders
63+
to the original directives and 'new_s' is the string with placeholders.
64+
"""
65+
placeholders: Dict[str, str] = {}
66+
counter = itertools.count()
67+
68+
def repl(match):
69+
ph = f"XASDF{next(counter):02}"
70+
placeholders[ph] = match.group(0)
71+
return ph
72+
73+
combined_pattern = "|".join(f"({p})" for p in _patterns)
74+
combined_regex = re.compile(combined_pattern)
75+
new_s = combined_regex.sub(repl, s)
76+
return placeholders, new_s
77+
78+
@staticmethod
79+
def undo_sphinx_directives_protection(
80+
placeholders: Dict[str, str], translated_text: str
81+
) -> str:
82+
"""
83+
Restore the original Sphinx directives in the translated text.
84+
"""
85+
for ph, value in placeholders.items():
86+
translated_text = translated_text.replace(ph, value)
87+
return translated_text
88+
89+
90+
class POExtractor:
91+
"""Extracts unique strings from .po files in given directories and exports them to an Excel file."""
92+
93+
def __init__(self, directories):
94+
self.directories = directories
95+
self.strings = {}
96+
97+
def extract_strings_from_po(self, po_path: str) -> None:
98+
po = polib.pofile(po_path)
99+
for entry in po:
100+
if entry.msgid:
101+
self.strings[entry.msgid] = None
102+
103+
def process_directories(self) -> None:
104+
for base_dir in self.directories:
105+
for root, _, files in os.walk(base_dir):
106+
for file in files:
107+
if file.endswith(".po"):
108+
po_path = os.path.join(root, file)
109+
self.extract_strings_from_po(po_path)
110+
111+
def generate_excel(self, output_excel: str) -> None:
112+
wb = Workbook()
113+
ws = wb.active
114+
ws.title = "Translations"
115+
ws.append(["Original", "Placeholders", "Temp Text", "Translation"])
116+
for original in self.strings:
117+
placeholders, temp_text = Normalizer.protect_sphinx_directives(original)
118+
ws.append([original, str(placeholders), temp_text, ""])
119+
wb.save(output_excel)
120+
print(f"Excel file saved to {output_excel}.")
121+
122+
123+
class POUpdater:
124+
"""Updates .po files with translations from an Excel file."""
125+
126+
def __init__(self, directories, excel_file: str):
127+
self.directories = directories
128+
self.excel_file = excel_file
129+
self.translations = {}
130+
131+
def load_translations(self) -> None:
132+
wb = load_workbook(self.excel_file)
133+
ws = wb.active
134+
for row in ws.iter_rows(min_row=2, values_only=True):
135+
if row[0]:
136+
original = row[0]
137+
placeholders = eval(row[1])
138+
translated = row[3]
139+
translated = Normalizer.undo_sphinx_directives_protection(
140+
placeholders, translated
141+
)
142+
self.translations[original] = translated
143+
144+
def update_po_file(self, po_path: str) -> None:
145+
po = polib.pofile(po_path)
146+
updated = False
147+
for entry in po:
148+
if entry.msgid in self.translations:
149+
new_translation = self.translations[entry.msgid]
150+
if new_translation != entry.msgstr:
151+
entry.msgstr = new_translation
152+
if "fuzzy" not in entry.flags:
153+
entry.flags.append("fuzzy")
154+
updated = True
155+
if updated:
156+
po.save()
157+
print(f"Updated translations in {po_path}")
158+
159+
def update_directories(self) -> None:
160+
self.load_translations()
161+
if not self.translations:
162+
print("No translations found in the Excel file.")
163+
return
164+
165+
for base_dir in self.directories:
166+
for root, _, files in os.walk(base_dir):
167+
for file in files:
168+
if file.endswith(".po"):
169+
self.update_po_file(os.path.join(root, file))
170+
171+
172+
def main():
173+
parser = argparse.ArgumentParser(
174+
description="Extract or update PO translations using an Excel file"
175+
)
176+
parser.add_argument(
177+
"mode",
178+
choices=["extract", "update"],
179+
help="Choose to extract from or update PO files",
180+
)
181+
parser.add_argument(
182+
"--dir",
183+
nargs="+",
184+
default=["./need_to_translate"],
185+
help="Directories containing PO files",
186+
)
187+
parser.add_argument(
188+
"--excel",
189+
type=str,
190+
default="translations.xlsx",
191+
help="Excel file to read from or write to",
192+
)
193+
args = parser.parse_args()
194+
195+
if args.mode == "extract":
196+
extractor = POExtractor(args.dir)
197+
extractor.process_directories()
198+
extractor.generate_excel(args.excel)
199+
elif args.mode == "update":
200+
updater = POUpdater(args.dir, args.excel)
201+
updater.update_directories()
202+
203+
204+
if __name__ == "__main__":
205+
main()

0 commit comments

Comments
 (0)