fix (actually nasty workaround), for groups incorrectly drawing in the object panel...
[blender.git] / release / scripts / modules / i18n / update_pot.py
1 #!/usr/bin/python3
2
3 # ***** BEGIN GPL LICENSE BLOCK *****
4 #
5 # This program is free software; you can redistribute it and/or
6 # modify it under the terms of the GNU General Public License
7 # as published by the Free Software Foundation; either version 2
8 # of the License, or (at your option) any later version.
9 #
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13 # GNU General Public License for more details.
14 #
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software Foundation,
17 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
18 #
19 # ***** END GPL LICENSE BLOCK *****
20
21 # <pep8 compliant>
22
23 # Update blender.pot file from messages.txt
24
25 import subprocess
26 import os
27 import sys
28 import re
29 #from codecs import open
30 import tempfile
31 import argparse
32 import time
33 import pickle
34
35 import settings
36 import utils
37
38
39 COMMENT_PREFIX = settings.COMMENT_PREFIX
40 COMMENT_PREFIX_SOURCE = settings.COMMENT_PREFIX_SOURCE
41 CONTEXT_PREFIX = settings.CONTEXT_PREFIX
42 FILE_NAME_MESSAGES = settings.FILE_NAME_MESSAGES
43 #FILE_NAME_POTFILES = settings.FILE_NAME_POTFILES
44 FILE_NAME_POT = settings.FILE_NAME_POT
45 SOURCE_DIR = settings.SOURCE_DIR
46 POTFILES_DIR = settings.POTFILES_SOURCE_DIR
47 SRC_POTFILES = settings.FILE_NAME_SRC_POTFILES
48
49 #GETTEXT_XGETTEXT_EXECUTABLE = settings.GETTEXT_XGETTEXT_EXECUTABLE
50 #GETTEXT_KEYWORDS = settings.GETTEXT_KEYWORDS
51 CONTEXT_DEFAULT = settings.CONTEXT_DEFAULT
52 PYGETTEXT_ALLOWED_EXTS = settings.PYGETTEXT_ALLOWED_EXTS
53
54 SVN_EXECUTABLE = settings.SVN_EXECUTABLE
55
56 WARN_NC = settings.WARN_MSGID_NOT_CAPITALIZED
57 NC_ALLOWED = settings.WARN_MSGID_NOT_CAPITALIZED_ALLOWED
58
59 SPELL_CACHE = settings.SPELL_CACHE
60
61
62 #def generate_valid_potfiles(final_potfiles):
63 #    "Generates a temp potfiles.in with aboslute paths."
64 #    with open(FILE_NAME_POTFILES, 'r', 'utf-8') as f, \
65 #         open(final_potfiles, 'w', 'utf-8') as w:
66 #        for line in f:
67 #            line = utils.stripeol(line)
68 #            if line:
69 #                w.write("".join((os.path.join(SOURCE_DIR,
70 #                                              os.path.normpath(line)), "\n")))
71
72 # Do this only once!
73 # Get contexts defined in blf.
74 CONTEXTS = {}
75 with open(os.path.join(SOURCE_DIR, settings.PYGETTEXT_CONTEXTS_DEFSRC)) as f:
76     reg = re.compile(settings.PYGETTEXT_CONTEXTS)
77     f = f.read()
78     # This regex is supposed to yield tuples
79     # (key=C_macro_name, value=C_string).
80     CONTEXTS = dict(m.groups() for m in reg.finditer(f))
81
82 # Build regexes to extract messages (with optinal contexts) from C source.
83 pygettexts = tuple(re.compile(r).search
84                    for r in settings.PYGETTEXT_KEYWORDS)
85 _clean_str = re.compile(settings.str_clean_re).finditer
86 clean_str = lambda s: "".join(m.group("clean") for m in _clean_str(s))
87
88 def check_file(path, rel_path, messages):
89     with open(path, encoding="utf-8") as f:
90         f = f.read()
91         for srch in pygettexts:
92             m = srch(f)
93             line = pos =0
94             while m:
95                 d = m.groupdict()
96                 # Context.
97                 ctxt = d.get("ctxt_raw")
98                 if ctxt:
99                     if ctxt in CONTEXTS:
100                         ctxt = CONTEXTS[ctxt]
101                     elif '"' in ctxt or "'" in ctxt:
102                         ctxt = clean_str(ctxt)
103                     else:
104                         print("WARNING: raw context “{}” couldn’t be resolved!"
105                               "".format(ctxt))
106                         ctxt = CONTEXT_DEFAULT
107                 else:
108                     ctxt = CONTEXT_DEFAULT
109                 # Message.
110                 msg = d.get("msg_raw")
111                 if msg:
112                     if '"' in msg or "'" in msg:
113                         msg = clean_str(msg)
114                     else:
115                         print("WARNING: raw message “{}” couldn’t be resolved!"
116                               "".format(msg))
117                         msg = ""
118                 else:
119                     msg = ""
120                 # Line.
121                 line += f[pos:m.start()].count('\n')
122                 # And we are done for this item!
123                 messages.setdefault((ctxt, msg), []).append(":".join((rel_path, str(line))))
124                 pos = m.end()
125                 line += f[m.start():pos].count('\n')
126                 m = srch(f, pos)
127
128
129 def py_xgettext(messages):
130     with open(SRC_POTFILES) as src:
131         forbidden = set()
132         forced = set()
133         for l in src:
134             if l[0] == '-':
135                 forbidden.add(l[1:].rstrip('\n'))
136             elif l[0] != '#':
137                 forced.add(l.rstrip('\n'))
138         for root, dirs, files in os.walk(POTFILES_DIR):
139             if "/.svn" in root:
140                 continue
141             for fname in files:
142                 if os.path.splitext(fname)[1] not in PYGETTEXT_ALLOWED_EXTS:
143                     continue
144                 path = os.path.join(root, fname)
145                 rel_path = os.path.relpath(path, SOURCE_DIR)
146                 if rel_path in forbidden | forced:
147                     continue
148                 check_file(path, rel_path, messages)
149         for path in forced:
150             if os.path.exists(path):
151                 check_file(os.path.join(SOURCE_DIR, path), path, messages)
152
153
154 # Spell checking!
155 import enchant
156 dict_spelling = enchant.Dict("en_US")
157
158 from spell_check_utils import (dict_uimsgs,
159                                split_words,
160                               )
161
162 _spell_checked = set()
163 def spell_check(txt, cache):
164     ret = []
165
166     if cache is not None and txt in cache:
167         return ret
168
169     for w in split_words(txt):
170         w_lower = w.lower()
171         if w_lower in dict_uimsgs | _spell_checked:
172             continue
173         if not dict_spelling.check(w):
174             ret.append("{}: suggestions are ({})"
175                        .format(w, "'" + "', '".join(dict_spelling.suggest(w))
176                                   + "'"))
177         else:
178             _spell_checked.add(w_lower)
179
180     if not ret:
181         if cache is not None:
182             cache.add(txt)
183
184     return ret
185
186
187 def get_svnrev():
188     cmd = [SVN_EXECUTABLE,
189            "info",
190            "--xml",
191            SOURCE_DIR,
192            ]
193     xml = subprocess.check_output(cmd)
194     return re.search(b'revision="(\d+)"', xml).group(1)
195
196
197 def gen_empty_pot():
198     blender_rev = get_svnrev()
199     utctime = time.gmtime()
200     time_str = time.strftime("%Y-%m-%d %H:%M+0000", utctime)
201     year_str = time.strftime("%Y", utctime)
202
203     return utils.gen_empty_messages(blender_rev, time_str, year_str)
204
205
206 def merge_messages(msgs, states, messages, do_checks, spell_cache):
207     num_added = num_present = 0
208     for (context, msgid), srcs in messages.items():
209         if do_checks:
210             err = spell_check(msgid, spell_cache)
211             if err:
212                 print("WARNING: spell check failed on “" + msgid + "”:")
213                 print("\t\t" + "\n\t\t".join(err))
214                 print("\tFrom:\n\t\t" + "\n\t\t".join(srcs))
215
216         # Escape some chars in msgid!
217         msgid = msgid.replace("\\", "\\\\")
218         msgid = msgid.replace("\"", "\\\"")
219         msgid = msgid.replace("\t", "\\t")
220
221         srcs = [COMMENT_PREFIX_SOURCE + s for s in srcs]
222
223         key = (context, msgid)
224         if key not in msgs:
225             msgs[key] = {"msgid_lines": [msgid],
226                          "msgstr_lines": [""],
227                          "comment_lines": srcs,
228                          "msgctxt_lines": [context]}
229             num_added += 1
230         else:
231             # We need to merge comments!
232             msgs[key]["comment_lines"].extend(srcs)
233             num_present += 1
234
235     return num_added, num_present
236
237
238 def main():
239     parser = argparse.ArgumentParser(description="Update blender.pot file " \
240                                                  "from messages.txt")
241     parser.add_argument('-w', '--warning', action="store_true",
242                         help="Show warnings.")
243     parser.add_argument('-i', '--input', metavar="File",
244                         help="Input messages file path.")
245     parser.add_argument('-o', '--output', metavar="File",
246                         help="Output pot file path.")
247
248     args = parser.parse_args()
249     if args.input:
250         global FILE_NAME_MESSAGES
251         FILE_NAME_MESSAGES = args.input
252     if args.output:
253         global FILE_NAME_POT
254         FILE_NAME_POT = args.output
255
256     print("Running fake py gettext…")
257     # Not using any more xgettext, simpler to do it ourself!
258     messages = {}
259     py_xgettext(messages)
260     print("Finished, found {} messages.".format(len(messages)))
261
262     if SPELL_CACHE and os.path.exists(SPELL_CACHE):
263         with open(SPELL_CACHE, 'rb') as f:
264             spell_cache = pickle.load(f)
265     else:
266         spell_cache = set()
267     print(len(spell_cache))
268
269     print("Generating POT file {}…".format(FILE_NAME_POT))
270     msgs, states = gen_empty_pot()
271     tot_messages, _a = merge_messages(msgs, states, messages,
272                                       True, spell_cache)
273
274     # add messages collected automatically from RNA
275     print("\tMerging RNA messages from {}…".format(FILE_NAME_MESSAGES))
276     messages = {}
277     with open(FILE_NAME_MESSAGES, encoding="utf-8") as f:
278         srcs = []
279         context = ""
280         for line in f:
281             line = utils.stripeol(line)
282
283             if line.startswith(COMMENT_PREFIX):
284                 srcs.append(line[len(COMMENT_PREFIX):].strip())
285             elif line.startswith(CONTEXT_PREFIX):
286                 context = line[len(CONTEXT_PREFIX):].strip()
287             else:
288                 key = (context, line)
289                 messages[key] = srcs
290                 srcs = []
291                 context = ""
292     num_added, num_present = merge_messages(msgs, states, messages,
293                                             True, spell_cache)
294     tot_messages += num_added
295     print("\tMerged {} messages ({} were already present)."
296           "".format(num_added, num_present))
297
298     # Write back all messages into blender.pot.
299     utils.write_messages(FILE_NAME_POT, msgs, states["comm_msg"],
300                          states["fuzzy_msg"])
301
302     print(len(spell_cache))
303     if SPELL_CACHE and spell_cache:
304         with open(SPELL_CACHE, 'wb') as f:
305             pickle.dump(spell_cache, f)
306
307     print("Finished, total: {} messages!".format(tot_messages - 1))
308
309     return 0
310
311
312 if __name__ == "__main__":
313     print("\n\n *** Running {} *** \n".format(__file__))
314     sys.exit(main())