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

« back to all changes in this revision

Viewing changes to internals/translations/build.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
import codecs
 
2
import glob
 
3
import os
 
4
import re
 
5
import shutil
 
6
import subprocess
 
7
 
 
8
from translations.utils import (
 
9
    find_bcp47_code,
 
10
    full_path,
 
11
    normalise_path,
 
12
    require,
 
13
    use_top_level_dir,
 
14
    verify_markdown_file,
 
15
)
 
16
 
 
17
from translations.po4a import PO4A
 
18
 
 
19
try:
 
20
    import polib
 
21
except ImportError:
 
22
    require('python3-polib')
 
23
 
 
24
from pelicanconf import (
 
25
    HIDE_FROM_POT,
 
26
    META_TAGS,
 
27
    PATH,
 
28
    TRANSLATIONS_DIR,
 
29
)
 
30
 
 
31
 
 
32
class POFile(object):
 
33
    pofile = None
 
34
 
 
35
    def __init__(self, po_fn):
 
36
        self.po_fn = po_fn
 
37
        self.load()
 
38
 
 
39
    def load(self):
 
40
        self.pofile = polib.pofile(full_path(self.po_fn))
 
41
 
 
42
    def merge(self, pot_file_ob):
 
43
        self.pofile.merge(pot_file_ob)
 
44
 
 
45
    def save(self):
 
46
        self.pofile.save(full_path(self.po_fn))
 
47
 
 
48
    def find_in_msgid(self, find_str, translated=True, fuzzy=True,
 
49
                      untranslated=True):
 
50
        entries = []
 
51
        if translated:
 
52
            entries += self.pofile.translated_entries()
 
53
        if fuzzy:
 
54
            entries += self.pofile.fuzzy_entries()
 
55
        if untranslated:
 
56
            entries += self.pofile.untranslated_entries()
 
57
        results = []
 
58
        for entry in entries:
 
59
            if find_str in entry.msgid:
 
60
                results += [entry]
 
61
        return results
 
62
 
 
63
    def hide_attr_list_statements(self):
 
64
        entries = []
 
65
        for statement in HIDE_FROM_POT:
 
66
            entries.extend(self.find_in_msgid(statement))
 
67
        statements = r'|'.join(HIDE_FROM_POT)
 
68
        for entry in entries:
 
69
            matches = re.findall(r'(.*?)\s*?(%s)\s*?' % statements,
 
70
                                 entry.msgid)
 
71
            # [('How do I update my system?', '!!T')]
 
72
            if len(matches) == 1 and len(matches[0]) == 2:
 
73
                entry.msgid = matches[0][0]
 
74
                entry.comment = matches[0][1]
 
75
            if matches[0][1] in entry.msgstr:
 
76
                entry.msgstr = entry.msgstr.replace(' %s' % matches[0][1], '')
 
77
        self.save()
 
78
 
 
79
    def readd_attr_list_statements(self):
 
80
        entries = []
 
81
        for entry_group in [self.pofile.translated_entries(),
 
82
                            self.pofile.fuzzy_entries(),
 
83
                            self.pofile.untranslated_entries()]:
 
84
            for entry in entry_group:
 
85
                for statement in HIDE_FROM_POT:
 
86
                    if statement in entry.comment:
 
87
                        entries += [entry]
 
88
        for entry in entries:
 
89
            if not entry.msgid.endswith(entry.comment):
 
90
                entry.msgid += ' %s' % entry.comment
 
91
            if entry.msgstr and not entry.msgstr.endswith(entry.comment):
 
92
                entry.msgstr += ' %s' % entry.comment
 
93
            entry.comment = ''
 
94
        self.save()
 
95
 
 
96
    def safeguard_meta_tags(self):
 
97
        for tag in META_TAGS:
 
98
            for entry in self.find_in_msgid(tag):
 
99
                if entry.msgid == tag:
 
100
                    entry.msgstr = entry.msgid
 
101
        self.save()
 
102
 
 
103
    def find_title_lines(self):
 
104
        results = []
 
105
        for entry in self.find_in_msgid('Title: '):
 
106
            if entry.msgid.startswith('Title: '):
 
107
                where = entry.occurrences[0][0]
 
108
                first_line = codecs.open(full_path(where),
 
109
                                         encoding='utf-8').readline().strip()
 
110
                results += [(entry, first_line)]
 
111
        return results
 
112
 
 
113
    def replace_title_lines(self):
 
114
        for entry, first_line in self.find_title_lines():
 
115
            if entry.msgid != first_line:
 
116
                print('Title line "%s" found, but not on the first line '
 
117
                      'of "%s".' % (entry.msgid, entry.linenum))
 
118
                return False
 
119
            entry.msgid = entry.msgid.replace('Title: ', '')
 
120
            if self.po_fn.endswith('.po'):
 
121
                entry.msgstr = ''
 
122
        self.save()
 
123
        return True
 
124
 
 
125
    def find_link_in_markdown_message(self, entry):
 
126
        link_regex = r'\[.+?\]\(\{filename\}(.+?)\).*?'
 
127
        link_msgid = re.findall(link_regex, entry.msgid)[0]
 
128
        link_msgstr = list(re.findall(link_regex, entry.msgstr))
 
129
        return (link_msgid, link_msgstr)
 
130
 
 
131
    def rewrite_links(self, documents, bcp47):
 
132
        for entry in self.find_in_msgid('{filename}'):
 
133
            (link_msgid, link_msgstr) = \
 
134
                self.find_link_in_markdown_message(entry)
 
135
            if [doc for doc in documents.docs if doc.endswith(link_msgid)]:
 
136
                translated_doc_fn = os.path.basename(
 
137
                    documents.translated_doc_fn(link_msgid, bcp47))
 
138
                if not link_msgstr:
 
139
                    entry.msgstr = entry.msgid
 
140
                    link_msgstr = [link_msgid]
 
141
                entry.msgstr = entry.msgstr.replace(link_msgstr[0],
 
142
                                                    translated_doc_fn)
 
143
        self.save()
 
144
 
 
145
    def find_translated_title_line(self, original_title):
 
146
        for entry in self.find_in_msgid(original_title):
 
147
            if entry.msgid == original_title:
 
148
                if entry.msgstr:
 
149
                    return entry.msgstr
 
150
                return entry.msgid
 
151
 
 
152
 
 
153
class PO(object):
 
154
    def __init__(self, po4a):
 
155
        self.fake_lang_code = 'en_US'
 
156
        self.fake_po_fn = normalise_path(
 
157
            os.path.join(TRANSLATIONS_DIR, 
 
158
                         '%s.po' % self.fake_lang_code))
 
159
        self.pot_fn = normalise_path(os.path.join(TRANSLATIONS_DIR,
 
160
                                                  'help.pot'))
 
161
        self.pot_file_ob = POFile(self.pot_fn)
 
162
        self.po4a = po4a
 
163
        self.langs = {}
 
164
        for po_fn in glob.glob(TRANSLATIONS_DIR+'/*.po'):
 
165
            self.add_language(normalise_path(po_fn))
 
166
 
 
167
    def add_language(self, po_fn):
 
168
        gettext_code = os.path.basename(po_fn).split('.po')[0]
 
169
        self.langs[po_fn] = {
 
170
            'bcp47': find_bcp47_code(gettext_code),
 
171
            'gettext_code': gettext_code,
 
172
            'pofile': None,
 
173
        }
 
174
 
 
175
    def _remove_fake_po_file(self):
 
176
        if os.path.exists(self.fake_po_fn):
 
177
            os.remove(self.fake_po_fn)
 
178
 
 
179
    def __del__(self):
 
180
        self._remove_fake_po_file()
 
181
 
 
182
    def load_pofile(self, po_fn):
 
183
        if not self.langs[po_fn]['pofile']:
 
184
            self.langs[po_fn]['pofile'] = POFile(po_fn)
 
185
 
 
186
    def gettextize(self, documents):
 
187
        if not self.po4a.gettextize(documents.docs, self.pot_fn):
 
188
            return False
 
189
        self.pot_file_ob.load()
 
190
        return True
 
191
 
 
192
    def generate_pot_file(self, documents):
 
193
        if not self.gettextize(documents):
 
194
            return False
 
195
        if not self.pot_file_ob.replace_title_lines():
 
196
            return False
 
197
        self.pot_file_ob.hide_attr_list_statements()
 
198
        for po_fn in self.langs:
 
199
            self.load_pofile(po_fn)
 
200
            self.langs[po_fn]['pofile'].merge(self.pot_file_ob.pofile)
 
201
            if not self.langs[po_fn]['pofile'].replace_title_lines():
 
202
                return False
 
203
            self.langs[po_fn]['pofile'].hide_attr_list_statements()
 
204
        return True
 
205
 
 
206
    # we generate a fake translation for en-US which is going to be
 
207
    # the default
 
208
    def generate_fake_pofile(self):
 
209
        pwd = use_top_level_dir()
 
210
        self._remove_fake_po_file()
 
211
        shutil.copy(self.pot_fn, self.fake_po_fn)
 
212
        os.chdir(pwd)
 
213
        self.add_language(self.fake_po_fn)
 
214
 
 
215
    def find_translated_title_line(self, original_title, po_fn):
 
216
        return self.langs[po_fn]['pofile'].find_translated_title_line(
 
217
            original_title)
 
218
 
 
219
    def rewrite_links(self, documents):
 
220
        for po_fn in self.langs:
 
221
            self.load_pofile(po_fn)
 
222
            self.langs[po_fn]['pofile'].rewrite_links(
 
223
                documents, self.langs[po_fn]['bcp47'])
 
224
 
 
225
    def safeguard_meta_tags(self):
 
226
        for po_fn in self.langs:
 
227
            self.load_pofile(po_fn)
 
228
            self.langs[po_fn]['pofile'].safeguard_meta_tags()
 
229
 
 
230
 
 
231
class Documents(object):
 
232
    def __init__(self):
 
233
        self.docs = [fn for fn in self.find_docs()
 
234
                     if verify_markdown_file(fn)]
 
235
 
 
236
    def find_docs(self):
 
237
        docs = []
 
238
        for dirpath, dirnames, fns in os.walk(PATH):
 
239
            docs += [normalise_path(os.path.join(dirpath, fn))
 
240
                     for fn in fns
 
241
                     if fn.endswith('.md')]
 
242
        return docs
 
243
 
 
244
    def translated_doc_fn(self, fn, bcp47_code):
 
245
        match = [doc for doc in self.docs
 
246
                 if os.path.basename(doc) == os.path.basename(fn)]
 
247
        if not match:
 
248
            return None
 
249
        return '%s.%s.md' % (match[0].split('.md')[0],
 
250
                             bcp47_code)
 
251
 
 
252
    def _call_po4a_translate(self, doc, po_fn, po4a):
 
253
        res = po4a.translate(doc, po_fn)
 
254
        return codecs.decode(res.communicate()[0])
 
255
 
 
256
    def write_translated_markdown(self, po, po4a):
 
257
        for po_fn in po.langs:
 
258
            po.langs[po_fn]['pofile'].readd_attr_list_statements()
 
259
            for doc_fn in self.docs:
 
260
                output = self._call_po4a_translate(doc_fn, po_fn, po4a)
 
261
                title_line = output.split('\n')[0].split('Title: ')[1]
 
262
                translated_title_line = po.find_translated_title_line(
 
263
                    title_line, po_fn)
 
264
                output = '\n'.join([line for line in output.split('\n')][1:])
 
265
                new_path = full_path(self.translated_doc_fn(
 
266
                    doc_fn, po.langs[po_fn]['bcp47']))
 
267
                text = "Title: %s\nDate:\n\n" % (translated_title_line)
 
268
                text += output
 
269
                if os.path.exists(new_path):
 
270
                    os.remove(new_path)
 
271
                if not os.path.exists(os.path.dirname(new_path)):
 
272
                    os.makedirs(os.path.dirname(new_path))
 
273
                with open(new_path, 'w', encoding='utf-8') as f:
 
274
                    f.write(text)
 
275
            po.langs[po_fn]['pofile'].hide_attr_list_statements()
 
276
 
 
277
 
 
278
class Translations(object):
 
279
    def __init__(self):
 
280
        self._cleanup()
 
281
        self.documents = Documents()
 
282
        self.po4a = PO4A()
 
283
        self.po = PO(self.po4a)
 
284
 
 
285
    def _cleanup(self):
 
286
        r = subprocess.Popen(['bzr', 'ignored'], stdout=subprocess.PIPE)
 
287
        fns = [full_path(f.split(' ')[0])
 
288
               for f in codecs.decode(r.communicate()[0]).split('\n')
 
289
               if f.strip() != '']
 
290
        fns = [f for f in fns if os.path.exists(f)]
 
291
        for f in fns:
 
292
            try:
 
293
                shutil.rmtree(f)
 
294
            except NotADirectoryError:
 
295
                os.remove(f)
 
296
 
 
297
    def generate_pot_file(self):
 
298
        return self.po.generate_pot_file(self.documents)
 
299
 
 
300
    def generate_translations(self):
 
301
        self.po.generate_fake_pofile()
 
302
        self.po.rewrite_links(self.documents)
 
303
        self.po.safeguard_meta_tags()
 
304
        self.documents.write_translated_markdown(self.po, self.po4a)