Merge branch 'stable' into devel
[tails.git] / bin / recover-lost-translations.py
blob3769a23107f3da48634062f19288748073439c2d
1 #!/usr/bin/env python3
3 """
4 Recover translations that were lost between two commits.
6 This script is meant to be used in situations where translations were
7 mistakenly invalidated, for example because of unintended changes in source
8 strings.
9 """
11 import git
12 import polib
14 import argparse
15 import logging
16 import subprocess
17 import sys
18 import pathlib
21 def should_be_recovered(e_old, e_new):
22 """
23 Decide whether a translation should be recovered or not.
25 Return True if the translation is valid in the old entry and invalid (and
26 not obsolete) in the new entry.
27 """
28 if not e_old.translated():
29 return False
31 if e_new.translated() or e_new.obsolete:
32 return False
34 return True
37 def copy(e_old, e_new):
38 """
39 Copies the content from an old entry to a new entry.
40 """
41 e_new.msgid = e_old.msgid
42 e_new.msgstr = e_old.msgstr
43 e_new.occurrences = e_old.occurrences
44 e_new.comment = e_old.comment
45 e_new.flags = e_old.flags[:] # clone flags
46 e_new.msgid_plural = e_old.msgid_plural
47 e_new.obsolete = e_old.obsolete
48 if e_old.msgstr_plural:
49 for pos in e_old.msgstr_plural:
50 try:
51 # keep existing translation at pos if any
52 e_new.msgstr_plural[pos]
53 except KeyError:
54 e_new.msgstr_plural[pos] = ""
57 def parse_args():
58 """
59 Parse command line arguments.
60 """
61 parser = argparse.ArgumentParser()
62 parser.add_argument(
63 "--width", "-w", type=int, default=80, help="Width to wrap lines"
65 parser.add_argument("old_ref", help="commit before the problematic commit")
66 parser.add_argument(
67 "new_ref",
68 default="HEAD",
69 help="commit onto you recover. normally you want to use HEAD here.",
70 nargs="?",
72 return parser.parse_args()
75 def recover_lost_translations(args, logger):
76 """
77 Recover translations that were valid in an old commit and are invalid in a
78 new commit.
79 """
80 repo = git.Repo("")
81 old = repo.commit(args.old_ref)
82 diff = old.diff(args.new_ref)
84 git_base = pathlib.Path(repo.git_dir).parent
86 for f in diff:
87 if not f.b_path.endswith(".po"):
88 continue
89 if f.change_type not in ("R", "M"):
90 continue
92 try:
93 pofile_old = polib.pofile(
94 f.a_blob.data_stream.read().decode("utf-8"),
95 encoding="utf-8",
96 wrapwidth=args.width,
98 except OSError as e:
99 logger.warning(f"{f.a_path}@{args.old_ref}: {e}")
100 continue
102 try:
103 pofile_new = polib.pofile(
104 f.b_blob.data_stream.read().decode("utf-8"),
105 encoding="utf-8",
106 wrapwidth=args.width,
108 except OSError as e:
109 logger.warning(f"{f.b_path}@{args.new_ref}: {e}")
110 continue
112 changed_file = False
114 for e_new in pofile_new:
115 e_old = pofile_old.find(e_new.msgid, by="msgid")
116 if not e_old:
117 continue
119 if should_be_recovered(e_old, e_new):
120 changed_file = True
121 copy(e_old, e_new)
123 if changed_file:
124 newpath = f.b_path
125 subprocess.run(
127 "msgcat",
128 "--width",
129 str(args.width),
130 "-t",
131 "utf-8",
132 "-o",
133 str(git_base / newpath),
134 "-",
136 input=pofile_new.__unicode__().encode("utf-8"),
137 check=True,
141 if __name__ == "__main__":
142 logging.basicConfig()
143 logger = logging.getLogger(__name__)
144 args = parse_args()
145 recover_lost_translations(args, logger)