~nskaggs/help-app/functional-test-template

« back to all changes in this revision

Viewing changes to internals/translations/utils.py

  • Committer: Daniel Holbach
  • Date: 2015-03-19 17:54:55 UTC
  • mfrom: (111.2.14 1429896)
  • Revision ID: daniel.holbach@canonical.com-20150319175455-sjwdzzy3aqaaiu9u
mergedĀ lp:~dholbach/help-app/1429896

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
import codecs
2
 
import copy
3
 
import glob
4
2
import os
5
 
import re
6
 
import shutil
7
 
import subprocess
8
3
import sys
9
4
import tempfile
10
5
 
15
10
    sys.exit(1)
16
11
 
17
12
try:
18
 
    import polib
19
 
except ImportError:
20
 
    require('python3-polib')
21
 
 
22
 
try:
23
13
    import magic
24
14
except:
25
15
    require('python3-magic')
29
19
except:
30
20
    require('python3-markdown')
31
21
 
32
 
from pelicanconf import PATH, MD_EXTENSIONS, META_TAGS, HIDE_FROM_POT
33
 
 
34
 
# This defines how complete we expect translations to be before we
35
 
# generate HTML from them. Needs to be string.
36
 
TRANSLATION_COMPLETION_PERCENTAGE = '0'
 
22
from pelicanconf import (
 
23
    MD_EXTENSIONS,
 
24
    TOP_LEVEL_DIR,
 
25
)
37
26
 
38
27
BCP47_OVERRIDES = (
39
28
    ('zh_CN', 'zh-hans'),
47
36
]
48
37
 
49
38
 
 
39
def normalise_path(path):
 
40
    return os.path.relpath(path, TOP_LEVEL_DIR)
 
41
 
 
42
 
 
43
def full_path(path):
 
44
    return os.path.abspath(os.path.join(TOP_LEVEL_DIR, path))
 
45
 
 
46
 
 
47
def use_top_level_dir():
 
48
    pwd = os.getcwd()
 
49
    os.chdir(TOP_LEVEL_DIR)
 
50
    return pwd
 
51
 
 
52
 
50
53
def _temp_write_markdown(fn):
51
54
    (ret, tmp) = tempfile.mkstemp()
52
55
    length = 0
65
68
def verify_markdown_file(fn):
66
69
    ms = magic.open(magic.MAGIC_NONE)
67
70
    ms.load()
 
71
    fn = full_path(fn)
68
72
    if ms.file(fn) not in MD_MAGIC_FILE_TYPES:
69
73
        return False
70
74
    (ret, length) = _temp_write_markdown(fn)
78
82
        return gettext_code.lower().replace('_', '-')
79
83
    return [c[1] for c in BCP47_OVERRIDES
80
84
            if c[0] == gettext_code][0]
81
 
 
82
 
 
83
 
class PO4A(object):
84
 
    def __init__(self):
85
 
        self.default_args = [
86
 
            '-f', 'text',
87
 
            '-o', 'markdown',
88
 
            '-M', 'utf-8',
89
 
            ]
90
 
        if not shutil.which('po4a'):
91
 
            require('po4a')
92
 
 
93
 
    def run(self, po4a_command, additional_args, with_output=False):
94
 
        args = copy.copy(self.default_args)
95
 
        args += additional_args
96
 
        if with_output:
97
 
            ret = subprocess.Popen([po4a_command]+args, stdout=subprocess.PIPE)
98
 
        else:
99
 
            ret = subprocess.call([po4a_command]+args)
100
 
        return ret
101
 
 
102
 
    def gettextize(self, document_fns, pot_file):
103
 
        args = copy.copy(self.default_args)
104
 
        for document_fn in document_fns:
105
 
            args += ['-m', document_fn]
106
 
        args += [
107
 
            '-p', pot_file,
108
 
            '-L', 'utf-8',
109
 
            ]
110
 
        ret = self.run('po4a-gettextize', args)
111
 
        return (not ret)
112
 
 
113
 
    def translate(self, doc, po_fn):
114
 
        args = [
115
 
            '-k', TRANSLATION_COMPLETION_PERCENTAGE,
116
 
            '-m', doc,
117
 
            '-p', po_fn,
118
 
            '-L', 'utf-8',
119
 
            ]
120
 
        return self.run('po4a-translate', args, with_output=True)
121
 
 
122
 
 
123
 
class POFile(object):
124
 
    def __init__(self, po_fn):
125
 
        self.po_fn = po_fn
126
 
        self.pofile = polib.pofile(po_fn)
127
 
 
128
 
    def reread(self):
129
 
        self.pofile = polib.pofile(self.po_fn)
130
 
 
131
 
    def merge(self, pot_file_ob):
132
 
        self.pofile.merge(pot_file_ob)
133
 
 
134
 
    def save(self):
135
 
        self.pofile.save(self.po_fn)
136
 
 
137
 
    def find_in_msgid(self, find_str, translated=True, fuzzy=True,
138
 
                      untranslated=True):
139
 
        entries = []
140
 
        if translated:
141
 
            entries += self.pofile.translated_entries()
142
 
        if fuzzy:
143
 
            entries += self.pofile.fuzzy_entries()
144
 
        if untranslated:
145
 
            entries += self.pofile.untranslated_entries()
146
 
        results = []
147
 
        for entry in entries:
148
 
            if find_str in entry.msgid:
149
 
                results += [entry]
150
 
        return results
151
 
 
152
 
    def hide_attr_list_statements(self):
153
 
        entries = []
154
 
        for statement in HIDE_FROM_POT:
155
 
            entries.extend(self.find_in_msgid(statement))
156
 
        statements = r'|'.join(HIDE_FROM_POT)
157
 
        for entry in entries:
158
 
            matches = re.findall(r'(.*?)\s*?(%s)\s*?' % statements,
159
 
                                 entry.msgid)
160
 
            # [('How do I update my system?', '!!T')]
161
 
            if len(matches) == 1 and len(matches[0]) == 2:
162
 
                entry.msgid = matches[0][0]
163
 
                entry.comment = matches[0][1]
164
 
            if matches[0][1] in entry.msgstr:
165
 
                entry.msgstr = entry.msgstr.replace(' %s' % matches[0][1], '')
166
 
        self.save()
167
 
 
168
 
    def readd_attr_list_statements(self):
169
 
        entries = []
170
 
        for entry_group in [self.pofile.translated_entries(),
171
 
                            self.pofile.fuzzy_entries(),
172
 
                            self.pofile.untranslated_entries()]:
173
 
            for entry in entry_group:
174
 
                for statement in HIDE_FROM_POT:
175
 
                    if statement in entry.comment:
176
 
                        entries += [entry]
177
 
        for entry in entries:
178
 
            if not entry.msgid.endswith(entry.comment):
179
 
                entry.msgid += ' %s' % entry.comment
180
 
            if entry.msgstr and not entry.msgstr.endswith(entry.comment):
181
 
                entry.msgstr += ' %s' % entry.comment
182
 
            entry.comment = ''
183
 
        self.save()
184
 
 
185
 
    def safeguard_meta_tags(self):
186
 
        for tag in META_TAGS:
187
 
            for entry in self.find_in_msgid(tag):
188
 
                if entry.msgid == tag:
189
 
                    entry.msgstr = entry.msgid
190
 
        self.save()
191
 
 
192
 
    def find_title_lines(self):
193
 
        results = []
194
 
        for entry in self.find_in_msgid('Title: '):
195
 
            if entry.msgid.startswith('Title: '):
196
 
                where = entry.occurrences[0][0]
197
 
                first_line = codecs.open(where,
198
 
                                         encoding='utf-8').readline().strip()
199
 
                results += [(entry, first_line)]
200
 
        return results
201
 
 
202
 
    def replace_title_lines(self):
203
 
        for entry, first_line in self.find_title_lines():
204
 
            if entry.msgid != first_line:
205
 
                print('Title line "%s" found, but not on the first line '
206
 
                      'of "%s".' % (entry.msgid, entry.linenum))
207
 
                return False
208
 
            entry.msgid = entry.msgid.replace('Title: ', '')
209
 
            if self.po_fn.endswith('.po'):
210
 
                entry.msgstr = ''
211
 
        self.save()
212
 
        return True
213
 
 
214
 
    def find_link_in_markdown_message(self, entry):
215
 
        link_regex = r'\[.+?\]\(\{filename\}(.+?)\).*?'
216
 
        link_msgid = re.findall(link_regex, entry.msgid)[0]
217
 
        link_msgstr = list(re.findall(link_regex, entry.msgstr))
218
 
        return (link_msgid, link_msgstr)
219
 
 
220
 
    def rewrite_links(self, documents, bcp47):
221
 
        for entry in self.find_in_msgid('{filename}'):
222
 
            (link_msgid, link_msgstr) = \
223
 
                self.find_link_in_markdown_message(entry)
224
 
            if [doc for doc in documents.docs if doc.endswith(link_msgid)]:
225
 
                translated_doc_fn = os.path.basename(
226
 
                    documents.translated_doc_fn(link_msgid, bcp47))
227
 
                if not link_msgstr:
228
 
                    entry.msgstr = entry.msgid
229
 
                    link_msgstr = [link_msgid]
230
 
                entry.msgstr = entry.msgstr.replace(link_msgstr[0],
231
 
                                                    translated_doc_fn)
232
 
        self.save()
233
 
 
234
 
    def find_translated_title_line(self, original_title):
235
 
        for entry in self.find_in_msgid(original_title):
236
 
            if entry.msgid == original_title:
237
 
                if entry.msgstr:
238
 
                    return entry.msgstr
239
 
                return entry.msgid
240
 
 
241
 
 
242
 
class PO(object):
243
 
    def __init__(self, po4a):
244
 
        self.translations_dir = os.path.abspath(os.path.join(PATH, '../po'))
245
 
        self.fake_lang_code = 'en_US'
246
 
        self.fake_po_fn = os.path.join(self.translations_dir,
247
 
                                       '%s.po' % self.fake_lang_code)
248
 
        self.pot_fn = os.path.join(self.translations_dir, 'help.pot')
249
 
        self.pot_file_ob = POFile(self.pot_fn)
250
 
        self.po4a = po4a
251
 
        self.langs = {}
252
 
        for po_fn in glob.glob(self.translations_dir+'/*.po'):
253
 
            self.add_language(po_fn)
254
 
 
255
 
    def add_language(self, po_fn):
256
 
        gettext_code = os.path.basename(po_fn).split('.po')[0]
257
 
        self.langs[po_fn] = {
258
 
            'bcp47': find_bcp47_code(gettext_code),
259
 
            'gettext_code': gettext_code,
260
 
            'pofile': None,
261
 
        }
262
 
 
263
 
    def _remove_fake_po_file(self):
264
 
        if os.path.exists(self.fake_po_fn):
265
 
            os.remove(self.fake_po_fn)
266
 
 
267
 
    def __del__(self):
268
 
        self._remove_fake_po_file()
269
 
 
270
 
    def load_pofile(self, po_fn):
271
 
        if not self.langs[po_fn]['pofile']:
272
 
            self.langs[po_fn]['pofile'] = POFile(po_fn)
273
 
 
274
 
    def gettextize(self, documents):
275
 
        if not self.po4a.gettextize(documents.docs, self.pot_fn):
276
 
            return False
277
 
        self.pot_file_ob.reread()
278
 
        return True
279
 
 
280
 
    def generate_pot_file(self, documents):
281
 
        if not self.gettextize(documents):
282
 
            return False
283
 
        if not self.pot_file_ob.replace_title_lines():
284
 
            return False
285
 
        self.pot_file_ob.hide_attr_list_statements()
286
 
        for po_fn in self.langs:
287
 
            self.load_pofile(po_fn)
288
 
            self.langs[po_fn]['pofile'].merge(self.pot_file_ob.pofile)
289
 
            if not self.langs[po_fn]['pofile'].replace_title_lines():
290
 
                return False
291
 
            self.langs[po_fn]['pofile'].hide_attr_list_statements()
292
 
        return True
293
 
 
294
 
    # we generate a fake translation for en-US which is going to be
295
 
    # the default
296
 
    def generate_fake_pofile(self):
297
 
        self._remove_fake_po_file()
298
 
        shutil.copy(self.pot_fn, self.fake_po_fn)
299
 
        self.add_language(self.fake_po_fn)
300
 
 
301
 
    def find_translated_title_line(self, original_title, po_fn):
302
 
        return self.langs[po_fn]['pofile'].find_translated_title_line(
303
 
            original_title)
304
 
 
305
 
    def rewrite_links(self, documents):
306
 
        for po_fn in self.langs:
307
 
            self.load_pofile(po_fn)
308
 
            self.langs[po_fn]['pofile'].rewrite_links(
309
 
                documents, self.langs[po_fn]['bcp47'])
310
 
 
311
 
    def safeguard_meta_tags(self):
312
 
        for po_fn in self.langs:
313
 
            self.load_pofile(po_fn)
314
 
            self.langs[po_fn]['pofile'].safeguard_meta_tags()
315
 
 
316
 
 
317
 
class Documents(object):
318
 
    def __init__(self):
319
 
        self.docs = [fn for fn in self.find_docs()
320
 
                     if verify_markdown_file(fn)]
321
 
 
322
 
    def find_docs(self):
323
 
        docs = []
324
 
        for dirpath, dirnames, fns in os.walk(PATH):
325
 
            docs += [os.path.relpath(os.path.join(dirpath, fn),
326
 
                                     os.path.join(PATH, '..'))
327
 
                     for fn in fns
328
 
                     if fn.endswith('.md')]
329
 
        return docs
330
 
 
331
 
    def translated_doc_fn(self, fn, bcp47_code):
332
 
        match = [doc for doc in self.docs
333
 
                 if os.path.basename(doc) == os.path.basename(fn)]
334
 
        if not match:
335
 
            return None
336
 
        return '%s.%s.md' % (match[0].split('.md')[0],
337
 
                             bcp47_code)
338
 
 
339
 
    def _call_po4a_translate(self, doc, po_fn, po4a):
340
 
        res = po4a.translate(doc, po_fn)
341
 
        return codecs.decode(res.communicate()[0])
342
 
 
343
 
    def write_translated_markdown(self, po, po4a):
344
 
        for po_fn in po.langs:
345
 
            po.langs[po_fn]['pofile'].readd_attr_list_statements()
346
 
            for doc_fn in self.docs:
347
 
                output = self._call_po4a_translate(doc_fn, po_fn, po4a)
348
 
                title_line = output.split('\n')[0].split('Title: ')[1]
349
 
                translated_title_line = po.find_translated_title_line(
350
 
                    title_line, po_fn)
351
 
                output = '\n'.join([line for line in output.split('\n')][1:])
352
 
                new_path = self.translated_doc_fn(doc_fn,
353
 
                                                  po.langs[po_fn]['bcp47'])
354
 
                text = "Title: %s\nDate:\n\n" % (translated_title_line)
355
 
                text += output
356
 
                if os.path.exists(new_path):
357
 
                    os.remove(new_path)
358
 
                if not os.path.exists(os.path.dirname(new_path)):
359
 
                    os.makedirs(os.path.dirname(new_path))
360
 
                with open(new_path, 'w', encoding='utf-8') as f:
361
 
                    f.write(text)
362
 
            po.langs[po_fn]['pofile'].hide_attr_list_statements()
363
 
 
364
 
 
365
 
class Translations(object):
366
 
    def __init__(self):
367
 
        self._cleanup()
368
 
        self.documents = Documents()
369
 
        self.po4a = PO4A()
370
 
        self.po = PO(self.po4a)
371
 
 
372
 
    def _cleanup(self):
373
 
        r = subprocess.Popen(['bzr', 'ignored'], stdout=subprocess.PIPE)
374
 
        fns = [os.path.join(PATH, '../..', f.split(' ')[0])
375
 
               for f in codecs.decode(r.communicate()[0]).split('\n')
376
 
               if f.strip() != '']
377
 
        fns = [f for f in fns if os.path.exists(f)]
378
 
        for f in fns:
379
 
            try:
380
 
                shutil.rmtree(f)
381
 
            except NotADirectoryError:
382
 
                os.remove(f)
383
 
 
384
 
    def generate_pot_file(self):
385
 
        return self.po.generate_pot_file(self.documents)
386
 
 
387
 
    def generate_translations(self):
388
 
        self.po.generate_fake_pofile()
389
 
        self.po.rewrite_links(self.documents)
390
 
        self.po.safeguard_meta_tags()
391
 
        self.documents.write_translated_markdown(self.po, self.po4a)