~ubuntu-branches/ubuntu/trusty/blender/trusty

« back to all changes in this revision

Viewing changes to release/scripts/modules/bl_i18n_utils/utils.py

  • Committer: Package Import Robot
  • Author(s): Jeremy Bicha
  • Date: 2013-03-06 12:08:47 UTC
  • mfrom: (1.5.1) (14.1.8 experimental)
  • Revision ID: package-import@ubuntu.com-20130306120847-frjfaryb2zrotwcg
Tags: 2.66a-1ubuntu1
* Resynchronize with Debian (LP: #1076930, #1089256, #1052743, #999024,
  #1122888, #1147084)
* debian/control:
  - Lower build-depends on libavcodec-dev since we're not
    doing the libav9 transition in Ubuntu yet

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# ***** BEGIN GPL LICENSE BLOCK *****
 
2
#
 
3
# This program is free software; you can redistribute it and/or
 
4
# modify it under the terms of the GNU General Public License
 
5
# as published by the Free Software Foundation; either version 2
 
6
# of the License, or (at your option) any later version.
 
7
#
 
8
# This program is distributed in the hope that it will be useful,
 
9
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
10
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
11
# GNU General Public License for more details.
 
12
#
 
13
# You should have received a copy of the GNU General Public License
 
14
# along with this program; if not, write to the Free Software Foundation,
 
15
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 
16
#
 
17
# ***** END GPL LICENSE BLOCK *****
 
18
 
 
19
# <pep8 compliant>
 
20
 
 
21
# Some misc utilities...
 
22
 
 
23
import collections
 
24
import concurrent.futures
 
25
import copy
 
26
import os
 
27
import re
 
28
import sys
 
29
 
 
30
from bl_i18n_utils import settings
 
31
 
 
32
 
 
33
PO_COMMENT_PREFIX = settings.PO_COMMENT_PREFIX
 
34
PO_COMMENT_PREFIX_MSG = settings.PO_COMMENT_PREFIX_MSG
 
35
PO_COMMENT_PREFIX_SOURCE = settings.PO_COMMENT_PREFIX_SOURCE
 
36
PO_COMMENT_PREFIX_SOURCE_CUSTOM = settings.PO_COMMENT_PREFIX_SOURCE_CUSTOM
 
37
PO_COMMENT_FUZZY = settings.PO_COMMENT_FUZZY
 
38
PO_MSGCTXT = settings.PO_MSGCTXT
 
39
PO_MSGID = settings.PO_MSGID
 
40
PO_MSGSTR = settings.PO_MSGSTR
 
41
 
 
42
PO_HEADER_KEY = settings.PO_HEADER_KEY
 
43
PO_HEADER_COMMENT = settings.PO_HEADER_COMMENT
 
44
PO_HEADER_COMMENT_COPYRIGHT = settings.PO_HEADER_COMMENT_COPYRIGHT
 
45
PO_HEADER_MSGSTR = settings.PO_HEADER_MSGSTR
 
46
 
 
47
PARSER_CACHE_HASH = settings.PARSER_CACHE_HASH
 
48
 
 
49
WARN_NC = settings.WARN_MSGID_NOT_CAPITALIZED
 
50
NC_ALLOWED = settings.WARN_MSGID_NOT_CAPITALIZED_ALLOWED
 
51
PARSER_CACHE_HASH = settings.PARSER_CACHE_HASH
 
52
 
 
53
 
 
54
##### Misc Utils #####
 
55
 
 
56
def stripeol(s):
 
57
    return s.rstrip("\n\r")
 
58
 
 
59
 
 
60
_valid_po_path_re = re.compile(r"^\S+:[0-9]+$")
 
61
def is_valid_po_path(path):
 
62
    return bool(_valid_po_path_re.match(path))
 
63
 
 
64
 
 
65
def get_best_similar(data):
 
66
    import difflib
 
67
    key, use_similar, similar_pool = data
 
68
 
 
69
    # try to find some close key in existing messages...
 
70
    # Optimized code inspired by difflib.get_close_matches (as we only need the best match).
 
71
    # We also consider to never make a match when len differs more than -len_key / 2, +len_key * 2 (which is valid
 
72
    # as long as use_similar is not below ~0.7).
 
73
    # Gives an overall ~20% of improvement!
 
74
    #tmp = difflib.get_close_matches(key[1], similar_pool, n=1, cutoff=use_similar)
 
75
    #if tmp:
 
76
        #tmp = tmp[0]
 
77
    tmp = None
 
78
    s = difflib.SequenceMatcher()
 
79
    s.set_seq2(key[1])
 
80
    len_key = len(key[1])
 
81
    min_len = len_key // 2
 
82
    max_len = len_key * 2
 
83
    for x in similar_pool:
 
84
        if min_len < len(x) < max_len:
 
85
            s.set_seq1(x)
 
86
            if s.real_quick_ratio() >= use_similar and s.quick_ratio() >= use_similar:
 
87
                sratio = s.ratio()
 
88
                if sratio >= use_similar:
 
89
                    tmp = x
 
90
                    use_similar = sratio
 
91
    return key, tmp
 
92
 
 
93
 
 
94
class I18nMessage:
 
95
    """
 
96
    Internal representation of a message.
 
97
    """
 
98
    __slots__ = ("msgctxt_lines", "msgid_lines", "msgstr_lines", "comment_lines", "is_fuzzy", "is_commented")
 
99
 
 
100
    def __init__(self, msgctxt_lines=[], msgid_lines=[], msgstr_lines=[], comment_lines=[],
 
101
                 is_commented=False, is_fuzzy=False):
 
102
        self.msgctxt_lines = msgctxt_lines
 
103
        self.msgid_lines = msgid_lines
 
104
        self.msgstr_lines = msgstr_lines
 
105
        self.comment_lines = comment_lines
 
106
        self.is_fuzzy = is_fuzzy
 
107
        self.is_commented = is_commented
 
108
 
 
109
    def _get_msgctxt(self):
 
110
        return ("".join(self.msgctxt_lines)).replace("\\n", "\n")
 
111
    def _set_msgctxt(self, ctxt):
 
112
        self.msgctxt_lines = [ctxt]
 
113
    msgctxt = property(_get_msgctxt, _set_msgctxt)
 
114
 
 
115
    def _get_msgid(self):
 
116
        return ("".join(self.msgid_lines)).replace("\\n", "\n")
 
117
    def _set_msgid(self, msgid):
 
118
        self.msgid_lines = [msgid]
 
119
    msgid = property(_get_msgid, _set_msgid)
 
120
 
 
121
    def _get_msgstr(self):
 
122
        return ("".join(self.msgstr_lines)).replace("\\n", "\n")
 
123
    def _set_msgstr(self, msgstr):
 
124
        self.msgstr_lines = [msgstr]
 
125
    msgstr = property(_get_msgstr, _set_msgstr)
 
126
 
 
127
    def _get_sources(self):
 
128
        lstrip1 = len(PO_COMMENT_PREFIX_SOURCE)
 
129
        lstrip2 = len(PO_COMMENT_PREFIX_SOURCE_CUSTOM)
 
130
        return ([l[lstrip1:] for l in self.comment_lines if l.startswith(PO_COMMENT_PREFIX_SOURCE)] +
 
131
                [l[lstrip2:] for l in self.comment_lines if l.startswith(PO_COMMENT_PREFIX_SOURCE_CUSTOM)])
 
132
    def _set_sources(self, sources):
 
133
        # list.copy() is not available in py3.2 ...
 
134
        cmmlines = []
 
135
        cmmlines[:] = self.comment_lines
 
136
        for l in cmmlines:
 
137
            if l.startswith(PO_COMMENT_PREFIX_SOURCE) or l.startswith(PO_COMMENT_PREFIX_SOURCE_CUSTOM):
 
138
                self.comment_lines.remove(l)
 
139
        lines_src = []
 
140
        lines_src_custom = []
 
141
        for src in  sources:
 
142
            if is_valid_po_path(src):
 
143
                lines_src.append(PO_COMMENT_PREFIX_SOURCE + src)
 
144
            else:
 
145
                lines_src_custom.append(PO_COMMENT_PREFIX_SOURCE_CUSTOM + src)
 
146
        self.comment_lines += lines_src_custom + lines_src
 
147
    sources = property(_get_sources, _set_sources)
 
148
 
 
149
    def _get_is_tooltip(self):
 
150
        # XXX For now, we assume that all messages > 30 chars are tooltips!
 
151
        return len(self.msgid) > 30
 
152
    is_tooltip = property(_get_is_tooltip)
 
153
 
 
154
    def normalize(self, max_len=80):
 
155
        """
 
156
        Normalize this message, call this before exporting it...
 
157
        Currently normalize msgctxt, msgid and msgstr lines to given max_len (if below 1, make them single line).
 
158
        """
 
159
        max_len -= 2  # The two quotes!
 
160
        # We do not need the full power of textwrap... We just split first at escaped new lines, then into each line
 
161
        # if needed... No word splitting, nor fancy spaces handling!
 
162
        def _wrap(text, max_len, init_len):
 
163
            if len(text) + init_len < max_len:
 
164
                return [text]
 
165
            lines = text.splitlines()
 
166
            ret = []
 
167
            for l in lines:
 
168
                tmp = []
 
169
                cur_len = 0
 
170
                words = l.split(' ')
 
171
                for w in words:
 
172
                    cur_len += len(w) + 1
 
173
                    if cur_len > (max_len - 1) and tmp:
 
174
                        ret.append(" ".join(tmp) + " ")
 
175
                        del tmp[:]
 
176
                        cur_len = len(w) + 1
 
177
                    tmp.append(w)
 
178
                if tmp:
 
179
                    ret.append(" ".join(tmp))
 
180
            return ret
 
181
        if max_len < 1:
 
182
            self.msgctxt_lines = self.msgctxt.replace("\n", "\\n\n").splitlines()
 
183
            self.msgid_lines = self.msgid.replace("\n", "\\n\n").splitlines()
 
184
            self.msgstr_lines = self.msgstr.replace("\n", "\\n\n").splitlines()
 
185
        else:
 
186
            init_len = len(PO_MSGCTXT) + 1
 
187
            if self.is_commented:
 
188
                init_len += len(PO_COMMENT_PREFIX_MSG)
 
189
            self.msgctxt_lines = _wrap(self.msgctxt.replace("\n", "\\n\n"), max_len, init_len)
 
190
 
 
191
            init_len = len(PO_MSGID) + 1
 
192
            if self.is_commented:
 
193
                init_len += len(PO_COMMENT_PREFIX_MSG)
 
194
            self.msgid_lines = _wrap(self.msgid.replace("\n", "\\n\n"), max_len, init_len)
 
195
 
 
196
            init_len = len(PO_MSGSTR) + 1
 
197
            if self.is_commented:
 
198
                init_len += len(PO_COMMENT_PREFIX_MSG)
 
199
            self.msgstr_lines = _wrap(self.msgstr.replace("\n", "\\n\n"), max_len, init_len)
 
200
 
 
201
 
 
202
class I18nMessages:
 
203
    """
 
204
    Internal representation of messages for one language (iso code), with additional stats info.
 
205
    """
 
206
 
 
207
    # Avoid parsing again!
 
208
    # Keys should be (pseudo) file-names, values are tuples (hash, I18nMessages)
 
209
    # Note: only used by po parser currently!
 
210
    _parser_cache = {}
 
211
 
 
212
    def __init__(self, iso="__POT__", kind=None, key=None, src=None):
 
213
        self.iso = iso
 
214
        self.msgs = self._new_messages()
 
215
        self.trans_msgs = set()
 
216
        self.fuzzy_msgs = set()
 
217
        self.comm_msgs = set()
 
218
        self.ttip_msgs = set()
 
219
        self.contexts = set()
 
220
        self.nbr_msgs = 0
 
221
        self.nbr_trans_msgs = 0
 
222
        self.nbr_ttips = 0
 
223
        self.nbr_trans_ttips = 0
 
224
        self.nbr_comm_msgs = 0
 
225
        self.nbr_signs = 0
 
226
        self.nbr_trans_signs = 0
 
227
        self.parsing_errors = []
 
228
        if kind and src:
 
229
            self.parse(kind, key, src)
 
230
        self.update_info()
 
231
 
 
232
    @staticmethod
 
233
    def _new_messages():
 
234
        return getattr(collections, 'OrderedDict', dict)()
 
235
 
 
236
    @classmethod
 
237
    def gen_empty_messages(cls, iso, blender_ver, blender_rev, time, year, default_copyright=True):
 
238
        """Generate an empty I18nMessages object (only header is present!)."""
 
239
        msgstr = PO_HEADER_MSGSTR.format(blender_ver=str(blender_ver), blender_rev=int(blender_rev),
 
240
                                         time=str(time), iso=str(iso))
 
241
        comment = ""
 
242
        if default_copyright:
 
243
            comment = PO_HEADER_COMMENT_COPYRIGHT.format(year=str(year))
 
244
        comment = comment + PO_HEADER_COMMENT
 
245
 
 
246
        msgs = cls(iso=iso)
 
247
        msgs.msgs[PO_HEADER_KEY] = I18nMessage([], [""], [msgstr], [comment], False, True)
 
248
        msgs.update_info()
 
249
 
 
250
        return msgs
 
251
 
 
252
    def normalize(self, max_len=80):
 
253
        for msg in self.msgs.values():
 
254
            msg.normalize(max_len)
 
255
 
 
256
    def merge(self, replace=False, *args):
 
257
        pass
 
258
 
 
259
    def update(self, ref, use_similar=0.75, keep_old_commented=True):
 
260
        """
 
261
        Update this I18nMessage with the ref one. Translations from ref are never used. Source comments from ref
 
262
        completely replace current ones. If use_similar is not 0.0, it will try to match new messages in ref with an
 
263
        existing one. Messages no more found in ref will be marked as commented if keep_old_commented is True,
 
264
        or removed.
 
265
        """
 
266
        similar_pool = {}
 
267
        if use_similar > 0.0:
 
268
            for key, msg in self.msgs.items():
 
269
                if msg.msgstr:  # No need to waste time with void translations!
 
270
                    similar_pool.setdefault(key[1], set()).add(key)
 
271
 
 
272
        msgs = self._new_messages().fromkeys(ref.msgs.keys())
 
273
        ref_keys = set(ref.msgs.keys())
 
274
        org_keys = set(self.msgs.keys())
 
275
        new_keys = ref_keys - org_keys
 
276
        removed_keys = org_keys - ref_keys
 
277
 
 
278
        # First process keys present in both org and ref messages.
 
279
        for key in ref_keys - new_keys:
 
280
            msg, refmsg = self.msgs[key], ref.msgs[key]
 
281
            msg.sources = refmsg.sources
 
282
            msg.is_commented = refmsg.is_commented
 
283
            msg.is_fuzzy = refmsg.is_fuzzy
 
284
            msgs[key] = msg
 
285
 
 
286
        # Next process new keys.
 
287
        if use_similar > 0.0:
 
288
            with concurrent.futures.ProcessPoolExecutor() as exctr:
 
289
                for key, msgid in exctr.map(get_best_similar,
 
290
                                            tuple((nk, use_similar, tuple(similar_pool.keys())) for nk in new_keys)):
 
291
                    if msgid:
 
292
                        # Try to get the same context, else just get one...
 
293
                        skey = (key[0], msgid)
 
294
                        if skey not in similar_pool[msgid]:
 
295
                            skey = tuple(similar_pool[msgid])[0]
 
296
                        # We keep org translation and comments, and mark message as fuzzy.
 
297
                        msg, refmsg = copy.deepcopy(self.msgs[skey]), ref.msgs[key]
 
298
                        msg.msgctxt = refmsg.msgctxt
 
299
                        msg.msgid = refmsg.msgid
 
300
                        msg.sources = refmsg.sources
 
301
                        msg.is_fuzzy = True
 
302
                        msg.is_commented = refmsg.is_commented
 
303
                        msgs[key] = msg
 
304
                    else:
 
305
                        msgs[key] = ref.msgs[key]
 
306
        else:
 
307
            for key in new_keys:
 
308
                msgs[key] = ref.msgs[key]
 
309
 
 
310
        # Add back all "old" and already commented messages as commented ones, if required
 
311
        # (and translation was not void!).
 
312
        if keep_old_commented:
 
313
            for key in removed_keys:
 
314
                msgs[key] = self.msgs[key]
 
315
                msgs[key].is_commented = True
 
316
                msgs[key].sources = []
 
317
 
 
318
        # Special 'meta' message, change project ID version and pot creation date...
 
319
        key = ("", "")
 
320
        rep = []
 
321
        markers = ("Project-Id-Version:", "POT-Creation-Date:")
 
322
        for mrk in markers:
 
323
            for rl in ref.msgs[key].msgstr_lines:
 
324
                if rl.startswith(mrk):
 
325
                    for idx, ml in enumerate(msgs[key].msgstr_lines):
 
326
                        if ml.startswith(mrk):
 
327
                            rep.append((idx, rl))
 
328
        for idx, txt in rep:
 
329
            msgs[key].msgstr_lines[idx] = txt
 
330
 
 
331
        # And finalize the update!
 
332
        self.msgs = msgs
 
333
 
 
334
    def update_info(self):
 
335
        self.trans_msgs.clear()
 
336
        self.fuzzy_msgs.clear()
 
337
        self.comm_msgs.clear()
 
338
        self.ttip_msgs.clear()
 
339
        self.contexts.clear()
 
340
        self.nbr_signs = 0
 
341
        self.nbr_trans_signs = 0
 
342
        for key, msg in self.msgs.items():
 
343
            if key == PO_HEADER_KEY:
 
344
                continue
 
345
            if msg.is_commented:
 
346
                self.comm_msgs.add(key)
 
347
            else:
 
348
                if msg.msgstr:
 
349
                    self.trans_msgs.add(key)
 
350
                if msg.is_fuzzy:
 
351
                    self.fuzzy_msgs.add(key)
 
352
                if msg.is_tooltip:
 
353
                    self.ttip_msgs.add(key)
 
354
                self.contexts.add(key[0])
 
355
                self.nbr_signs += len(msg.msgid)
 
356
                self.nbr_trans_signs += len(msg.msgstr)
 
357
        self.nbr_msgs = len(self.msgs)
 
358
        self.nbr_trans_msgs = len(self.trans_msgs)
 
359
        self.nbr_ttips = len(self.ttip_msgs)
 
360
        self.nbr_trans_ttips = len(self.ttip_msgs & self.trans_msgs)
 
361
        self.nbr_comm_msgs = len(self.comm_msgs)
 
362
 
 
363
    def print_stats(self, prefix=""):
 
364
        """
 
365
        Print out some stats about an I18nMessages object.
 
366
        """
 
367
        lvl = 0.0
 
368
        lvl_ttips = 0.0
 
369
        lvl_comm = 0.0
 
370
        lvl_trans_ttips = 0.0
 
371
        lvl_ttips_in_trans = 0.0
 
372
        if self.nbr_msgs > 0:
 
373
            lvl = float(self.nbr_trans_msgs) / float(self.nbr_msgs)
 
374
            lvl_ttips = float(self.nbr_ttips) / float(self.nbr_msgs)
 
375
            lvl_comm = float(self.nbr_comm_msgs) / float(self.nbr_msgs + self.nbr_comm_msgs)
 
376
        if self.nbr_ttips > 0:
 
377
            lvl_trans_ttips = float(self.nbr_trans_ttips) / float(self.nbr_ttips)
 
378
        if self.nbr_trans_msgs > 0:
 
379
            lvl_ttips_in_trans = float(self.nbr_trans_ttips) / float(self.nbr_trans_msgs)
 
380
 
 
381
        lines = ("",
 
382
                 "{:>6.1%} done! ({} translated messages over {}).\n"
 
383
                 "".format(lvl, self.nbr_trans_msgs, self.nbr_msgs),
 
384
                 "{:>6.1%} of messages are tooltips ({} over {}).\n"
 
385
                 "".format(lvl_ttips, self.nbr_ttips, self.nbr_msgs),
 
386
                 "{:>6.1%} of tooltips are translated ({} over {}).\n"
 
387
                 "".format(lvl_trans_ttips, self.nbr_trans_ttips, self.nbr_ttips),
 
388
                 "{:>6.1%} of translated messages are tooltips ({} over {}).\n"
 
389
                 "".format(lvl_ttips_in_trans, self.nbr_trans_ttips, self.nbr_trans_msgs),
 
390
                 "{:>6.1%} of messages are commented ({} over {}).\n"
 
391
                 "".format(lvl_comm, self.nbr_comm_msgs, self.nbr_comm_msgs + self.nbr_msgs),
 
392
                 "This translation is currently made of {} signs.\n".format(self.nbr_trans_signs))
 
393
        print(prefix.join(lines))
 
394
 
 
395
    def parse(self, kind, key, src):
 
396
        del self.parsing_errors[:]
 
397
        self.parsers[kind](self, src, key)
 
398
        if self.parsing_errors:
 
399
            print("WARNING! Errors while parsing {}:".format(key))
 
400
            for line, error in self.parsing_errors:
 
401
                print("    Around line {}: {}".format(line, error))
 
402
            print("The parser solved them as well as it could...")
 
403
        self.update_info()
 
404
 
 
405
    def parse_messages_from_po(self, src, key=None):
 
406
        """
 
407
        Parse a po file.
 
408
        Note: This function will silently "arrange" mis-formated entries, thus using afterward write_messages() should
 
409
              always produce a po-valid file, though not correct!
 
410
        """
 
411
        reading_msgid = False
 
412
        reading_msgstr = False
 
413
        reading_msgctxt = False
 
414
        reading_comment = False
 
415
        is_commented = False
 
416
        is_fuzzy = False
 
417
        msgctxt_lines = []
 
418
        msgid_lines = []
 
419
        msgstr_lines = []
 
420
        comment_lines = []
 
421
 
 
422
        # Helper function
 
423
        def finalize_message(self, line_nr):
 
424
            nonlocal reading_msgid, reading_msgstr, reading_msgctxt, reading_comment
 
425
            nonlocal is_commented, is_fuzzy, msgid_lines, msgstr_lines, msgctxt_lines, comment_lines
 
426
 
 
427
            msgid = "".join(msgid_lines)
 
428
            msgctxt = "".join(msgctxt_lines)
 
429
            msgkey = (msgctxt, msgid)
 
430
 
 
431
            # Never allow overriding existing msgid/msgctxt pairs!
 
432
            if msgkey in self.msgs:
 
433
                self.parsing_errors.append((line_nr, "{} context/msgid is already in current messages!".format(msgkey)))
 
434
                return
 
435
 
 
436
            self.msgs[msgkey] = I18nMessage(msgctxt_lines, msgid_lines, msgstr_lines, comment_lines,
 
437
                                            is_commented, is_fuzzy)
 
438
 
 
439
            # Let's clean up and get ready for next message!
 
440
            reading_msgid = reading_msgstr = reading_msgctxt = reading_comment = False
 
441
            is_commented = is_fuzzy = False
 
442
            msgctxt_lines = []
 
443
            msgid_lines = []
 
444
            msgstr_lines = []
 
445
            comment_lines = []
 
446
 
 
447
        # try to use src as file name...
 
448
        if os.path.exists(src):
 
449
            if not key:
 
450
                key = src
 
451
            with open(src, 'r', encoding="utf-8") as f:
 
452
                src = f.read()
 
453
 
 
454
        # Try to use values from cache!
 
455
        curr_hash = None
 
456
        if key and key in self._parser_cache:
 
457
            old_hash, msgs = self._parser_cache[key]
 
458
            import hashlib
 
459
            curr_hash = hashlib.new(PARSER_CACHE_HASH, src.encode()).digest()
 
460
            if curr_hash == old_hash:
 
461
                self.msgs = copy.deepcopy(msgs)  # we might edit self.msgs!
 
462
                return
 
463
 
 
464
        _comm_msgctxt = PO_COMMENT_PREFIX_MSG + PO_MSGCTXT
 
465
        _len_msgctxt = len(PO_MSGCTXT + '"')
 
466
        _len_comm_msgctxt = len(_comm_msgctxt + '"')
 
467
        _comm_msgid = PO_COMMENT_PREFIX_MSG + PO_MSGID
 
468
        _len_msgid = len(PO_MSGID + '"')
 
469
        _len_comm_msgid = len(_comm_msgid + '"')
 
470
        _comm_msgstr = PO_COMMENT_PREFIX_MSG + PO_MSGSTR
 
471
        _len_msgstr = len(PO_MSGSTR + '"')
 
472
        _len_comm_msgstr = len(_comm_msgstr + '"')
 
473
        _len_comm_str = len(PO_COMMENT_PREFIX_MSG + '"')
 
474
 
 
475
        # Main loop over all lines in src...
 
476
        for line_nr, line in enumerate(src.splitlines()):
 
477
            if line == "":
 
478
                if reading_msgstr:
 
479
                    finalize_message(self, line_nr)
 
480
                continue
 
481
 
 
482
            elif line.startswith(PO_MSGCTXT) or line.startswith(_comm_msgctxt):
 
483
                reading_comment = False
 
484
                reading_ctxt = True
 
485
                if line.startswith(PO_COMMENT_PREFIX_MSG):
 
486
                    is_commented = True
 
487
                    line = line[_len_comm_msgctxt:-1]
 
488
                else:
 
489
                    line = line[_len_msgctxt:-1]
 
490
                msgctxt_lines.append(line)
 
491
 
 
492
            elif line.startswith(PO_MSGID) or line.startswith(_comm_msgid):
 
493
                reading_comment = False
 
494
                reading_msgid = True
 
495
                if line.startswith(PO_COMMENT_PREFIX_MSG):
 
496
                    if not is_commented and reading_ctxt:
 
497
                        self.parsing_errors.append((line_nr, "commented msgid following regular msgctxt"))
 
498
                    is_commented = True
 
499
                    line = line[_len_comm_msgid:-1]
 
500
                else:
 
501
                    line = line[_len_msgid:-1]
 
502
                reading_ctxt = False
 
503
                msgid_lines.append(line)
 
504
 
 
505
            elif line.startswith(PO_MSGSTR) or line.startswith(_comm_msgstr):
 
506
                if not reading_msgid:
 
507
                    self.parsing_errors.append((line_nr, "msgstr without a prior msgid"))
 
508
                else:
 
509
                    reading_msgid = False
 
510
                reading_msgstr = True
 
511
                if line.startswith(PO_COMMENT_PREFIX_MSG):
 
512
                    line = line[_len_comm_msgstr:-1]
 
513
                    if not is_commented:
 
514
                        self.parsing_errors.append((line_nr, "commented msgstr following regular msgid"))
 
515
                else:
 
516
                    line = line[_len_msgstr:-1]
 
517
                    if is_commented:
 
518
                        self.parsing_errors.append((line_nr, "regular msgstr following commented msgid"))
 
519
                msgstr_lines.append(line)
 
520
 
 
521
            elif line.startswith(PO_COMMENT_PREFIX[0]):
 
522
                if line.startswith(PO_COMMENT_PREFIX_MSG):
 
523
                    if reading_msgctxt:
 
524
                        if is_commented:
 
525
                            msgctxt_lines.append(line[_len_comm_str:-1])
 
526
                        else:
 
527
                            msgctxt_lines.append(line)
 
528
                            self.parsing_errors.append((line_nr, "commented string while reading regular msgctxt"))
 
529
                    elif reading_msgid:
 
530
                        if is_commented:
 
531
                            msgid_lines.append(line[_len_comm_str:-1])
 
532
                        else:
 
533
                            msgid_lines.append(line)
 
534
                            self.parsing_errors.append((line_nr, "commented string while reading regular msgid"))
 
535
                    elif reading_msgstr:
 
536
                        if is_commented:
 
537
                            msgstr_lines.append(line[_len_comm_str:-1])
 
538
                        else:
 
539
                            msgstr_lines.append(line)
 
540
                            self.parsing_errors.append((line_nr, "commented string while reading regular msgstr"))
 
541
                else:
 
542
                    if reading_msgctxt or reading_msgid or reading_msgstr:
 
543
                        self.parsing_errors.append((line_nr,
 
544
                                                    "commented string within msgctxt, msgid or msgstr scope, ignored"))
 
545
                    elif line.startswith(PO_COMMENT_FUZZY):
 
546
                        is_fuzzy = True
 
547
                    else:
 
548
                        comment_lines.append(line)
 
549
                    reading_comment = True
 
550
 
 
551
            else:
 
552
                if reading_msgctxt:
 
553
                    msgctxt_lines.append(line[1:-1])
 
554
                elif reading_msgid:
 
555
                    msgid_lines.append(line[1:-1])
 
556
                elif reading_msgstr:
 
557
                    line = line[1:-1]
 
558
                    msgstr_lines.append(line)
 
559
                else:
 
560
                    self.parsing_errors.append((line_nr, "regular string outside msgctxt, msgid or msgstr scope"))
 
561
                    #self.parsing_errors += (str(comment_lines), str(msgctxt_lines), str(msgid_lines), str(msgstr_lines))
 
562
 
 
563
        # If no final empty line, last message is not finalized!
 
564
        if reading_msgstr:
 
565
            finalize_message(self, line_nr)
 
566
 
 
567
        if key:
 
568
            if not curr_hash:
 
569
                import hashlib
 
570
                curr_hash = hashlib.new(PARSER_CACHE_HASH, src.encode()).digest()
 
571
            self._parser_cache[key] = (curr_hash, self.msgs)
 
572
 
 
573
    def write(self, kind, dest):
 
574
        self.writers[kind](self, dest)
 
575
 
 
576
    def write_messages_to_po(self, fname):
 
577
        """
 
578
        Write messages in fname po file.
 
579
        """
 
580
        self.normalize(max_len=0)  # No wrapping for now...
 
581
        with open(fname, 'w', encoding="utf-8") as f:
 
582
            for msg in self.msgs.values():
 
583
                f.write("\n".join(msg.comment_lines))
 
584
                # Only mark as fuzzy if msgstr is not empty!
 
585
                if msg.is_fuzzy and msg.msgstr:
 
586
                    f.write("\n" + PO_COMMENT_FUZZY)
 
587
                _p = PO_COMMENT_PREFIX_MSG if msg.is_commented else ""
 
588
                _pmsgctxt = _p + PO_MSGCTXT
 
589
                _pmsgid = _p + PO_MSGID
 
590
                _pmsgstr = _p + PO_MSGSTR
 
591
                chunks = []
 
592
                if msg.msgctxt:
 
593
                    if len(msg.msgctxt_lines) > 1:
 
594
                        chunks += [
 
595
                            "\n" + _pmsgctxt + "\"\"\n" + _p + "\"",
 
596
                            ("\"\n" + _p + "\"").join(msg.msgctxt_lines),
 
597
                            "\"",
 
598
                        ]
 
599
                    else:
 
600
                        chunks += ["\n" + _pmsgctxt + "\"" + msg.msgctxt + "\""]
 
601
                if len(msg.msgid_lines) > 1:
 
602
                    chunks += [
 
603
                        "\n" + _pmsgid + "\"\"\n" + _p + "\"",
 
604
                        ("\"\n" + _p + "\"").join(msg.msgid_lines),
 
605
                        "\"",
 
606
                    ]
 
607
                else:
 
608
                    chunks += ["\n" + _pmsgid + "\"" + msg.msgid + "\""]
 
609
                if len(msg.msgstr_lines) > 1:
 
610
                    chunks += [
 
611
                        "\n" + _pmsgstr + "\"\"\n" + _p + "\"",
 
612
                        ("\"\n" + _p + "\"").join(msg.msgstr_lines),
 
613
                        "\"",
 
614
                    ]
 
615
                else:
 
616
                    chunks += ["\n" + _pmsgstr + "\"" + msg.msgstr + "\""]
 
617
                chunks += ["\n\n"]
 
618
                f.write("".join(chunks))
 
619
 
 
620
    parsers = {
 
621
        "PO": parse_messages_from_po,
 
622
#        "PYTUPLE": parse_messages_from_pytuple,
 
623
    }
 
624
 
 
625
    writers = {
 
626
        "PO": write_messages_to_po,
 
627
        #"PYDICT": write_messages_to_pydict,
 
628
    }
 
629
 
 
630
 
 
631
class I18n:
 
632
    """
 
633
    Internal representation of a whole translation set.
 
634
    """
 
635
 
 
636
    def __init__(self, src):
 
637
        self.trans = {}
 
638
        self.update_info()
 
639
 
 
640
    def update_info(self):
 
641
        self.nbr_trans = 0
 
642
        self.lvl = 0.0
 
643
        self.lvl_ttips = 0.0
 
644
        self.lvl_trans_ttips = 0.0
 
645
        self.lvl_ttips_in_trans = 0.0
 
646
        self.lvl_comm = 0.0
 
647
        self.nbr_signs = 0
 
648
        self.nbr_trans_signs = 0
 
649
        self.contexts = set()
 
650
 
 
651
        if TEMPLATE_ISO_ID in self.trans:
 
652
            self.nbr_trans = len(self.trans) - 1
 
653
            self.nbr_signs = self.trans[TEMPLATE_ISO_ID].nbr_signs
 
654
        else:
 
655
            self.nbr_trans = len(self.trans)
 
656
        for iso, msgs in self.trans.items():
 
657
            msgs.update_info()
 
658
            if msgs.nbr_msgs > 0:
 
659
                self.lvl += float(msgs.nbr_trans_msgs) / float(msgs.nbr_msgs)
 
660
                self.lvl_ttips += float(msgs.nbr_ttips) / float(msgs.nbr_msgs)
 
661
                self.lvl_comm += float(msgs.nbr_comm_msgs) / float(msgs.nbr_msgs + msgs.nbr_comm_msgs)
 
662
            if msgs.nbr_ttips > 0:
 
663
                self.lvl_trans_ttips = float(msgs.nbr_trans_ttips) / float(msgs.nbr_ttips)
 
664
            if msgs.nbr_trans_msgs > 0:
 
665
                self.lvl_ttips_in_trans = float(msgs.nbr_trans_ttips) / float(msgs.nbr_trans_msgs)
 
666
            if self.nbr_signs == 0:
 
667
                self.nbr_signs = msgs.nbr_signs
 
668
            self.nbr_trans_signs += msgs.nbr_trans_signs
 
669
            self.contexts |= msgs.contexts
 
670
 
 
671
    def print_stats(self, prefix="", print_msgs=True):
 
672
        """
 
673
        Print out some stats about an I18n object.
 
674
        If print_msgs is True, it will also print all its translations' stats.
 
675
        """
 
676
        if print_msgs:
 
677
            msgs_prefix = prefix + "    "
 
678
            for key, msgs in self.trans:
 
679
                if key == TEMPLATE_ISO_ID:
 
680
                    continue
 
681
                print(prefix + key + ":")
 
682
                msgs.print_stats(prefix=msgs_prefix)
 
683
                print(prefix)
 
684
 
 
685
        nbr_contexts = len(self.contexts - {CONTEXT_DEFAULT})
 
686
        if nbr_contexts != 1:
 
687
            if nbr_contexts == 0:
 
688
                nbr_contexts = "No"
 
689
            _ctx_txt = "s are"
 
690
        else:
 
691
            _ctx_txt = " is"
 
692
        lines = ("",
 
693
                 "Average stats for all {} translations:\n".format(self.nbr_trans),
 
694
                 "    {:>6.1%} done!\n".format(self.lvl / self.nbr_trans),
 
695
                 "    {:>6.1%} of messages are tooltips.\n".format(self.lvl_ttips / self.nbr_trans),
 
696
                 "    {:>6.1%} of tooltips are translated.\n".format(self.lvl_trans_ttips / self.nbr_trans),
 
697
                 "    {:>6.1%} of translated messages are tooltips.\n".format(self.lvl_ttips_in_trans / self.nbr_trans),
 
698
                 "    {:>6.1%} of messages are commented.\n".format(self.lvl_comm / self.nbr_trans),
 
699
                 "    The org msgids are currently made of {} signs.\n".format(self.nbr_signs),
 
700
                 "    All processed translations are currently made of {} signs.\n".format(self.nbr_trans_signs),
 
701
                 "    {} specific context{} present:\n            {}\n"
 
702
                 "".format(self.nbr_contexts, _ctx_txt, "\n            ".join(self.contexts - {CONTEXT_DEFAULT})),
 
703
                 "\n")
 
704
        print(prefix.join(lines))
 
705
 
 
706
 
 
707
##### Parsers #####
 
708
 
 
709
#def parse_messages_from_pytuple(self, src, key=None):
 
710
    #"""
 
711
    #Returns a dict of tuples similar to the one returned by parse_messages_from_po (one per language, plus a 'pot'
 
712
    #one keyed as '__POT__').
 
713
    #"""
 
714
    ## src may be either a string to be interpreted as py code, or a real tuple!
 
715
    #if isinstance(src, str):
 
716
        #src = eval(src)
 
717
#
 
718
    #curr_hash = None
 
719
    #if key and key in _parser_cache:
 
720
        #old_hash, ret = _parser_cache[key]
 
721
        #import hashlib
 
722
        #curr_hash = hashlib.new(PARSER_CACHE_HASH, str(src).encode()).digest()
 
723
        #if curr_hash == old_hash:
 
724
            #return ret
 
725
#
 
726
    #pot = new_messages()
 
727
    #states = gen_states()
 
728
    #stats = gen_stats()
 
729
    #ret = {"__POT__": (pot, states, stats)}
 
730
    #for msg in src:
 
731
        #key = msg[0]
 
732
        #messages[msgkey] = gen_message(msgid_lines, msgstr_lines, comment_lines, msgctxt_lines)
 
733
        #pot[key] = gen_message(msgid_lines=[key[1]], msgstr_lines=[
 
734
        #for lang, trans, (is_fuzzy, comments) in msg[2:]:
 
735
            #if trans and not is_fuzzy:
 
736
                #i18n_dict.setdefault(lang, dict())[key] = trans
 
737
#
 
738
    #if key:
 
739
        #if not curr_hash:
 
740
            #import hashlib
 
741
            #curr_hash = hashlib.new(PARSER_CACHE_HASH, str(src).encode()).digest()
 
742
        #_parser_cache[key] = (curr_hash, val)
 
743
    #return ret
 
 
b'\\ No newline at end of file'