Merge lp:~chromium-team/chromium-browser/chromium-translations-tools.head into lp:chromium-browser
- chromium-translations-tools.head
- Merge into chromium-browser.head
Status: | Rejected |
---|---|
Rejected by: | Nathan Teodosio |
Proposed branch: | lp:~chromium-team/chromium-browser/chromium-translations-tools.head |
Merge into: | lp:chromium-browser |
Diff against target: |
3433 lines (+3409/-0) 5 files modified
chromium2pot.py (+2610/-0) create-patches.sh (+185/-0) desktop2gettext.py (+378/-0) update-inspector.py (+149/-0) update-pot.sh (+87/-0) |
To merge this branch: | bzr merge lp:~chromium-team/chromium-browser/chromium-translations-tools.head |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Chromium team | Pending | ||
Review via email: mp+461443@code.launchpad.net |
Commit message
Description of the change
Unmerged revisions
- 121. By Chad Miller
-
Handle GRD partial files.
Ignore "external" references, which are usually images.
- 120. By Ken VanDine
-
handled latest grd format
- 119. By Micah Gersten
-
* Temporarily workaround muliple bg locales in generated_resources
- 118. By Cris Dywan
-
* Add temporary workaround for new type not being used yet
- 117. By Fabien Tassin
-
* Add some helper scripts
- 116. By Fabien Tassin
-
* When updating common.gypi, fold 'locales' by size (instead of by groups of 10)
- 115. By Fabien Tassin
-
* Fix a regression introduced by the new fake-bidi pseudo locale
(see https://sites. google. com/a/chromium. org/dev/ Home/fake- bidi and
http://code.google. com/p/chromium/ issues/ detail? id=73052) - 114. By Fabien Tassin
-
* Add a --map-template-
names knob allowing to handle renamed templates
in some branches - 113. By Fabien Tassin
-
* Add support for 'string-enum' and 'int-enum' outside of 'group' policies
(needed since http://codereview. chromium. org/7287001/ landed) - 112. By Fabien Tassin
-
* Move all new xtb files to third_party/
launchpad_ translations (relative to $SRC)
Preview Diff
1 | === added file 'chromium2pot.py' |
2 | --- chromium2pot.py 1970-01-01 00:00:00 +0000 |
3 | +++ chromium2pot.py 2024-02-28 12:58:47 +0000 |
4 | @@ -0,0 +1,2610 @@ |
5 | +#!/usr/bin/python |
6 | +# -*- coding: utf-8 -*- |
7 | + |
8 | +# (c) 2010-2011, Fabien Tassin <fta@ubuntu.com> |
9 | + |
10 | +# Convert grd/xtb files into pot/po for integration into the Launchpad |
11 | +# translation system |
12 | + |
13 | +## grd files contain the strings for the 'pot' file(s). |
14 | +## Keys are alphabetical (IDS_XXX). |
15 | +# Sources: |
16 | +# - $SRC/chrome/app/*.grd |
17 | +# - $SRC/webkit/glue/*.grd |
18 | + |
19 | +## xtb files are referenced to by the grd files. They contain the translated |
20 | +## strings for the 'po' our files. Keys are numerical (64bit ids). |
21 | +# Sources: |
22 | +# - $SRC/chrome/app/resources/*.xtb |
23 | +# - $SRC/webkit/glue/resources/*.xtb |
24 | +# and for launchpad contributed strings that already landed: |
25 | +# - $SRC/third_party/launchpad_translations/*.xtb |
26 | + |
27 | +## the mapping between those keys is done using FingerPrint() |
28 | +## [ taken from grit ] on a stripped version of the untranslated string |
29 | + |
30 | +## grd files contain a lot of <if expr="..."> (python-like) conditions. |
31 | +## Evaluate those expressions but only skip strings with a lang restriction. |
32 | +## For all other conditions (os, defines), simply expose them so translators |
33 | +## know when a given string is expected. |
34 | + |
35 | +## TODO: handle <message translateable="false"> |
36 | + |
37 | +import os, sys, shutil, re, getopt, codecs, urllib |
38 | +from xml.dom import minidom |
39 | +from xml.sax.saxutils import unescape |
40 | +from datetime import datetime |
41 | +from difflib import unified_diff |
42 | +import textwrap, filecmp, json |
43 | + |
44 | +lang_mapping = { |
45 | + 'no': 'nb', # 'no' is obsolete and the more specific 'nb' (Norwegian Bokmal) |
46 | + # and 'nn' (Norwegian Nynorsk) are preferred. |
47 | + 'pt-PT': 'pt' |
48 | +} |
49 | + |
50 | +#### |
51 | +# vanilla from $SRC/tools/grit/grit/extern/FP.py (r10982) |
52 | +# See svn log http://src.chromium.org/svn/trunk/src/tools/grit/grit/extern/FP.py |
53 | + |
54 | +# Copyright (c) 2006-2008 The Chromium Authors. All rights reserved. |
55 | +# Use of this source code is governed by a BSD-style license that can be |
56 | +# found in the LICENSE file. |
57 | + |
58 | +try: |
59 | + import hashlib |
60 | + _new_md5 = hashlib.md5 |
61 | +except ImportError: |
62 | + import md5 |
63 | + _new_md5 = md5.new |
64 | + |
65 | +def UnsignedFingerPrint(str, encoding='utf-8'): |
66 | + """Generate a 64-bit fingerprint by taking the first half of the md5 |
67 | + of the string.""" |
68 | + hex128 = _new_md5(str).hexdigest() |
69 | + int64 = long(hex128[:16], 16) |
70 | + return int64 |
71 | + |
72 | +def FingerPrint(str, encoding='utf-8'): |
73 | + fp = UnsignedFingerPrint(str, encoding=encoding) |
74 | + # interpret fingerprint as signed longs |
75 | + if fp & 0x8000000000000000L: |
76 | + fp = - ((~fp & 0xFFFFFFFFFFFFFFFFL) + 1) |
77 | + return fp |
78 | +#### |
79 | + |
80 | +class EvalConditions: |
81 | + """ A class allowing an <if expr="xx"/> to be evaluated, based on an array of defines, |
82 | + a dict of local variables. |
83 | + As of Chromium 10: |
84 | + - the known defines are: |
85 | + [ 'chromeos', '_google_chrome', 'toolkit_views', 'touchui', 'use_titlecase' ] |
86 | + On Linux, only [ 'use_titlecase' ] is set. |
87 | + - the known variables are: |
88 | + 'os' ('linux2' on Linux) |
89 | + 'lang' |
90 | + See http://src.chromium.org/svn/trunk/src/build/common.gypi |
91 | + """ |
92 | + |
93 | + def eval(self, expression, defines = [ 'use_titlecase' ], vars = { 'os': "linux2" }): |
94 | + |
95 | + def pp_ifdef(match): |
96 | + return str(match.group(1) in defines) |
97 | + |
98 | + # evaluate all ifdefs |
99 | + expression = re.sub(r"pp_ifdef\('(.*?)'\)", pp_ifdef, expression) |
100 | + # evaluate the whole expression using the vars dict |
101 | + vars['__builtins__'] = { 'True': True, 'False': False } # prevent eval from using the real current globals |
102 | + return eval(expression, vars) |
103 | + |
104 | + def lang_eval(self, expression, lang): |
105 | + """ only evaluate the expression against the lang, ignore all defines and other variables. |
106 | + This is needed to ignore a string that has lang restrictions (numerals, plurals, ..) but |
107 | + still keep it even if it's OS or defined don't match the local platform. |
108 | + """ |
109 | + conditions = [ x for x in re.split(r'\s+(and|or)\s+', expression) if x.find('lang') >= 0 ] |
110 | + if len(conditions) == 0: |
111 | + return True |
112 | + assert len(conditions) == 1, "Expression '%s' has multiple lang conditions" % expression |
113 | + vars = { 'lang': lang, '__builtins__': { 'True': True, 'False': False } } |
114 | + return eval(conditions[0], vars) |
115 | + |
116 | + def test(self): |
117 | + data = [ |
118 | + { 'expr': "lang == 'ar'", |
119 | + 'vars': { 'lang': 'ar' }, |
120 | + 'result': True |
121 | + }, |
122 | + { 'expr': "lang == 'ar'", |
123 | + 'vars': { 'lang': 'fr' }, |
124 | + 'result': False |
125 | + }, |
126 | + { 'expr': "lang in ['ar', 'ro', 'lv']", |
127 | + 'vars': { 'lang': 'ar' }, |
128 | + 'result': True |
129 | + }, |
130 | + { 'expr': "lang in ['ar', 'ro', 'lv']", |
131 | + 'vars': { 'lang': 'pt-BR' }, |
132 | + 'result': False |
133 | + }, |
134 | + { 'expr': "lang not in ['ar', 'ro', 'lv']", |
135 | + 'vars': { 'lang': 'ar' }, |
136 | + 'result': False |
137 | + }, |
138 | + { 'expr': "lang not in ['ar', 'ro', 'lv']", |
139 | + 'vars': { 'lang': 'no' }, |
140 | + 'result': True |
141 | + }, |
142 | + { 'expr': "os != 'linux2' and os != 'darwin' and os.find('bsd') == -1", |
143 | + 'vars': { 'lang': 'no', 'os': 'bsdos' }, |
144 | + 'result': False, |
145 | + 'lresult': True # no lang restriction in 'expr', so 'no' is ok |
146 | + }, |
147 | + { 'expr': "os != 'linux2' and os != 'darwin' and os.find('bsd') > -1", |
148 | + 'vars': { 'lang': 'no', 'os': 'bsdos' }, |
149 | + 'result': True, |
150 | + }, |
151 | + { 'expr': "not pp_ifdef('chromeos')", |
152 | + 'vars': { 'lang': 'no' }, |
153 | + 'defines': [], |
154 | + 'result': True, |
155 | + }, |
156 | + { 'expr': "not pp_ifdef('chromeos')", |
157 | + 'vars': { 'lang': 'no' }, |
158 | + 'defines': [ 'chromeos' ], |
159 | + 'result': False, |
160 | + 'lresult': True # no lang restriction in 'expr', so 'no' is ok |
161 | + }, |
162 | + { 'expr': "pp_ifdef('_google_chrome') and (os == 'darwin')", |
163 | + 'vars': { 'lang': 'no', 'os': 'linux2' }, |
164 | + 'defines': [ 'chromeos' ], |
165 | + 'result': False, |
166 | + 'lresult': True # no lang restriction in 'expr', so 'no' is ok |
167 | + }, |
168 | + { 'expr': "pp_ifdef('_google_chrome') and (os == 'darwin')", |
169 | + 'vars': { 'lang': 'no', 'os': 'darwin' }, |
170 | + 'defines': [ '_google_chrome' ], |
171 | + 'result': True |
172 | + }, |
173 | + { 'expr': "not pp_ifdef('chromeos') and pp_ifdef('_google_chrome') and 'pt-PT' == lang", |
174 | + 'vars': { 'lang': 'pt-PT', 'os': 'darwin' }, |
175 | + 'defines': [ '_google_chrome' ], |
176 | + 'result': True |
177 | + }, |
178 | + { 'expr': "not pp_ifdef('chromeos') and pp_ifdef('_google_chrome') and 'pt-PT' == lang", |
179 | + 'vars': { 'lang': 'pt-PT', 'os': 'darwin' }, |
180 | + 'defines': [ ], |
181 | + 'result': False, |
182 | + 'lresult': True |
183 | + }, |
184 | + ] |
185 | + i = -1 |
186 | + for d in data: |
187 | + i += 1 |
188 | + defines = d['defines'] if 'defines' in d else [] |
189 | + vars = d['vars'] if 'vars' in d else {} |
190 | + lvars = vars.copy() # make a copy because eval modifies it |
191 | + res = self.eval(d['expr'], defines = defines, vars = lvars) |
192 | + assert res == d['result'], "FAILED %d: expr: \"%s\" returned %s with vars = %s and defines = %s" % \ |
193 | + (i, d['expr'], repr(res), repr(vars), repr(defines)) |
194 | + print "All %d tests passed for EvalConditions.eval()" % (i + 1) |
195 | + i = -1 |
196 | + for d in data: |
197 | + i += 1 |
198 | + assert 'lang' in vars, "All test must have a 'lang' in 'vars', test %d doesn't: %s" % (i, repr(d)) |
199 | + res = self.lang_eval(d['expr'], lang = d['vars']['lang']) |
200 | + expected = d['lresult'] if 'lresult' in d else d['result'] |
201 | + assert res == expected, "FAILED %d: expr: \"%s\" returned %s with lang = %s for the lang_eval test" % \ |
202 | + (i, d['expr'], repr(res), d['vars']['lang']) |
203 | + print "All %d tests passed for EvalConditions.lang_eval()" % (i + 1) |
204 | + |
205 | +class StringCvt: |
206 | + """ A class converting grit formatted strings to gettext back and forth. |
207 | + The idea is to always have: |
208 | + a/ grd2gettext(xtb2gettext(s)) == s |
209 | + b/ xtb2gettext(s) produces a string that the msgfmt checker likes and |
210 | + that makes sense to translators |
211 | + c/ grd2gettext(s) produces a string acceptable by upstream |
212 | + """ |
213 | + |
214 | + def xtb2gettext(self, string): |
215 | + """ parse the xtb (xml encoded) string and convert it to a gettext string """ |
216 | + |
217 | + def fold(string): |
218 | + return textwrap.wrap(string, break_long_words=False, width=76, drop_whitespace=False, |
219 | + expand_tabs=False, replace_whitespace=False, break_on_hyphens=False) |
220 | + |
221 | + s = string.replace('\\n', '\\\\n') |
222 | + # escape all single '\' (not followed by 'n') |
223 | + s = re.sub(r'(?<!\\)(\\[^n\\\\])', r'\\\1', s) |
224 | + # remove all xml encodings |
225 | + s = self.unescape_xml(s) |
226 | + # replace '<ph name="FOO"/>' by '%{FOO}' |
227 | + s = re.sub(r'<ph name="(.*?)"/>', r'%{\1}', s) |
228 | + # fold |
229 | + # 1/ fold at \n |
230 | + # 2/ fold each part at ~76 char |
231 | + v = [] |
232 | + ll = s.split('\n') |
233 | + sz = len(ll) |
234 | + if sz > 1: |
235 | + i = 0 |
236 | + for l in ll: |
237 | + i += 1 |
238 | + if i == sz: |
239 | + v.extend(fold(l)) |
240 | + else: |
241 | + v.extend(fold(l + '\\n')) |
242 | + else: |
243 | + v.extend(fold(ll[0])) |
244 | + if len(v) > 1: |
245 | + v[:0] = [ '' ] |
246 | + s = '"' + '"\n"'.join(v) + '"' |
247 | + return s |
248 | + |
249 | + def decode_xml_entities(self, string): |
250 | + def replace_xmlent(match): |
251 | + if match.group(1)[:1] == 'x': |
252 | + return unichr(int("0" + match.group(1), 16)) |
253 | + else: |
254 | + return unichr(int(match.group(1))) |
255 | + |
256 | + return re.sub(r'&#(x\w+|\d+);', replace_xmlent, string) |
257 | + |
258 | + def unescape_xml(self, string): |
259 | + string = unescape(string).replace('"', '\\"').replace(''', "'") |
260 | + string = self.decode_xml_entities(string) |
261 | + return string |
262 | + |
263 | + def grd2gettext(self, string): |
264 | + """ parse the string returned from minidom and convert it to a gettext string. |
265 | + This is similar to str_cvt_xtb2gettext but minidom has its own magic for encoding |
266 | + """ |
267 | + return self.xtb2gettext(string) |
268 | + |
269 | + def gettext2xtb(self, string): |
270 | + """ parse the gettext string and convert it to an xtb (xml encoded) string. """ |
271 | + u = [] |
272 | + for s in string.split(u'\n'): |
273 | + # remove the enclosing double quotes |
274 | + u.append(s[1:][:-1]) |
275 | + s = u"".join(u) |
276 | + |
277 | + # encode the xml special chars |
278 | + s = s.replace("&", "&") # must be first! |
279 | + s = s.replace("<", "<") |
280 | + s = s.replace(">", ">") |
281 | + s = s.replace('\\"', """) |
282 | + # special case, html comments |
283 | + s = re.sub(r'<!--(.*?)-->', r'<!--\1-->', s, re.S) |
284 | + # replace non-ascii by &#xxx; codes |
285 | + # s = s.encode("ascii", "xmlcharrefreplace") |
286 | + # replace '%{FOO}' by '<ph name="FOO"/>' |
287 | + s = re.sub(r'%{(.*?)}', r'<ph name="\1"/>', s) |
288 | + # unquote \\n and \\\\n |
289 | + s = re.sub(r'(?<!\\)\\n', r'\n', s) |
290 | + # unquote all control chars |
291 | + s = re.sub(r'\\\\([^\\])', r'\\\1', s) |
292 | + |
293 | + # launchpad seems to always quote tabs |
294 | + s = s.replace("\\t", "\t") |
295 | + return s |
296 | + |
297 | + def test(self): |
298 | + # unit tests |
299 | + data = [ |
300 | + # tab |
301 | + { 'id': '0', |
302 | + 'xtb': u'foo bar', |
303 | + 'po': u'"foo bar"' }, |
304 | + { 'id': '1', |
305 | + 'xtb': u'foo\tbar', |
306 | + 'po': u'"foo\tbar"' }, |
307 | + # & |
308 | + { 'id': '6779164083355903755', |
309 | + 'xtb': u'Supprime&r', |
310 | + 'po': u'"Supprime&r"' }, |
311 | + # " |
312 | + { 'id': '4194570336751258953', |
313 | + 'xtb': u'Activer la fonction "taper pour cliquer"', |
314 | + 'po': u'"Activer la fonction \\"taper pour cliquer\\""' }, |
315 | + # < / > |
316 | + { 'id': '7615851733760445951', |
317 | + 'xtb': u'<aucun cookie sélectionné>', |
318 | + 'po': u'"<aucun cookie sélectionné>"' }, |
319 | + # <ph name="FOO"/> |
320 | + { 'id': '5070288309321689174', |
321 | + 'xtb': u'<ph name="EXTENSION_NAME"/> :', |
322 | + 'po': u'"%{EXTENSION_NAME} :"' }, |
323 | + { 'id': '1467071896935429871', |
324 | + 'xtb': u'Téléchargement de la mise à jour du système : <ph name="PERCENT"/>% terminé', |
325 | + 'po': u'"Téléchargement de la mise à jour du système : %{PERCENT}% terminé"' }, |
326 | + # line folding |
327 | + { 'id': '1526811905352917883', |
328 | + 'xtb': u'Une nouvelle tentative de connexion avec SSL 3.0 a dû être effectuée. Cette opération indique généralement que le serveur utilise un logiciel très ancien et qu\'il est susceptible de présenter d\'autres problèmes de sécurité.', |
329 | + 'po': u'""\n"Une nouvelle tentative de connexion avec SSL 3.0 a dû être effectuée. Cette "\n"opération indique généralement que le serveur utilise un logiciel très "\n"ancien et qu\'il est susceptible de présenter d\'autres problèmes de sécurité."' }, |
330 | + { 'id': '7999229196265990314', |
331 | + 'xtb': u'Les fichiers suivants ont été créés :\n\nExtension : <ph name="EXTENSION_FILE"/>\nFichier de clé : <ph name="KEY_FILE"/>\n\nConservez votre fichier de clé en lieu sûr. Vous en aurez besoin lors de la création de nouvelles versions de l\'extension.', |
332 | + 'po': u'""\n"Les fichiers suivants ont été créés :\\n"\n"\\n"\n"Extension : %{EXTENSION_FILE}\\n"\n"Fichier de clé : %{KEY_FILE}\\n"\n"\\n"\n"Conservez votre fichier de clé en lieu sûr. Vous en aurez besoin lors de la "\n"création de nouvelles versions de l\'extension."' }, |
333 | + # quoted LF |
334 | + { 'id': '4845656988780854088', |
335 | + 'xtb': u'Synchroniser uniquement les paramètres et\\ndonnées qui ont changé depuis la dernière connexion\\n(requiert votre mot de passe précédent)', |
336 | + 'po': u'""\n"Synchroniser uniquement les paramètres et\\\\ndonnées qui ont changé depuis la"\n" dernière connexion\\\\n(requiert votre mot de passe précédent)"' }, |
337 | + { 'id': '1761265592227862828', # lang: 'el' |
338 | + 'xtb': u'Συγχρονισμός όλων των ρυθμίσεων και των δεδομένων\\n (ενδέχεται να διαρκέσει ορισμένο χρονικό διάστημα)', |
339 | + 'po': u'""\n"Συγχρονισμός όλων των ρυθμίσεων και των δεδομένων\\\\n (ενδέχεται να διαρκέσει"\n" ορισμένο χρονικό διάστημα)"' }, |
340 | + { 'id': '1768211415369530011', # lang: 'de' |
341 | + 'xtb': u'Folgende Anwendung wird gestartet, wenn Sie diese Anforderung akzeptieren:\\n\\n <ph name="APPLICATION"/>', |
342 | + 'po': u'""\n"Folgende Anwendung wird gestartet, wenn Sie diese Anforderung "\n"akzeptieren:\\\\n\\\\n %{APPLICATION}"' }, |
343 | + # weird controls |
344 | + { 'id': '5107325588313356747', # lang: 'es-419' |
345 | + 'xtb': u'Para ocultar el acceso a este programa, debes desinstalarlo. Para ello, utiliza\\n<ph name="CONTROL_PANEL_APPLET_NAME"/> del Panel de control.\\n\¿Deseas iniciar <ph name="CONTROL_PANEL_APPLET_NAME"/>?', |
346 | + 'po': u'""\n"Para ocultar el acceso a este programa, debes desinstalarlo. Para ello, "\n"utiliza\\\\n%{CONTROL_PANEL_APPLET_NAME} del Panel de control.\\\\n\\\\¿Deseas "\n"iniciar %{CONTROL_PANEL_APPLET_NAME}?"' } |
347 | + ] |
348 | + |
349 | + for string in data: |
350 | + s = u"<x>" + string['xtb'] + u"</x>" |
351 | + s = s.encode('ascii', 'xmlcharrefreplace') |
352 | + dom = minidom.parseString(s) |
353 | + s = dom.firstChild.toxml()[3:][:-4] |
354 | + e = self.grd2gettext(s) |
355 | + if e != string['po']: |
356 | + assert False, "grd2gettext() failed for id " + string['id'] + \ |
357 | + ". \nExpected: " + repr(string['po']) + "\nGot: " + repr(e) |
358 | + e = self.xtb2gettext(string['xtb']) |
359 | + if e != string['po']: |
360 | + assert False, "xtb2gettext() failed for id " + string['id'] + \ |
361 | + ". \nExpected: " + repr(string['po']) + "\nGot: " + repr(e) |
362 | + u = self.gettext2xtb(e) |
363 | + if u != string['xtb']: |
364 | + assert False, "gettext2xtb() failed for id " + string['id'] + \ |
365 | + ". \nExpected: " + repr(string['xtb']) + "\nGot: " + repr(u) |
366 | + print string['id'] + " ok" |
367 | + |
368 | + # more tests with only po to xtb to test some weird launchpad po exports |
369 | + data2 = [ |
370 | + { 'id': '1768211415369530011', # lang: 'de' |
371 | + 'po': u'""\n"Folgende Anwendung wird gestartet, wenn Sie diese Anforderung akzeptieren:\\\\"\n"n\\\\n %{APPLICATION}"', |
372 | + 'xtb': u'Folgende Anwendung wird gestartet, wenn Sie diese Anforderung akzeptieren:\\n\\n <ph name="APPLICATION"/>' }, |
373 | + ] |
374 | + for string in data2: |
375 | + u = self.gettext2xtb(string['po']) |
376 | + if u != string['xtb']: |
377 | + assert False, "gettext2xtb() failed for id " + string['id'] + \ |
378 | + ". \nExpected: " + repr(string['xtb']) + "\nGot: " + repr(u) |
379 | + print string['id'] + " ok" |
380 | + |
381 | +###### |
382 | + |
383 | +class PotFile(dict): |
384 | + """ |
385 | + Read and write gettext pot files |
386 | + """ |
387 | + |
388 | + def __init__(self, filename, date = None, debug = False, branch_name = "default", branch_dir = os.getcwd()): |
389 | + self.debug = debug |
390 | + self.lang = None |
391 | + self.filename = filename |
392 | + self.tfile = filename + ".new" |
393 | + self.branch_dir = branch_dir |
394 | + self.branch_name = branch_name |
395 | + self.template_date = date |
396 | + self.translation_date = "YEAR-MO-DA HO:MI+ZONE" |
397 | + self.is_pot = True |
398 | + self.fd = None |
399 | + self.fd_mode = "rb" |
400 | + if self.template_date is None: |
401 | + self.template_date = datetime.utcnow().strftime("%Y-%m-%d %H:%M+0000") |
402 | + self.strings = [] |
403 | + |
404 | + def add_string(self, id, comment, string, translation = "", origin = None): |
405 | + self.strings.append({ 'id': id, 'comment': comment, 'string': string, |
406 | + 'origin': origin, 'translation': translation }) |
407 | + |
408 | + def replace_file_if_newer(self): |
409 | + filename = os.path.join(self.branch_dir, self.filename) if self.branch_dir is not None \ |
410 | + else self.filename |
411 | + tfile = os.path.join(self.branch_dir, self.tfile) if self.branch_dir is not None \ |
412 | + else self.tfile |
413 | + if os.path.isfile(filename) and filecmp.cmp(filename, tfile) == 1: |
414 | + os.unlink(tfile) |
415 | + return 0 |
416 | + else: |
417 | + os.rename(tfile, filename) |
418 | + return 1 |
419 | + |
420 | + def get_mtime(self, file): |
421 | + rfile = os.path.join(self.branch_dir, file) |
422 | + if self.debug: |
423 | + print "getmtime(%s) [%s]" % (file, os.path.abspath(rfile)) |
424 | + return os.path.getmtime(rfile) |
425 | + |
426 | + def open(self, mode = "rb", filename = None): |
427 | + if filename is not None: |
428 | + self.filename = filename |
429 | + self.tfile = filename + ".new" |
430 | + rfile = os.path.join(self.branch_dir, self.filename) |
431 | + rtfile = os.path.join(self.branch_dir, self.tfile) |
432 | + if self.fd is not None: |
433 | + self.close() |
434 | + self.fd_mode = mode |
435 | + if mode.find("r") != -1: |
436 | + if self.debug: |
437 | + print "open %s [mode=%s] from branch '%s' [%s]" % (self.filename, mode, self.branch_name, os.path.abspath(rfile)) |
438 | + self.fd = codecs.open(rfile, mode, encoding="utf-8") |
439 | + else: |
440 | + if self.debug: |
441 | + print "open %s [mode=%s] from branch '%s' [%s]" % (self.tfile, mode, self.branch_name, os.path.abspath(rtfile)) |
442 | + self.fd = codecs.open(rtfile, mode, encoding="utf-8") |
443 | + |
444 | + def close(self): |
445 | + self.fd.close() |
446 | + self.fd = None |
447 | + if self.fd_mode.find("w") != -1: |
448 | + return self.replace_file_if_newer() |
449 | + |
450 | + def read_string(self): |
451 | + string = {} |
452 | + cur = None |
453 | + while 1: |
454 | + s = self.fd.readline() |
455 | + if len(s) == 0 or s == "\n": |
456 | + break # EOF or end of block |
457 | + if s.rfind('\n') == len(s) - 1: |
458 | + s = s[:-1] # chomp |
459 | + if s.find("# ") == 0 or s == "#": # translator-comment |
460 | + if 'comment' not in string: |
461 | + string['comment'] = '' |
462 | + string['comment'] += s[2:] |
463 | + continue |
464 | + if s.find("#:") == 0: # reference |
465 | + if 'reference' not in string: |
466 | + string['reference'] = '' |
467 | + string['reference'] += s[2:] |
468 | + if s[2:].find(" id: ") == 0: |
469 | + string['id'] = s[7:].split(' ')[0] |
470 | + continue |
471 | + if s.find("#.") == 0: # extracted-comments |
472 | + if 'extracted' not in string: |
473 | + string['extracted'] = '' |
474 | + string['extracted'] += s[2:] |
475 | + if s[2:].find(" - condition: ") == 0: |
476 | + if 'conditions' not in string: |
477 | + string['conditions'] = [] |
478 | + string['conditions'].append(s[16:]) |
479 | + continue |
480 | + if s.find("#~") == 0: # obsolete messages |
481 | + continue |
482 | + if s.find("#") == 0: # something else |
483 | + print "%s not expected. Skip" % repr(s) |
484 | + continue # not supported/expected |
485 | + if s.find("msgid ") == 0: |
486 | + cur = "string" |
487 | + if cur not in string: |
488 | + string[cur] = u"" |
489 | + else: |
490 | + string[cur] += "\n" |
491 | + string[cur] += s[6:] |
492 | + continue |
493 | + if s.find("msgstr ") == 0: |
494 | + cur = "translation" |
495 | + if cur not in string: |
496 | + string[cur] = u"" |
497 | + else: |
498 | + string[cur] += "\n" |
499 | + string[cur] += s[7:] |
500 | + continue |
501 | + if s.find('"') == 0: |
502 | + if cur is None: |
503 | + print "'%s' not expected here. Skip" % s |
504 | + continue |
505 | + string[cur] += "\n" + s |
506 | + continue |
507 | + print "'%s' not expected here. Skip" % s |
508 | + return None if string == {} else string |
509 | + |
510 | + def write(self, string): |
511 | + self.fd.write(string) |
512 | + |
513 | + def write_header(self): |
514 | + lang_team = "LANGUAGE <LL@li.org>" if self.is_pot else "%s <%s@li.org>" % (self.lang, self.lang) |
515 | + lang_str = "template" if self.is_pot else "for lang '%s'" % self.lang |
516 | + date = "YEAR-MO-DA HO:MI+ZONE" if self.is_pot else \ |
517 | + datetime.fromtimestamp(self.translation_date).strftime("%Y-%m-%d %H:%M+0000") |
518 | + self.write("# Chromium Translations %s.\n" |
519 | + "# Copyright (C) 2010-2011 Fabien Tassin\n" |
520 | + "# This file is distributed under the same license as the chromium-browser package.\n" |
521 | + "# Fabien Tassin <fta@ubuntu.com>, 2010-2011.\n" |
522 | + "#\n" % lang_str) |
523 | + # FIXME: collect contributors (can LP export them?) |
524 | + self.write('msgid ""\n' |
525 | + 'msgstr ""\n' |
526 | + '"Project-Id-Version: chromium-browser.head\\n"\n' |
527 | + '"Report-Msgid-Bugs-To: https://bugs.launchpad.net/ubuntu/+source/chromium-browser/+filebug\\n"\n' |
528 | + '"POT-Creation-Date: %s\\n"\n' |
529 | + '"PO-Revision-Date: %s\\n"\n' |
530 | + '"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"\n' |
531 | + '"Language-Team: %s\\n"\n' |
532 | + '"MIME-Version: 1.0\\n"\n' |
533 | + '"Content-Type: text/plain; charset=UTF-8\\n"\n' |
534 | + '"Content-Transfer-Encoding: 8bit\\n"\n\n' % \ |
535 | + (datetime.fromtimestamp(self.template_date).strftime("%Y-%m-%d %H:%M+0000"), |
536 | + date, lang_team)) |
537 | + |
538 | + def write_footer(self): |
539 | + pass |
540 | + |
541 | + def write_all_strings(self): |
542 | + for string in self.strings: |
543 | + self.write(u"#. %s\n" % u"\n#. ".join(string['comment'].split("\n"))) |
544 | + self.write(u"#: id: %s (used in the following branches: %s)\n" % \ |
545 | + (string['id'], ", ".join(string['origin']))) |
546 | + self.write(u'msgid %s\n' % StringCvt().xtb2gettext(string['string'])) |
547 | + self.write(u'msgstr %s\n\n' % StringCvt().xtb2gettext(string['translation'])) |
548 | + |
549 | + def export_file(self, directory = None, filename = None): |
550 | + self.open(mode = "wb", filename = filename) |
551 | + self.write_header() |
552 | + self.write_all_strings() |
553 | + self.write_footer() |
554 | + return self.close() |
555 | + |
556 | + def import_file(self): |
557 | + self.mtime = self.get_mtime(self.filename) |
558 | + self.open() |
559 | + while 1: |
560 | + string = self.read_string() |
561 | + if string is None: |
562 | + break |
563 | + self.strings.append(string) |
564 | + self.close() |
565 | + |
566 | + def pack_comment(self, data): |
567 | + comment = "" |
568 | + for ent in sorted(data, lambda x,y: cmp(x['code'], y['code'])): |
569 | + comment += "%s\n- description: %s\n" % (ent['code'], ent['desc']) |
570 | + if ent['test'] is not None: |
571 | + comment += "- condition: %s\n" % ent['test'] |
572 | + comment = comment[:-1] # strip trailing \n |
573 | + return comment |
574 | + |
575 | + def get_origins(self, data): |
576 | + o = [] |
577 | + for ent in sorted(data, lambda x,y: cmp(x['code'], y['code'])): |
578 | + for origin in ent['origin']: |
579 | + if origin not in o: |
580 | + o.append(origin) |
581 | + return o |
582 | + |
583 | + def import_grd(self, grd): |
584 | + imported = 0 |
585 | + for id in sorted(grd.supported_ids.keys()): |
586 | + if 'ids' not in grd.supported_ids[id]: |
587 | + continue |
588 | + comment = self.pack_comment(grd.supported_ids[id]['ids']) |
589 | + string = grd.supported_ids[id]['ids'][0]['val'] |
590 | + origin = self.get_origins(grd.supported_ids[id]['ids']) |
591 | + self.strings.append({ 'id': id, 'comment': comment, 'string': string, |
592 | + 'origin': origin, 'translation': '' }) |
593 | + imported += 1 |
594 | + if self.debug: |
595 | + print "imported %d strings from the grd template" % imported |
596 | + |
597 | +class PoFile(PotFile): |
598 | + """ |
599 | + Read and write gettext po files |
600 | + """ |
601 | + |
602 | + def __init__(self, lang, filename, template, date = None, debug = None, |
603 | + branch_name = "default", branch_dir = os.getcwd()): |
604 | + super(PoFile, self).__init__(filename, date = template.template_date, debug = debug, |
605 | + branch_name = branch_name, branch_dir = branch_dir) |
606 | + self.template = template |
607 | + self.lang = lang |
608 | + self.translation_date = date |
609 | + self.is_pot = False |
610 | + |
611 | + def import_xtb(self, xtb): |
612 | + # only import strings present in the current template |
613 | + imported = 0 |
614 | + for id in sorted(xtb.template.supported_ids.keys()): |
615 | + if 'ids' not in xtb.template.supported_ids[id]: |
616 | + continue |
617 | + translation = xtb.strings[id] if id in xtb.strings else "" |
618 | + comment = self.template.pack_comment(xtb.template.supported_ids[id]['ids']) |
619 | + string = xtb.template.supported_ids[id]['ids'][0]['val'] |
620 | + origin = self.get_origins(xtb.template.supported_ids[id]['ids']) |
621 | + self.add_string(id, comment, string, translation, origin) |
622 | + imported += 1 |
623 | + if self.debug: |
624 | + print "imported %d translations for lang %s from xtb into po %s" % (imported, self.lang, self.filename) |
625 | + |
626 | +class GrdFile(PotFile): |
627 | + """ |
628 | + Read a Grit GRD file (write is not supported) |
629 | + """ |
630 | + def __init__(self, filename, date = None, lang_mapping = None, debug = None, |
631 | + branch_name = "default", branch_dir = os.getcwd()): |
632 | + super(GrdFile, self).__init__(filename, date = date, debug = debug, |
633 | + branch_name = branch_name, branch_dir = branch_dir) |
634 | + self.lang_mapping = lang_mapping |
635 | + self.mapped_langs = {} |
636 | + self.supported_langs = {} |
637 | + self.supported_ids = {} |
638 | + self.supported_ids_counts = {} |
639 | + self.translated_strings = {} |
640 | + self.stats = {} # per lang |
641 | + self.debug = debug |
642 | + self._PH_REGEXP = re.compile('(<ph name=")([^"]*)("/>)') |
643 | + |
644 | + def open(self): |
645 | + pass |
646 | + |
647 | + def close(self): |
648 | + pass |
649 | + |
650 | + def write_header(self): |
651 | + raise Exception("Not implemented!") |
652 | + |
653 | + def write_footer(self): |
654 | + raise Exception("Not implemented!") |
655 | + |
656 | + def write_all_strings(self): |
657 | + raise Exception("Not implemented!") |
658 | + |
659 | + def export_file(self, directory = None, filename = None, global_langs = None, langs = None): |
660 | + fdi = codecs.open(self.filename, 'rb', encoding="utf-8") |
661 | + fdo = codecs.open(filename, 'wb', encoding="utf-8") |
662 | + # can't use minidom here as the file is manually generated and the |
663 | + # output will create big diffs. parse the source file line by line |
664 | + # and insert our xtb in the <translations> section. Also insert new |
665 | + # langs in the <outputs> section (with type="data_package" or type="js_map_format"). |
666 | + # Let everything else untouched |
667 | + tr_found = False |
668 | + tr_saved = [] |
669 | + tr_has_ifs = False |
670 | + tr_skipping_if_not = False |
671 | + pak_found = False |
672 | + pak_saved = [] |
673 | + # langs, sorted by their xtb names |
674 | + our_langs = map(lambda x: x[0], |
675 | + sorted(map(lambda x: (x, self.mapped_langs[x]['xtb_file']), |
676 | + self.mapped_langs), |
677 | + key = lambda x: x[1])) # d'oh! |
678 | + if langs is None: |
679 | + langs = our_langs[:] |
680 | + for line in fdi.readlines(): |
681 | + if re.match(r'.*?<output filename=".*?" type="(data_package|js_map_format)"', line): |
682 | + pak_found = True |
683 | + pak_saved.append(line) |
684 | + continue |
685 | + if line.find('</outputs>') > 0: |
686 | + pak_found = False |
687 | + ours = global_langs[:] |
688 | + chunks = {} |
689 | + c = None |
690 | + pak_if = None |
691 | + pak_is_in_if = False |
692 | + for l in pak_saved: |
693 | + if l.find("<!-- ") > 0: |
694 | + c = l |
695 | + continue |
696 | + if l.find("<if ") > -1: |
697 | + c = l if c is None else c + l |
698 | + tr_has_ifs = True |
699 | + pak_is_in_if = True |
700 | + continue |
701 | + if l.find("</if>") > -1: |
702 | + c = l if c is None else c + l |
703 | + pak_is_in_if = False |
704 | + continue |
705 | + m = re.match(r'.*?<output filename="(.*?)_([^_\.]+)\.(pak|js)" type="(data_package|js_map_format)" lang="(.*?)" />', l) |
706 | + if m is not None: |
707 | + x = { 'name': m.group(1), 'ext': m.group(3), 'lang': m.group(5), 'file_lang': m.group(2), |
708 | + 'type': m.group(4), 'in_if': pak_is_in_if, 'line': l } |
709 | + if c is not None: |
710 | + x['comment'] = c |
711 | + c = None |
712 | + k = m.group(2) if m.group(2) != 'nb' else 'no' |
713 | + chunks[k] = x |
714 | + else: |
715 | + if c is None: |
716 | + c = l |
717 | + else: |
718 | + c += l |
719 | + is_in_if = False |
720 | + for lang in sorted(chunks.keys()): |
721 | + tlang = lang if lang != 'no' else 'nb' |
722 | + while len(ours) > 0 and ((ours[0] == 'nb' and 'no' < tlang) or (ours[0] != 'nb' and ours[0] < tlang)): |
723 | + if ours[0] in chunks: |
724 | + ours = ours[1:] |
725 | + continue |
726 | + if tr_has_ifs and is_in_if is False: |
727 | + fdo.write(' <if expr="pp_ifdef(\'use_third_party_translations\')">\n') |
728 | + f = "%s_%s.%s" % (chunks[lang]['name'], ours[0], chunks[lang]['ext']) |
729 | + fdo.write(' %s<output filename="%s" type="%s" lang="%s" />\n' % \ |
730 | + (' ' if tr_has_ifs else '', f, chunks[lang]['type'], ours[0])) |
731 | + is_in_if = True |
732 | + if tr_has_ifs and chunks[lang]['in_if'] is False: |
733 | + if 'comment' not in chunks[lang] or chunks[lang]['comment'].find('</if>') == -1: |
734 | + fdo.write(' </if>\n') |
735 | + is_in_if = False |
736 | + ours = ours[1:] |
737 | + if 'comment' in chunks[lang]: |
738 | + for s in chunks[lang]['comment'].split('\n')[:-1]: |
739 | + if chunks[lang]['in_if'] is True and is_in_if and s.find('<if ') > -1: |
740 | + continue |
741 | + if s.find('<!-- No translations available. -->') > -1: |
742 | + continue |
743 | + fdo.write(s + '\n') |
744 | + fdo.write(chunks[lang]['line']) |
745 | + if ours[0] == tlang: |
746 | + ours = ours[1:] |
747 | + is_in_if = chunks[lang]['in_if'] |
748 | + if len(chunks.keys()) > 0: |
749 | + while len(ours) > 0: |
750 | + f = "%s_%s.%s" % (chunks[lang]['name'], ours[0], chunks[lang]['ext']) |
751 | + if tr_has_ifs and is_in_if is False: |
752 | + fdo.write(' <if expr="pp_ifdef(\'use_third_party_translations\')">\n') |
753 | + fdo.write(' %s<output filename="%s" type="data_package" lang="%s" />\n' % \ |
754 | + (' ' if tr_has_ifs else '', f, ours[0])) |
755 | + is_in_if = True |
756 | + ours = ours[1:] |
757 | + if tr_has_ifs and is_in_if: |
758 | + fdo.write(' </if>\n') |
759 | + is_in_if = False |
760 | + if c is not None: |
761 | + for s in c.split('\n')[:-1]: |
762 | + if s.find('<!-- No translations available. -->') > -1: |
763 | + continue |
764 | + if s.find('</if>') > -1: |
765 | + continue |
766 | + fdo.write(s + '\n') |
767 | + if line.find('<translations>') > 0: |
768 | + fdo.write(line) |
769 | + tr_found = True |
770 | + continue |
771 | + if line.find('</translations>') > 0: |
772 | + tr_found = False |
773 | + ours = our_langs[:] |
774 | + chunks = {} |
775 | + obsolete = [] |
776 | + c = None |
777 | + tr_if = None |
778 | + tr_is_in_if = False |
779 | + for l in tr_saved: |
780 | + if l.find("</if>") > -1: |
781 | + if tr_skipping_if_not: |
782 | + tr_skipping_if_not = False |
783 | + continue |
784 | + tr_is_in_if = False |
785 | + continue |
786 | + if tr_skipping_if_not: |
787 | + continue |
788 | + if l.find("<!-- ") > 0: |
789 | + c = l if c is None else c + l |
790 | + continue |
791 | + if l.find("<if ") > -1: |
792 | + m = re.match(r'.*?<if expr="not pp_ifdef\(\'use_third_party_translations\'\)"', l) |
793 | + if m is not None: |
794 | + tr_skipping_if_not = True |
795 | + continue |
796 | + tr_has_ifs = True |
797 | + tr_is_in_if = True |
798 | + continue |
799 | + m = re.match(r'.*?<file path=".*_([^_]+)\.xtb" lang="(.*?)"', l) |
800 | + if m is not None: |
801 | + tlang = m.group(2) |
802 | + if m.group(1) == 'iw': |
803 | + tlang = m.group(1) |
804 | + x = { 'lang': tlang, 'line': l, 'in_if': tr_is_in_if } |
805 | + if c is not None: |
806 | + x['comment'] = c |
807 | + c = None |
808 | + chunks[tlang] = x |
809 | + if tlang not in langs and tlang not in map(lambda t: self.mapped_langs[t]['grit'], langs): |
810 | + obsolete.append(tlang) |
811 | + else: |
812 | + if c is None: |
813 | + c = l |
814 | + else: |
815 | + c += l |
816 | + is_in_if = False |
817 | + # Do we want <if/> in the <translations/> block? (they are only mandatory in the <outputs/> block) |
818 | + want_ifs_in_translations = False |
819 | + for lang in sorted(chunks.keys()): |
820 | + while len(ours) > 0 and self.mapped_langs[ours[0]]['xtb_file'] < lang.replace('@', '-'): |
821 | + if ours[0] not in self.supported_langs: |
822 | + if self.debug: |
823 | + print "Skipped export of lang '%s' (most probably a 'po' file without any translated strings)" % ours[0] |
824 | + ours = ours[1:] |
825 | + continue |
826 | + if ours[0] in obsolete: |
827 | + if self.debug: |
828 | + print "Skipped export of lang '%s' (now obsolete)" % ours[0] |
829 | + ours = ours[1:] |
830 | + continue |
831 | + f = os.path.relpath(self.supported_langs[ours[0]], os.path.dirname(self.filename)) |
832 | + if want_ifs_in_translations and tr_has_ifs and is_in_if is False: |
833 | + fdo.write(' <if expr="pp_ifdef(\'use_third_party_translations\')">\n') |
834 | + is_in_if = True |
835 | + fdo.write(' %s<file path="%s" lang="%s" />\n' % |
836 | + (' ' if (is_in_if or want_ifs_in_translations) and tr_has_ifs else '', f, ours[0])) |
837 | + if tr_has_ifs and chunks[lang]['in_if'] is False: |
838 | + if want_ifs_in_translations: |
839 | + fdo.write(' </if>\n') |
840 | + is_in_if = False |
841 | + ours = ours[1:] |
842 | + if 'comment' in chunks[lang]: |
843 | + for s in chunks[lang]['comment'].split('\n')[:-1]: |
844 | + if chunks[lang]['in_if'] is True and is_in_if and s.find('<if ') > -1: |
845 | + continue |
846 | + if s.find('<!-- No translations available. -->') > -1: |
847 | + continue |
848 | + fdo.write(s + '\n') |
849 | + if lang not in obsolete: |
850 | + fdo.write(chunks[lang]['line']) |
851 | + ours = ours[1:] |
852 | + is_in_if = chunks[lang]['in_if'] |
853 | + while len(ours) > 0: |
854 | + if ours[0] in self.supported_langs: |
855 | + f = os.path.relpath(self.supported_langs[ours[0]], os.path.dirname(self.filename)) |
856 | + if want_ifs_in_translations and tr_has_ifs and is_in_if is False: |
857 | + fdo.write(' <if expr="pp_ifdef(\'use_third_party_translations\')">\n') |
858 | + is_in_if = True |
859 | + fdo.write(' %s<file path="%s" lang="%s" />\n' % |
860 | + (' ' if (is_in_if or want_ifs_in_translations) and tr_has_ifs else '', f, ours[0])) |
861 | + elif self.debug: |
862 | + print "Skipped lang %s with no translated strings" % ours[0] |
863 | + ours = ours[1:] |
864 | + |
865 | + if is_in_if and want_ifs_in_translations: |
866 | + fdo.write(' </if>\n') |
867 | + is_in_if = False |
868 | + if c is not None: |
869 | + for s in c.split('\n')[:-1]: |
870 | + if s.find('<!-- No translations available. -->') > -1: |
871 | + continue |
872 | + if s.find('</if>') > -1: |
873 | + continue |
874 | + fdo.write(s + '\n') |
875 | + if tr_found: |
876 | + tr_saved.append(line) |
877 | + continue |
878 | + if pak_found: |
879 | + pak_saved.append(line) |
880 | + continue |
881 | + fdo.write(line) |
882 | + fdi.close() |
883 | + fdo.close() |
884 | + |
885 | + def uc(self, match): |
886 | + return match.group(2).upper() |
887 | + |
888 | + def uc_name(self, match): |
889 | + return match.group(1) + match.group(2).upper() + match.group(3) |
890 | + |
891 | + def is_string_valid_for_lang(self, id, lang): |
892 | + ok = False |
893 | + for string in self.supported_ids[id]['ids']: |
894 | + if string['test'] is not None: |
895 | + ok |= EvalConditions().lang_eval(string['test'], lang) |
896 | + if ok: |
897 | + break |
898 | + else: |
899 | + ok = True |
900 | + break |
901 | + return ok |
902 | + |
903 | + def get_supported_strings_count(self, lang): |
904 | + # need to ignore strings for which this lang is not wanted in the <if> conditions |
905 | + if lang in self.supported_ids_counts: |
906 | + return self.supported_ids_counts[lang]['count'], self.supported_ids_counts[lang]['skipped'] |
907 | + count = 0 |
908 | + skipped = 0 |
909 | + for id in self.supported_ids: |
910 | + ok = self.is_string_valid_for_lang(id, lang) |
911 | + if ok: |
912 | + count += 1 |
913 | + else: |
914 | + skipped += 1 |
915 | + assert count + skipped == len(self.supported_ids.keys()) |
916 | + self.supported_ids_counts[lang] = { 'count': count, 'skipped': skipped } |
917 | + return count, skipped |
918 | + |
919 | + def get_supported_langs(self): |
920 | + return sorted(self.supported_langs.keys()) |
921 | + |
922 | + def get_supported_lang_filenames(self): |
923 | + """ return the list of (xtb) filenames sorted by langs (so it's |
924 | + possible to zip() it) """ |
925 | + return map(lambda l: self.supported_langs[l], sorted(self.supported_langs.keys())) |
926 | + |
927 | + def update_stats(self, lang, translated_upstream = 0, obsolete = 0, |
928 | + new = 0, updated = 0, skipped_lang = 0, mandatory_linux = 0): |
929 | + if lang not in self.stats: |
930 | + self.stats[lang] = { 'translated_upstream': 0, 'skipped_lang': 0, |
931 | + 'obsolete': 0, 'new': 0, 'updated': 0, |
932 | + 'mandatory_linux': 0 } |
933 | + self.stats[lang]['translated_upstream'] += translated_upstream - updated |
934 | + self.stats[lang]['obsolete'] += obsolete |
935 | + self.stats[lang]['new'] += new |
936 | + self.stats[lang]['updated'] += updated |
937 | + self.stats[lang]['skipped_lang'] += skipped_lang |
938 | + self.stats[lang]['mandatory_linux'] += mandatory_linux |
939 | + |
940 | + def merge_template(self, template, newer_preferred = True): |
941 | + """ merge strings from 'template' into self (the master template). |
942 | + If the string differs, prefer the new one when newer_preferred is set """ |
943 | + for id in template.supported_ids: |
944 | + if id not in self.supported_ids: |
945 | + if self.debug: |
946 | + print "merged code %s (id %s) from branch '%s' from %s" % \ |
947 | + (template.supported_ids[id]['ids'][0]['code'], id, |
948 | + template.supported_ids[id]['ids'][0]['origin'][0], template.filename) |
949 | + self.supported_ids[id] = template.supported_ids[id] |
950 | + else: |
951 | + for ent in template.supported_ids[id]['ids']: |
952 | + found = False |
953 | + for ent2 in self.supported_ids[id]['ids']: |
954 | + if ent2['code'] != ent['code']: |
955 | + continue |
956 | + found = True |
957 | + ent2['origin'].append(ent['origin'][0]) |
958 | + if ent['test'] != ent2['test'] or \ |
959 | + ent['desc'] != ent2['desc']: |
960 | + if newer_preferred: |
961 | + ent2['test'] = ent['test'] |
962 | + ent2['desc'] = ent['desc'] |
963 | + if not found: |
964 | + if self.debug: |
965 | + print "adding new ids code '%s' from branch '%s' for string id %s" % \ |
966 | + (ent['code'], template.supported_ids[id]['ids'][0]['origin'][0], id) |
967 | + self.supported_ids[id]['ids'].append(ent) |
968 | + |
969 | + def add_translation(self, lang, id, translation): |
970 | + if id not in self.supported_ids: |
971 | + if self.debug: |
972 | + print "*warn* obsolete string id %s for lang %s" % (id, lang) |
973 | + return |
974 | + self.supported_ids[id]['lang'][lang] = translation |
975 | + |
976 | + def merge_translations(self, lang, xtb, master_xtb = None, newer_preferred = True): |
977 | + if lang not in self.supported_langs: |
978 | + self.supported_langs[lang] = xtb.filename |
979 | + for id in xtb.strings: |
980 | + if id not in self.supported_ids: |
981 | + # d'oh!! obsolete translation? |
982 | + self.update_stats(lang, obsolete = 1) |
983 | + continue |
984 | + if not self.is_string_valid_for_lang(id, lang): |
985 | + # string not wanted for that lang, skipped |
986 | + continue |
987 | + if 'lang' not in self.supported_ids[id]: |
988 | + self.supported_ids[id]['lang'] = {} |
989 | + if lang in self.supported_ids[id]['lang']: |
990 | + # already have a translation for this string |
991 | + if newer_preferred and xtb.strings[id] != self.supported_ids[id]['lang'][lang]: |
992 | + self.supported_ids[id]['lang'][lang] = xtb.strings[id] |
993 | + else: |
994 | + self.update_stats(lang, translated_upstream = 1) |
995 | + self.supported_ids[id]['lang'][lang] = xtb.strings[id] |
996 | + if master_xtb is not None: |
997 | + master_xtb.strings[id] = xtb.strings[id] |
998 | + |
999 | + def read_string(self, node, test = None): |
1000 | + desc = node.getAttribute('desc') |
1001 | + name = node.getAttribute('name') |
1002 | + if not node.firstChild: |
1003 | + # no string? weird. Skip. (e.g. IDS_LOAD_STATE_IDLE) |
1004 | + return |
1005 | + |
1006 | + # Get a/ the full string from the grd, b/ its transformation |
1007 | + # into the smaller version found in xtb files (val) and c/ another into |
1008 | + # something suitable for the 64bit key generator (kval) |
1009 | + |
1010 | + orig_val = "".join([ n.toxml() for n in node.childNodes ]) |
1011 | + |
1012 | + # encode the value to create the 64bit ID needed for the xtb mapping. |
1013 | + # |
1014 | + # grd: 'f&oo "<ph name="IDS_xX">$1<ex>blabla</ex></ph>" bar' |
1015 | + # xtb: 'f&oo "<ph name="IDS_XX"/>" bar' |
1016 | + # but the string used to create the 64bit id is only 'f&oo "IDS_XX" bar'. |
1017 | + # Also, the final value must be positive, while FingerPrint() returns |
1018 | + # a signed long. Of course, none of this is documented... |
1019 | + |
1020 | + # grd->xtb |
1021 | + for x in node.getElementsByTagName('ph'): |
1022 | + while x.hasChildNodes(): |
1023 | + x.removeChild(x.childNodes[0]) |
1024 | + val = "".join([ n.toxml() for n in node.childNodes ]).strip() |
1025 | + # xtb->id |
1026 | + kval = StringCvt().decode_xml_entities(unescape(self._PH_REGEXP.sub(self.uc, val))).encode('utf-8') |
1027 | + kval = kval.replace('"', '"') # not replaced by unescape() |
1028 | + |
1029 | + val = self._PH_REGEXP.sub(self.uc_name, val) |
1030 | + val = val.encode("ascii", "xmlcharrefreplace").strip().encode('utf-8') |
1031 | + |
1032 | + # finally, create the 64bit ID |
1033 | + id = str(FingerPrint(kval) & 0x7fffffffffffffffL) |
1034 | + |
1035 | + if val == '': |
1036 | + # unexpect <message/> block with attributes but without value, skip |
1037 | + return |
1038 | + |
1039 | + if id not in self.supported_ids: |
1040 | + self.supported_ids[id] = { 'ids': [] } |
1041 | + self.supported_ids[id]['ids'].append({ 'code': name, 'desc': desc, |
1042 | + 'val': val, 'test': test, |
1043 | + 'origin': [ self.branch_name ] }) |
1044 | + |
1045 | + def read_strings(self, node, test = None): |
1046 | + for n in node.childNodes: |
1047 | + if n.nodeName == '#text' or n.nodeName == '#comment': |
1048 | + # comments, skip |
1049 | + continue |
1050 | + if n.nodeName == 'message': |
1051 | + self.read_string(n, test) |
1052 | + continue |
1053 | + if n.nodeName == 'if': |
1054 | + expr = n.getAttribute('expr') |
1055 | + if expr is not None and test is not None: |
1056 | + assert "nested <if> not supported" |
1057 | + self.read_strings(n, expr) |
1058 | + continue |
1059 | + if n.nodeName == 'part': |
1060 | + f = n.getAttribute('file') |
1061 | + qualified_file = os.path.join(os.path.dirname(self.filename), f) |
1062 | + self.import_file(override_filename=qualified_file) |
1063 | + continue |
1064 | + raise Exception("unknown tag (<%s> type %s): ''%s''" % \ |
1065 | + (n.nodeName, n.nodeType, n.toxml())) |
1066 | + |
1067 | + def import_json_file(self, filename): |
1068 | + # unlike its name seems to indicate, this file is definitely not a json file. |
1069 | + # It's a python object, dumped in a file. It means it's far easier to parse |
1070 | + # because there's no extra unescaping to do on all the strings. It also |
1071 | + # means we can't use the json module |
1072 | + rfile = os.path.join(self.branch_dir, filename) |
1073 | + if self.debug: |
1074 | + print "parse_json('%s') [%s]" % (filename, rfile) |
1075 | + fd = open(rfile, "rb") |
1076 | + data = fd.read() |
1077 | + fd.close() |
1078 | + vars = { '__builtins__': { 'True': True, 'False': False } } # prevent eval from using the real current globals |
1079 | + data = eval(data, vars) |
1080 | + # Check if this is a format we support |
1081 | + if 'policy_definitions' in data and len(data['policy_definitions']) > 0 and \ |
1082 | + 'caption' not in data['policy_definitions'][0]: |
1083 | + # most probably Chromium v9. It used 'annotations' instead of 'caption' |
1084 | + # Not worth supporting that, all the strings we need in v9 are already in |
1085 | + # the grd file. Skip this json file |
1086 | + if self.debug: |
1087 | + print "Found older unsupported json format. Skipped" |
1088 | + return |
1089 | + if 'messages' in data: |
1090 | + for msg in data['messages']: |
1091 | + self.read_policy('IDS_POLICY_' + msg.upper(), |
1092 | + data['messages'][msg]['desc'], |
1093 | + data['messages'][msg]['text']) |
1094 | + if 'policy_definitions' in data: |
1095 | + for policy in data['policy_definitions']: |
1096 | + name = 'IDS_POLICY_' + policy['name'].upper() |
1097 | + if policy['type'] in [ 'main', 'int', 'string', 'list', 'string-enum', 'int-enum', 'string-enum-list' ]: |
1098 | + # caption |
1099 | + self.read_policy(name + '_CAPTION', |
1100 | + "Caption of the '%s' policy." % policy['name'], |
1101 | + policy['caption']) |
1102 | + # label (optional) |
1103 | + if 'label' in policy: |
1104 | + self.read_policy(name + '_LABEL', |
1105 | + "Label of the '%s' policy." % policy['name'], |
1106 | + policy['label']) |
1107 | + # desc |
1108 | + self.read_policy(name + '_DESC', |
1109 | + "Description of the '%s' policy." % policy['name'], |
1110 | + policy['desc']) |
1111 | + if policy['type'] in [ 'string-enum', 'int-enum', 'string-enum-list' ]: |
1112 | + for item in policy['items']: |
1113 | + self.read_policy('IDS_POLICY_ENUM_' + item['name'].upper().replace(' ', '_') + '_CAPTION', |
1114 | + "Label in a '%s' dropdown menu for selecting '%s'" % \ |
1115 | + (policy['name'], item['name']), |
1116 | + item['caption']) |
1117 | + continue |
1118 | + if policy['type'] == 'group': |
1119 | + # group caption |
1120 | + self.read_policy(name + '_CAPTION', |
1121 | + "Caption of the group of '%s' related policies." % name, |
1122 | + policy['caption']) |
1123 | + # group label (optional) |
1124 | + if 'label' in policy: |
1125 | + self.read_policy(name + '_LABEL', |
1126 | + "Label of the group of '%s' related policies." % name, |
1127 | + policy['label']) |
1128 | + # group desc |
1129 | + self.read_policy(name + '_DESC', |
1130 | + "Description of the group of '%s' related policies." % name, |
1131 | + policy['desc']) |
1132 | + for spolicy in policy['policies']: |
1133 | + sname = 'IDS_POLICY_' + spolicy['name'].upper() |
1134 | + # desc |
1135 | + self.read_policy(sname + '_DESC', |
1136 | + "Description of the '%s' policy." % spolicy['name'], |
1137 | + spolicy['desc']) |
1138 | + # label (optional) |
1139 | + if 'label' in spolicy: |
1140 | + self.read_policy(sname + '_LABEL', |
1141 | + "Label of the '%s' policy." % spolicy['name'], |
1142 | + spolicy['label']) |
1143 | + # caption |
1144 | + self.read_policy(sname + '_CAPTION', |
1145 | + "Caption of the '%s' policy." % spolicy['name'], |
1146 | + spolicy['caption']) |
1147 | + if spolicy['type'] in [ 'int-enum', 'string-enum' ]: |
1148 | + # only caption |
1149 | + for item in spolicy['items']: |
1150 | + self.read_policy('IDS_POLICY_ENUM_' + item['name'].upper() + '_CAPTION', |
1151 | + "Label in a '%s' dropdown menu for selecting a '%s' of '%s'" % \ |
1152 | + (policy['name'], spolicy['name'], item['name']), |
1153 | + item['caption']) |
1154 | + continue |
1155 | + # The new type is not yet being used: http://code.google.com/p/chromium/issues/detail?id=108992 |
1156 | + if policy['type'] == 'external': |
1157 | + continue |
1158 | + if policy['type'] == 'dict': |
1159 | + continue |
1160 | + |
1161 | + assert False, "Policy type '%s' not supported while parsing %s" % (policy['type'], rfile) |
1162 | + |
1163 | + def read_policy(self, name, desc, text): |
1164 | + xml = '<x><message name="%s" desc="%s">\n%s\n</message></x>' % (name, desc, text) |
1165 | + dom = minidom.parseString(xml) |
1166 | + self.read_strings(dom.getElementsByTagName('x')[0]) |
1167 | + |
1168 | + def _add_xtb(self, node): |
1169 | + if node.nodeName != 'file': |
1170 | + return |
1171 | + path = node.getAttribute('path') |
1172 | + m = re.match(r'.*_([^_]+)\.xtb', path) |
1173 | + flang = m.group(1) |
1174 | + lang = node.getAttribute('lang') |
1175 | + tlang = lang |
1176 | + if self.lang_mapping is not None and lang in self.lang_mapping: |
1177 | + if self.debug: |
1178 | + print "# mapping lang '%s' to '%s'" % (lang, self.lang_mapping[lang]) |
1179 | + tlang = self.lang_mapping[lang] |
1180 | + tlang = tlang.replace('-', '_') |
1181 | + self.supported_langs[lang] = os.path.normpath(os.path.join(os.path.dirname(self.filename), path)) |
1182 | + self.translated_strings[lang] = {} |
1183 | + glang = lang |
1184 | + if flang == 'iw': |
1185 | + glang = flang |
1186 | + #assert lang not in self.mapped_langs, "'%s' already in self.mapped_langs" % lang |
1187 | + if lang not in self.mapped_langs: |
1188 | + self.mapped_langs[lang] = { 'xtb_file': flang, 'grit': glang, 'gettext': tlang } |
1189 | + |
1190 | + def import_file(self, override_filename=None): |
1191 | + if override_filename: |
1192 | + assert self.branch_dir |
1193 | + filename = os.path.join(self.branch_dir, override_filename) |
1194 | + else: |
1195 | + filename = os.path.join(self.branch_dir, self.filename) if self.branch_dir is not None \ |
1196 | + else self.filename |
1197 | + self.supported_langs = {} |
1198 | + self.mtime = self.get_mtime(self.filename) |
1199 | + if self.debug: |
1200 | + print "minidom.parse(%s)" % filename |
1201 | + dom = minidom.parse(filename) |
1202 | + grits = dom.getElementsByTagName('grit') |
1203 | + if not grits: |
1204 | + grits = dom.getElementsByTagName('grit-part') |
1205 | + grit = grits[0] |
1206 | + for node in grit.childNodes: |
1207 | + if node.nodeName == '#text' or node.nodeName == '#comment': |
1208 | + # comments, skip |
1209 | + continue |
1210 | + if node.nodeName == 'outputs': |
1211 | + # skip, nothing for us here |
1212 | + continue |
1213 | + if node.nodeName == 'translations': |
1214 | + # collect the supported langs by scanning the list of xtb files |
1215 | + for n in node.childNodes: |
1216 | + if n.nodeName == 'if': |
1217 | + for nn in n.childNodes: |
1218 | + self._add_xtb(nn) |
1219 | + continue |
1220 | + self._add_xtb(n) |
1221 | + continue |
1222 | + if node.nodeName == 'release': |
1223 | + for n in node.childNodes: |
1224 | + if n.nodeName == '#text' or n.nodeName == '#comment': |
1225 | + # comments, skip |
1226 | + continue |
1227 | + if n.nodeName == 'includes': |
1228 | + # skip, nothing for us here |
1229 | + continue |
1230 | + if n.nodeName == 'structures': |
1231 | + for sn in n.childNodes: |
1232 | + if sn.nodeName != 'structure': |
1233 | + continue |
1234 | + type = sn.getAttribute('type') |
1235 | + if type == 'dialog': |
1236 | + # nothing for us here |
1237 | + continue |
1238 | + name = sn.getAttribute('name') |
1239 | + file = sn.getAttribute('file') |
1240 | + if type == 'policy_template_metafile': |
1241 | + # included file containing the strings that are usually in the <messages> tree. |
1242 | + fname = os.path.normpath(os.path.join(os.path.dirname(self.filename), file)) |
1243 | + self.import_json_file(fname) |
1244 | + continue |
1245 | + else: |
1246 | + if self.debug: |
1247 | + print "unknown <structure> type found ('%s') in %s" % (type, self.filename) |
1248 | + continue |
1249 | + if n.nodeName == 'messages': |
1250 | + self.read_strings(n) |
1251 | + continue |
1252 | + print "unknown tag (<%s> type %s): ''%s''" % (n.nodeName, n.nodeType, n.toxml()) |
1253 | + continue |
1254 | + print "unknown tag (<%s> type %s): ''%s''" % (node.nodeName, node.nodeType, node.toxml()) |
1255 | + |
1256 | +class XtbFile(PoFile): |
1257 | + """ |
1258 | + Read and write a Grit XTB file |
1259 | + """ |
1260 | + |
1261 | + def __init__(self, lang, filename, grd, date = None, debug = None, |
1262 | + branch_name = "default", branch_dir = os.getcwd()): |
1263 | + super(XtbFile, self).__init__(lang, filename, grd, date = date, debug = debug, |
1264 | + branch_name = branch_name, branch_dir = branch_dir) |
1265 | + self.template = grd |
1266 | + self.strings = {} |
1267 | + self.strings_updated = 0 |
1268 | + self.strings_new = 0 |
1269 | + self.strings_order = [] # needed to recreate xtb files in a similar order :( |
1270 | + |
1271 | + def add_translation(self, id, string): |
1272 | + assert id in self.template.supported_ids, "'%s' is not in supported_ids (file=%s)" % (id, self.filename) |
1273 | + while string[-1:] == '\n' and self.template.supported_ids[id]['ids'][0]['val'][-1:] != '\n': |
1274 | + # prevent the `msgid' and `msgstr' entries do not both end with '\n' error |
1275 | + if self.debug: |
1276 | + print "Found unwanted \\n at the end of translation id " + id + " lang " + self.lang + ". Dropped" |
1277 | + string = string[:-1] |
1278 | + while string[0] == '\n' and self.template.supported_ids[id]['ids'][0]['val'][0] != '\n': |
1279 | + # prevent the `msgid' and `msgstr' entries do not both begin with '\n' error |
1280 | + if self.debug: |
1281 | + print "Found unwanted \\n at the begin of translation id " + id + " lang " + self.lang + ". Dropped" |
1282 | + string = string[1:] |
1283 | + self.strings[id] = string |
1284 | + self.strings_order.append(id) |
1285 | + |
1286 | + def write_header(self): |
1287 | + self.write('<?xml version="1.0" ?>\n') |
1288 | + self.write('<!DOCTYPE translationbundle>\n') |
1289 | + self.write('<translationbundle lang="%s">\n' % \ |
1290 | + self.template.mapped_langs[self.lang]['grit']) |
1291 | + |
1292 | + def write_footer(self): |
1293 | + self.write('</translationbundle>') |
1294 | + |
1295 | + def write_all_strings(self): |
1296 | + for id in self.strings_order: |
1297 | + if id in self.strings: |
1298 | + self.write('<translation id="%s">%s</translation>\n' % \ |
1299 | + (id, self.strings[id])) |
1300 | + for id in sorted(self.strings.keys()): |
1301 | + if id in self.strings_order: |
1302 | + continue |
1303 | + self.write('<translation id="%s">%s</translation>\n' % \ |
1304 | + (id, self.strings[id])) |
1305 | + |
1306 | + def import_po(self, po): |
1307 | + for string in po.strings: |
1308 | + if string['string'] == '': |
1309 | + continue |
1310 | + self.add_string(string['id'], string['extracted'], |
1311 | + string['string'], string['translation']) |
1312 | + |
1313 | + def import_file(self): |
1314 | + self.open() |
1315 | + file = self.fd.read() # *sigh* |
1316 | + self.close() |
1317 | + imported = 0 |
1318 | + for m in re.finditer('<translation id="(.*?)">(.*?)</translation>', |
1319 | + file, re.S): |
1320 | + if m.group(1) not in self.template.supported_ids: |
1321 | + if self.debug: |
1322 | + print "found a translation for obsolete string id %s in upstream xtb %s" % (m.group(1), self.filename) |
1323 | + continue |
1324 | + self.add_translation(m.group(1), m.group(2)) |
1325 | + imported += 1 |
1326 | + for m in re.finditer('<translationbundle lang="(.*?)">', file): |
1327 | + lang = m.group(1) |
1328 | + if self.lang in self.template.mapped_langs: |
1329 | + assert self.template.mapped_langs[self.lang]['grit'] == lang, \ |
1330 | + "bad lang mapping for '%s' while importing %s, expected '%s'" % \ |
1331 | + (lang, self.filename, self.template.mapped_langs[self.lang]['grit']) |
1332 | + else: |
1333 | + tlang = lang |
1334 | + if self.template.lang_mapping is not None and lang in self.template.lang_mapping: |
1335 | + if self.debug: |
1336 | + print "# mapping lang '%s' to '%s'" % (lang, self.template.lang_mapping[lang]) |
1337 | + tlang = self.template.lang_mapping[lang] |
1338 | + tlang = tlang.replace('-', '_') |
1339 | + flang = lang.replace('@', '-') |
1340 | + self.template.mapped_langs[lang] = { 'xtb_file': flang, 'grit': lang, 'gettext': tlang } |
1341 | + if self.debug: |
1342 | + print "imported %d strings from the xtb file into lang '%s'" % (imported, self.lang) |
1343 | + self.mtime = self.get_mtime(self.filename) |
1344 | + |
1345 | +### |
1346 | + |
1347 | +class Converter(dict): |
1348 | + """ |
1349 | + Given a grd template and its xtb translations, |
1350 | + a/ exports gettext pot template and po translations, |
1351 | + possibly by merging grd/xtb files from multiple branches |
1352 | + or |
1353 | + b/ imports and merges some gettext po translations, |
1354 | + and exports xtb translations |
1355 | + """ |
1356 | + |
1357 | + def __init__(self, template_filename, lang_mapping = None, date = None, debug = False, |
1358 | + template_mapping = {}, html_output = False, branches = None): |
1359 | + self.debug = debug |
1360 | + self.translations = {} |
1361 | + self.errors = 0 |
1362 | + self.template_changes = 0 |
1363 | + self.translations_changes = 0 |
1364 | + self.lang_mapping = lang_mapping |
1365 | + self.template_mapping = template_mapping |
1366 | + self.file_mapping = {} |
1367 | + self.html_output = html_output |
1368 | + self.stats = {} |
1369 | + self.branches = branches if branches is not None else [ { 'branch': 'default', 'dir': os.getcwd(), 'grd': template_filename } ] |
1370 | + |
1371 | + # read a grd template from a file |
1372 | + self.template = GrdFile(self.branches[0]['grd'], date, lang_mapping = self.lang_mapping, debug = self.debug, |
1373 | + branch_name = self.branches[0]['branch'], branch_dir = self.branches[0]['dir']) |
1374 | + self.file_mapping['grd'] = { 'src': self.branches[0]['grd'], |
1375 | + 'branches': { self.branches[0]['branch']: self.branches[0]['dir'] } } |
1376 | + if 'mapped_grd' in self.branches[0]: |
1377 | + self.file_mapping['grd']['mapped_grd'] = self.branches[0]['mapped_grd'] |
1378 | + self.template.import_file() |
1379 | + self.template_pot = None |
1380 | + for lang, file in zip(self.template.get_supported_langs(), |
1381 | + self.template.get_supported_lang_filenames()): |
1382 | + try: |
1383 | + # also read all the xtb files referenced by this grd template |
1384 | + rfile = os.path.join(self.branches[0]['dir'] , file) |
1385 | + xtb = XtbFile(lang, file, self.template, date = self.template.get_mtime(file), debug = self.debug, |
1386 | + branch_name = self.branches[0]['branch'], branch_dir = self.branches[0]['dir']) |
1387 | + xtb.import_file() |
1388 | + |
1389 | + self.file_mapping['lang_' + lang] = { 'src': file, |
1390 | + 'branches': { self.branches[0]['branch']: self.branches[0]['dir'] } } |
1391 | + self.stats[lang] = { 'strings': self.template.get_supported_strings_count(lang), |
1392 | + 'translated_upstream': 0, |
1393 | + 'changed_in_gettext': 0, |
1394 | + 'rejected': 0 |
1395 | + } |
1396 | + self.template.merge_translations(lang, xtb) |
1397 | + self.translations[lang] = xtb |
1398 | + except Exception, e: |
1399 | + print "Skipping a XTB file, ERROR while importing xtb %s from grd file %s: %s" % (file, self.branches[0]['grd'], str(e)) |
1400 | + |
1401 | + # read other grd templates |
1402 | + if len(self.branches) > 1: |
1403 | + for branch in self.branches[1:]: |
1404 | + if self.debug: |
1405 | + print "merging %s from branch '%s' from %s" % (branch['grd'], branch['branch'], branch['dir']) |
1406 | + template = GrdFile(branch['grd'], date, lang_mapping = self.lang_mapping, debug = self.debug, |
1407 | + branch_name = branch['branch'], branch_dir = branch['dir']) |
1408 | + self.file_mapping['grd']['branches'][branch['branch']] = branch['dir'] |
1409 | + template.import_file() |
1410 | + self.template.merge_template(template, newer_preferred = False) |
1411 | + for lang, file in zip(template.get_supported_langs(), |
1412 | + template.get_supported_lang_filenames()): |
1413 | + xtb = XtbFile(lang, file, self.template, date = template.get_mtime(file), debug = self.debug, |
1414 | + branch_name = branch['branch'], branch_dir = branch['dir']) |
1415 | + if 'lang_' + lang not in self.file_mapping: |
1416 | + self.file_mapping['lang_' + lang] = { 'src': file, 'branches': {} } |
1417 | + self.file_mapping['lang_' + lang]['branches'][branch['branch']] = branch['dir'] |
1418 | + # TODO: stats |
1419 | + xtb.import_file() |
1420 | + if lang not in self.translations: |
1421 | + if self.debug: |
1422 | + print "Add lang '%s' as master xtb for alt branch '%s'" % (lang, branch['branch']) |
1423 | + self.translations[lang] = xtb |
1424 | + self.template.merge_translations(lang, xtb, master_xtb = self.translations[lang], |
1425 | + newer_preferred = False) |
1426 | + |
1427 | + def export_gettext_files(self, directory): |
1428 | + fname = self.file_mapping['grd']['mapped_grd'] \ |
1429 | + if 'mapped_grd' in self.file_mapping['grd'] else self.template.filename |
1430 | + name = os.path.splitext(os.path.basename(fname))[0] |
1431 | + if directory is not None: |
1432 | + directory = os.path.join(directory, name) |
1433 | + if not os.path.isdir(directory): |
1434 | + os.makedirs(directory, 0755) |
1435 | + filename = os.path.join(directory, name + ".pot") |
1436 | + else: |
1437 | + filename = os.path.splitext(fname)[0] + ".pot" |
1438 | + # create a pot template and merge the grd strings into it |
1439 | + self.template_pot = PotFile(filename, date = self.template.mtime, debug = self.debug) |
1440 | + self.template_pot.import_grd(self.template) |
1441 | + # write it to a file |
1442 | + self.template_changes += self.template_pot.export_file(directory = directory) |
1443 | + |
1444 | + # do the same for all langs (xtb -> po) |
1445 | + for lang in self.translations: |
1446 | + gtlang = self.template.mapped_langs[lang]['gettext'] |
1447 | + file = os.path.join(os.path.dirname(filename), gtlang + ".po") |
1448 | + po = PoFile(gtlang, file, self.template_pot, |
1449 | + date = self.translations[lang].translation_date, debug = self.debug) |
1450 | + po.import_xtb(self.translations[lang]) |
1451 | + self.translations_changes += po.export_file(directory) |
1452 | + |
1453 | + def export_grit_xtb_file(self, lang, directory): |
1454 | + name = os.path.splitext(os.path.basename(self.template.filename))[0] |
1455 | + file = os.path.join(directory, os.path.basename(self.template.supported_langs[lang])) |
1456 | + if len(self.translations[lang].strings.keys()) > 0: |
1457 | + if 'lang_' + lang in self.file_mapping: |
1458 | + self.file_mapping['lang_' + lang]['dst'] = file |
1459 | + else: |
1460 | + self.file_mapping['lang_' + lang] = { 'src': None, 'dst': file } |
1461 | + self.translations[lang].export_file(filename = file) |
1462 | + |
1463 | + def export_grit_files(self, directory, langs): |
1464 | + grd_dst = os.path.join(directory, os.path.basename(self.template.filename)) |
1465 | + if len(self.translations.keys()) == 0: |
1466 | + if self.debug: |
1467 | + print "no translation at all, nothing to export here (template: %s)" % self.template.filename |
1468 | + return |
1469 | + if not os.path.isdir(directory): |
1470 | + os.makedirs(directory, 0755) |
1471 | + # 'langs' may contain langs for which this template no longer have translations for. |
1472 | + # They need to be dropped from the grd file |
1473 | + self.template.export_file(filename = grd_dst, global_langs = langs, langs = self.translations.keys()) |
1474 | + self.file_mapping['grd']['dst'] = grd_dst |
1475 | + self.file_mapping['grd']['dir'] = directory[:-len(os.path.dirname(self.template.filename)) - 1] |
1476 | + for lang in self.translations: |
1477 | + prefix = self.template.supported_langs[lang] |
1478 | + fdirectory = os.path.normpath(os.path.join(self.file_mapping['grd']['dir'], os.path.dirname(prefix))) |
1479 | + if not os.path.isdir(fdirectory): |
1480 | + os.makedirs(fdirectory, 0755) |
1481 | + self.export_grit_xtb_file(lang, fdirectory) |
1482 | + |
1483 | + def get_supported_strings_count(self): |
1484 | + return len(self.template.supported_ids.keys()) |
1485 | + |
1486 | + def compare_translations(self, old, new, id, lang): |
1487 | + # strip leading and trailing whitespaces from the upstream strings |
1488 | + # (this should be done upstream) |
1489 | + old = old.strip() |
1490 | + if old != new: |
1491 | + s = self.template.supported_ids[id]['ids'][0]['val'] if 'ids' in self.template.supported_ids[id] else "<none?>" |
1492 | + if self.debug: |
1493 | + print "Found a different translation for id %s in lang '%s':\n string: \"%s\"\n " \ |
1494 | + "upstream: \"%s\"\n launchpad: \"%s\"\n" % (id, lang, s, old, new) |
1495 | + return old == new |
1496 | + |
1497 | + def import_gettext_po_file(self, lang, filename): |
1498 | + """ import a single lang file into the current translations set, |
1499 | + matching the current template. Could be useful to merge the upstream |
1500 | + and launchpad translations, or to merge strings from another project |
1501 | + (like webkit) """ |
1502 | + po = PoFile(self.template.mapped_langs[lang]['gettext'], filename, self.template, |
1503 | + date = self.template.get_mtime(filename), debug = self.debug) |
1504 | + po.import_file() |
1505 | + # no need to continue if there are no translation in this po |
1506 | + translated_count = 0 |
1507 | + for s in po.strings: |
1508 | + if s['string'] != '""' and s['translation'] != '""': |
1509 | + translated_count += 1 |
1510 | + if translated_count == 0: |
1511 | + if self.debug: |
1512 | + print "No translation found for lang %s in %s" % (lang, filename) |
1513 | + return |
1514 | + if lang not in self.translations: |
1515 | + # assuming the filename should be third_party/launchpad_translations/<template_name>_<lang>.xtb |
1516 | + # (relative to $SRC), we need it relatively to the grd directory |
1517 | + tname = os.path.splitext(os.path.basename(self.template.filename))[0] |
1518 | + f = os.path.normpath(os.path.join('third_party/launchpad_translations', |
1519 | + tname + '_' + self.template.mapped_langs[lang]['xtb_file'] + '.xtb')) |
1520 | + self.translations[lang] = XtbFile(lang, f, self.template, date = po.mtime, debug = self.debug) |
1521 | + self.template.supported_langs[lang] = f # *sigh* |
1522 | + |
1523 | + lp669831_skipped = 0 |
1524 | + for string in po.strings: |
1525 | + if 'id' not in string: |
1526 | + continue # PO header |
1527 | + id = string['id'] |
1528 | + if id in self.template.supported_ids: |
1529 | + if 'conditions' in string: |
1530 | + # test the lang against all those conditions. If at least one passes, we need |
1531 | + # the string |
1532 | + found = False |
1533 | + for c in string['conditions']: |
1534 | + found |= EvalConditions().lang_eval(c, lang) |
1535 | + if found is False: |
1536 | + self.template.update_stats(lang, skipped_lang = 1) |
1537 | + if self.debug: |
1538 | + print "Skipped string (lang condition) for %s/%s: %s" % \ |
1539 | + (os.path.splitext(os.path.basename(self.template.filename))[0], |
1540 | + lang, repr(string)) |
1541 | + continue |
1542 | + # workaround bug https://bugs.launchpad.net/rosetta/+bug/669831 |
1543 | + ustring = StringCvt().gettext2xtb(string['string']) |
1544 | + gt_translation = string['translation'][1:-1].replace('"\n"', '') |
1545 | + string['translation'] = StringCvt().gettext2xtb(string['translation']) |
1546 | + if string['translation'] != "": |
1547 | + while string['translation'][-1:] == '\n' and ustring[-1:] != '\n': |
1548 | + # prevent the `msgid' and `msgstr' entries do not both end with '\n' error |
1549 | + if self.debug: |
1550 | + print "Found unwanted \\n at the end of translation id " + id + " lang " + self.lang + ". Dropped" |
1551 | + string['translation'] = string['translation'][:-1] |
1552 | + while string['translation'][0] == '\n' and ustring[0] != '\n': |
1553 | + # prevent the `msgid' and `msgstr' entries do not both begin with '\n' error |
1554 | + if self.debug: |
1555 | + print "Found unwanted \\n at the begin of translation id " + id + " lang " + self.lang + ". Dropped" |
1556 | + string['translation'] = string['translation'][1:] |
1557 | + grit_str = StringCvt().decode_xml_entities(self.template.supported_ids[id]['ids'][0]['val']) |
1558 | + if False and 'ids' in self.template.supported_ids[id] and \ |
1559 | + ustring != grit_str: |
1560 | + # the string for this id is no longer the same, skip it |
1561 | + lp669831_skipped += 1 |
1562 | + if self.debug: |
1563 | + print "lp669831_skipped:\n lp: '%s'\n grd: '%s'" % (ustring, grit_str) |
1564 | + continue |
1565 | + # check for xml errors when '<' or '>' are in the string |
1566 | + if string['translation'].find('<') >= 0 or \ |
1567 | + string['translation'].find('>') >= 0: |
1568 | + try: |
1569 | + # try to parse it with minidom (it's slow!!), and skip if it fails |
1570 | + s = u"<x>" + string['translation'] + u"</x>" |
1571 | + dom = minidom.parseString(s.encode('utf-8')) |
1572 | + except Exception as inst: |
1573 | + print "Parse error in '%s/%s' for id %s. Skipped.\n%s\n%s" % \ |
1574 | + (os.path.splitext(os.path.basename(self.template.filename))[0], lang, id, |
1575 | + repr(string['translation']), inst) |
1576 | + continue |
1577 | + # if the upstream string is not empty, but the contributed string is, keep |
1578 | + # the upstream string untouched |
1579 | + if string['translation'] == '': |
1580 | + continue |
1581 | + # check if we have the same variables in both the upstream string and its |
1582 | + # translation. Otherwise, complain and reject the translation |
1583 | + if 'ids' in self.template.supported_ids[id]: |
1584 | + uvars = sorted([e for e in re.split('(<ph name=".*?"/>)', self.template.supported_ids[id]['ids'][0]['val']) \ |
1585 | + if re.match('^<ph name=".*?"/>$', e)]) |
1586 | + tvars = sorted([e for e in re.split('(<ph name=".*?"/>)', string['translation'])\ |
1587 | + if re.match('^<ph name=".*?"/>$', e)]) |
1588 | + lostvars = list(set(uvars).difference(set(tvars))) |
1589 | + createdvars = list(set(tvars).difference(set(uvars))) |
1590 | + if len(lostvars) or len(createdvars): |
1591 | + template = os.path.splitext(os.path.basename(self.template.filename))[0].replace('_', '-') |
1592 | + self.errors += 1 |
1593 | + if self.html_output: |
1594 | + print "<div class='error'>[<a id='pherr-%s-%d' href='javascript:toggle(\"pherr-%s-%d\");'>+</a>] " \ |
1595 | + "<b>ERROR</b>: Found mismatching placeholder variables in string id %s of <b>%s</b> lang <b>%s</b>" % \ |
1596 | + (template, self.errors, template, self.errors, id, template, lang) |
1597 | + else: |
1598 | + print "ERROR: Found mismatching placeholder variables in string id %s of %s/%s:" % \ |
1599 | + (id, template, lang) |
1600 | + url = 'https://translations.launchpad.net/chromium-browser/translations/+pots/%s/%s/+translate?batch=10&show=all&search=%s' % \ |
1601 | + (template, self.template.mapped_langs[lang]['gettext'], urllib.quote(gt_translation.encode('utf-8'))) |
1602 | + if self.html_output: |
1603 | + print "<div id='pherr-%s-%d-t' style='display: none'>\n" \ |
1604 | + "<fieldset><legend>Details</legend><p><ul>" % (template, self.errors) |
1605 | + print "<li> <a href='%s'>this string in Launchpad</a>\n" % url |
1606 | + if len(lostvars): |
1607 | + print " <li> expected but not found: <code>%s</code>" % " ".join([ re.sub(r'<ph name="(.*?)"/>', r'%{\1}', s) for s in lostvars ]) |
1608 | + if len(createdvars): |
1609 | + print " <li> found but not expected: <code>%s</code>" % " ".join([ re.sub(r'<ph name="(.*?)"/>', r'%{\1}', s) for s in createdvars ]) |
1610 | + print "</ul><table border='1'>" \ |
1611 | + "<tr><th rowspan='2'>GetText</th><th>template</th><td><code>%s</code></td></tr>\n" \ |
1612 | + "<tr><th>translation</th><td><code>%s</code></td></tr>\n" \ |
1613 | + "<tr><th rowspan='2'>Grit</th><th>template</th><td><code>%s</code></td></tr>\n" \ |
1614 | + "<tr><th>translation</th><td><code>%s</code></td></tr>\n" \ |
1615 | + "</table><p> => <b>translation skipped</b>\n" % \ |
1616 | + (string['string'][1:-1].replace('"\n"', '').replace('<', '<').replace('>', '>'), |
1617 | + gt_translation.replace('<', '<').replace('>', '>'), |
1618 | + self.template.supported_ids[id]['ids'][0]['val'].replace('<', '<').replace('>', '>'), |
1619 | + string['translation'].replace('<', '<').replace('>', '>')) |
1620 | + print "</fieldset></div></div>" |
1621 | + else: |
1622 | + if len(lostvars): |
1623 | + print " - expected but not found: " + " ".join(lostvars) |
1624 | + if len(createdvars): |
1625 | + print " - found but not expected: " + " ".join(createdvars) |
1626 | + print " string: '%s'\n translation: '%s'\n gettext: '%s'\n url: %s\n => translation skipped\n" % \ |
1627 | + (self.template.supported_ids[id]['ids'][0]['val'], string['translation'], gt_translation, url) |
1628 | + continue |
1629 | + # check if the translated string is the same |
1630 | + if 'lang' in self.template.supported_ids[id] and \ |
1631 | + lang in self.template.supported_ids[id]['lang']: |
1632 | + # compare |
1633 | + if self.compare_translations(self.template.supported_ids[id]['lang'][lang], |
1634 | + string['translation'], id, lang): |
1635 | + continue # it's the same |
1636 | + if id in self.translations[lang].strings: |
1637 | + # already added from a previously merged gettext po file |
1638 | + if self.debug: |
1639 | + print "already added from a previously merged gettext po file for" + \ |
1640 | + " template %s %s id %s in lang %s: %s" % \ |
1641 | + (self.template.branch_name, self.template.filename, |
1642 | + id, lang, repr(string['translation'])) |
1643 | + # compare |
1644 | + if self.compare_translations(self.translations[lang].strings[id], |
1645 | + string['translation'], id, lang): |
1646 | + continue # it's the same |
1647 | + # update it.. |
1648 | + if self.debug: |
1649 | + print "updated string for template %s %s id %s in lang %s: %s" % \ |
1650 | + (self.template.branch_name, self.template.filename, id, lang, |
1651 | + repr(string['translation'])) |
1652 | + self.template.update_stats(lang, updated = 1) |
1653 | + self.translations[lang].strings[id] = string['translation'] |
1654 | + self.translations[lang].strings_updated += 1 |
1655 | + elif id in self.translations[lang].strings: |
1656 | + # already added from a previously merged gettext po file |
1657 | + if self.debug: |
1658 | + print "already added from a previously merged gettext po file for" + \ |
1659 | + "template %s %s id %s in lang %s: %s" % \ |
1660 | + (self.template.branch_name, self.template.filename, |
1661 | + id, lang, repr(string['translation'])) |
1662 | + # compare |
1663 | + if self.compare_translations(self.translations[lang].strings[id], |
1664 | + string['translation'], id, lang): |
1665 | + continue # it's the same |
1666 | + # update it.. |
1667 | + self.translations[lang].strings[id] = string['translation'] |
1668 | + else: |
1669 | + # add |
1670 | + if self.debug: |
1671 | + print "add new string for template %s %s id %s in lang %s: %s" % \ |
1672 | + (self.template.branch_name, self.template.filename, |
1673 | + id, lang, repr(string['translation'])) |
1674 | + self.template.update_stats(lang, new = 1) |
1675 | + self.translations[lang].strings[id] = string['translation'] |
1676 | + self.translations[lang].strings_new += 1 |
1677 | + if self.debug and lp669831_skipped > 0: |
1678 | + print "lp669831: skipped %s bogus/obsolete strings from %s" % \ |
1679 | + (lp669831_skipped, filename[filename[:filename.rfind('/')].rfind('/') + 1:]) |
1680 | + |
1681 | + def import_gettext_po_files(self, directory): |
1682 | + fname = self.file_mapping['grd']['mapped_grd'] \ |
1683 | + if 'mapped_grd' in self.file_mapping['grd'] else self.template.filename |
1684 | + template_name = os.path.splitext(os.path.basename(fname))[0] |
1685 | + directory = os.path.join(directory, template_name) |
1686 | + if not os.path.isdir(directory): |
1687 | + if self.debug: |
1688 | + print "WARN: Launchpad didn't export anything for template '%s' [%s]" % (template_name, directory) |
1689 | + return |
1690 | + for file in os.listdir(directory): |
1691 | + base, ext = os.path.splitext(file) |
1692 | + if ext != ".po": |
1693 | + continue |
1694 | + # 'base' is a gettext lang, map it |
1695 | + lang = None |
1696 | + for l in self.template.mapped_langs: |
1697 | + if base == self.template.mapped_langs[l]['gettext']: |
1698 | + lang = l |
1699 | + break |
1700 | + if lang is None: # most probably a new lang, map back |
1701 | + lang = base.replace('_', '-') |
1702 | + for l in self.lang_mapping: |
1703 | + if lang == self.lang_mapping[l]: |
1704 | + lang = l |
1705 | + break |
1706 | + flang = lang.replace('@', '-') |
1707 | + self.template.mapped_langs[lang] = { 'xtb_file': flang, 'grit': lang, 'gettext': base } |
1708 | + self.import_gettext_po_file(lang, os.path.join(directory, file)) |
1709 | + # remove from the supported langs list all langs with no translated strings |
1710 | + # (to catch either empty 'po' files exported by Launchpad, or 'po' files |
1711 | + # containing only obsolete or too new strings for this branch) |
1712 | + dropped = [] |
1713 | + for lang in self.translations: |
1714 | + if len(self.translations[lang].strings.keys()) == 0: |
1715 | + if self.debug: |
1716 | + print "no translation found for template '%s' and lang '%s'. lang removed from the supported lang list" % \ |
1717 | + (os.path.splitext(os.path.basename(self.template.filename))[0], lang) |
1718 | + del(self.template.supported_langs[lang]) |
1719 | + dropped.append(lang) |
1720 | + for lang in dropped: |
1721 | + del(self.translations[lang]) |
1722 | + |
1723 | + def copy_grit_files(self, directory): |
1724 | + fname = self.file_mapping['grd']['mapped_grd'] \ |
1725 | + if 'mapped_grd' in self.file_mapping['grd'] else self.template.filename |
1726 | + dst = os.path.join(directory, os.path.dirname(fname)) |
1727 | + if not os.path.isdir(dst): |
1728 | + os.makedirs(dst, 0755) |
1729 | + shutil.copy2(fname, dst) |
1730 | + for lang in self.template.supported_langs: |
1731 | + dst = os.path.join(directory, os.path.dirname(self.translations[lang].filename)) |
1732 | + if not os.path.isdir(dst): |
1733 | + os.makedirs(dst, 0755) |
1734 | + shutil.copy2(self.translations[lang].filename, dst) |
1735 | + |
1736 | + def create_patches(self, directory): |
1737 | + if not os.path.isdir(directory): |
1738 | + os.makedirs(directory, 0755) |
1739 | + template_name = os.path.splitext(os.path.basename(self.template.filename))[0] |
1740 | + patch = codecs.open(os.path.join(directory, "translations-" + template_name + ".patch"), |
1741 | + "wb", encoding="utf-8") |
1742 | + for e in sorted(self.file_mapping.keys()): |
1743 | + if 'dst' not in self.file_mapping[e]: |
1744 | + self.file_mapping[e]['dst'] = None |
1745 | + if self.file_mapping[e]['src'] is not None and \ |
1746 | + self.file_mapping[e]['dst'] is not None and \ |
1747 | + filecmp.cmp(self.file_mapping[e]['src'], self.file_mapping[e]['dst']) == True: |
1748 | + continue # files are the same |
1749 | + |
1750 | + if self.file_mapping[e]['src'] is not None: |
1751 | + fromfile = "old/" + self.file_mapping[e]['src'] |
1752 | + tofile = "new/" + self.file_mapping[e]['src'] |
1753 | + fromdate = datetime.fromtimestamp(self.template.get_mtime( |
1754 | + self.file_mapping[e]['src'])).strftime("%Y-%m-%d %H:%M:%S.%f000 +0000") |
1755 | + fromlines = codecs.open(self.file_mapping[e]['src'], 'rb', encoding="utf-8").readlines() |
1756 | + else: |
1757 | + fromfile = "old/" + self.file_mapping[e]['dst'][len(self.file_mapping['grd']['dir']) + 1:] |
1758 | + tofile = "new/" + self.file_mapping[e]['dst'][len(self.file_mapping['grd']['dir']) + 1:] |
1759 | + fromdate = datetime.fromtimestamp(0).strftime("%Y-%m-%d %H:%M:%S.%f000 +0000") |
1760 | + fromlines = "" |
1761 | + if self.file_mapping[e]['dst'] is not None: |
1762 | + todate = datetime.fromtimestamp(self.template.get_mtime( |
1763 | + self.file_mapping[e]['dst'])).strftime("%Y-%m-%d %H:%M:%S.%f000 +0000") |
1764 | + tolines = codecs.open(self.file_mapping[e]['dst'], 'rb', encoding="utf-8").readlines() |
1765 | + else: |
1766 | + todate = datetime.fromtimestamp(0).strftime("%Y-%m-%d %H:%M:%S.%f000 +0000") |
1767 | + tolines = "" |
1768 | + diff = unified_diff(fromlines, tolines, fromfile, tofile, |
1769 | + fromdate, todate, n=3) |
1770 | + patch.write("diff -Nur %s %s\n" % (fromfile, tofile)) |
1771 | + s = ''.join(diff) |
1772 | + # fix the diff so that older patch (<< 2.6) don't fail on new files |
1773 | + s = re.sub(r'@@ -1,0 ', '@@ -0,0 ', s) |
1774 | + # ..and make sure patch is able to detect a patch removing files |
1775 | + s = re.sub(r'(@@ \S+) \+1,0 @@', '\\1 +0,0 @@', s) |
1776 | + patch.writelines(s) |
1777 | + if s[-1:] != '\n': |
1778 | + patch.write("\n\\ No newline at end of file\n") |
1779 | + patch.close() |
1780 | + |
1781 | + def update_supported_langs_in_grd(self, grd_in, grd_out, langs): |
1782 | + fdi = codecs.open(grd_in, 'rb', encoding="utf-8") |
1783 | + fdo = codecs.open(grd_out, 'wb', encoding="utf-8") |
1784 | + # can't use minidom here as the file is manually generated and the |
1785 | + # output will create big diffs. parse the source file line by line |
1786 | + # and insert new langs in the <outputs> section (with type="data_package" |
1787 | + # or type="js_map_format"). Let everything else untouched |
1788 | + # FIXME: this is mostly a copy of GrdFile::export_file() |
1789 | + pak_found = False |
1790 | + pak_saved = [] |
1791 | + has_ifs = False |
1792 | + for line in fdi.readlines(): |
1793 | + if re.match(r'.*?<output filename=".*?" type="(data_package|js_map_format)"', line): |
1794 | + pak_found = True |
1795 | + pak_saved.append(line) |
1796 | + continue |
1797 | + if line.find('</outputs>') > 0: |
1798 | + pak_found = False |
1799 | + ours = langs[:] |
1800 | + chunks = {} |
1801 | + c = None |
1802 | + pak_if = None |
1803 | + pak_is_in_if = False |
1804 | + for l in pak_saved: |
1805 | + if l.find("<!-- ") > 0: |
1806 | + c = l |
1807 | + continue |
1808 | + if l.find("<if ") > -1: |
1809 | + c = l if c is None else c + l |
1810 | + has_ifs = True |
1811 | + pak_is_in_if = True |
1812 | + continue |
1813 | + if l.find("</if>") > -1: |
1814 | + c = l if c is None else c + l |
1815 | + pak_is_in_if = False |
1816 | + continue |
1817 | + m = re.match(r'.*?<output filename="(.*?)_([^_\.]+)\.(pak|js)" type="(data_package|js_map_format)" lang="(.*?)" />', l) |
1818 | + if m is not None: |
1819 | + x = { 'name': m.group(1), 'ext': m.group(3), 'lang': m.group(5), 'file_lang': m.group(2), |
1820 | + 'type': m.group(4), 'in_if': pak_is_in_if, 'line': l } |
1821 | + if c is not None: |
1822 | + x['comment'] = c |
1823 | + c = None |
1824 | + k = m.group(2) if m.group(2) != 'nb' else 'no' |
1825 | + chunks[k] = x |
1826 | + else: |
1827 | + if c is None: |
1828 | + c = l |
1829 | + else: |
1830 | + c += l |
1831 | + is_in_if = False |
1832 | + for lang in sorted(chunks.keys()): |
1833 | + tlang = lang if lang != 'no' else 'nb' |
1834 | + while len(ours) > 0 and ((ours[0] == 'nb' and 'no' < tlang) or (ours[0] != 'nb' and ours[0] < tlang)): |
1835 | + if ours[0] in chunks: |
1836 | + ours = ours[1:] |
1837 | + continue |
1838 | + if has_ifs and is_in_if is False: |
1839 | + fdo.write(' <if expr="pp_ifdef(\'use_third_party_translations\')">\n') |
1840 | + f = "%s_%s.%s" % (chunks[lang]['name'], ours[0], chunks[lang]['ext']) |
1841 | + fdo.write(' %s<output filename="%s" type="%s" lang="%s" />\n' % \ |
1842 | + (' ' if has_ifs else '', f, chunks[lang]['type'], ours[0])) |
1843 | + is_in_if = True |
1844 | + if has_ifs and chunks[lang]['in_if'] is False: |
1845 | + if 'comment' not in chunks[lang] or chunks[lang]['comment'].find('</if>') == -1: |
1846 | + fdo.write(' </if>\n') |
1847 | + is_in_if = False |
1848 | + ours = ours[1:] |
1849 | + if 'comment' in chunks[lang]: |
1850 | + for s in chunks[lang]['comment'].split('\n')[:-1]: |
1851 | + if chunks[lang]['in_if'] is True and is_in_if and s.find('<if ') > -1: |
1852 | + continue |
1853 | + if s.find('<!-- No translations available. -->') > -1: |
1854 | + continue |
1855 | + fdo.write(s + '\n') |
1856 | + fdo.write(chunks[lang]['line']) |
1857 | + if ours[0] == tlang: |
1858 | + ours = ours[1:] |
1859 | + is_in_if = chunks[lang]['in_if'] |
1860 | + if len(chunks.keys()) > 0: |
1861 | + while len(ours) > 0: |
1862 | + f = "%s_%s.%s" % (chunks[lang]['name'], ours[0], chunks[lang]['ext']) |
1863 | + if has_ifs and is_in_if is False: |
1864 | + fdo.write(' <if expr="pp_ifdef(\'use_third_party_translations\')">\n') |
1865 | + fdo.write(' %s<output filename="%s" type="data_package" lang="%s" />\n' % \ |
1866 | + (' ' if has_ifs else '', f, ours[0])) |
1867 | + is_in_if = True |
1868 | + ours = ours[1:] |
1869 | + if has_ifs and is_in_if: |
1870 | + fdo.write(' </if>\n') |
1871 | + is_in_if = False |
1872 | + if c is not None: |
1873 | + for s in c.split('\n')[:-1]: |
1874 | + if s.find('<!-- No translations available. -->') > -1: |
1875 | + continue |
1876 | + if s.find('</if>') > -1: |
1877 | + continue |
1878 | + fdo.write(s + '\n') |
1879 | + if pak_found: |
1880 | + pak_saved.append(line) |
1881 | + continue |
1882 | + fdo.write(line) |
1883 | + fdi.close() |
1884 | + fdo.close() |
1885 | + |
1886 | + def create_build_gyp_patch(self, directory, build_gyp_file, other_grd_files, nlangs, |
1887 | + whitelisted_new_langs = None): |
1888 | + # read the list of langs supported upstream |
1889 | + fd = open(build_gyp_file, "r") |
1890 | + data = fd.read() |
1891 | + fd.close() |
1892 | + r = data[data.find("'locales':"):] |
1893 | + olangs = sorted(re.findall("'(.*?)'", r[r.find('['):r.find(']')])) |
1894 | + # check for an optional use_third_party_translations list of locales |
1895 | + tpt = data.find('use_third_party_translations==1') |
1896 | + if tpt > 0: |
1897 | + tpt += data[tpt:].find("'locales':") |
1898 | + r = data[tpt:] |
1899 | + tptlangs = sorted(re.findall("'(.*?)'", r[r.find('['):r.find(']')])) |
1900 | + if nlangs == sorted(tptlangs + olangs): |
1901 | + return tptlangs |
1902 | + else: |
1903 | + if nlangs == olangs: |
1904 | + return [] |
1905 | + # check if we need to only activate some whitelisted new langs |
1906 | + xlangs = None |
1907 | + nnlangs = [ x for x in nlangs if x not in olangs ] |
1908 | + if whitelisted_new_langs is not None: |
1909 | + if tpt > 0: |
1910 | + nlangs = [ x for x in nlangs if x not in olangs and x in whitelisted_new_langs ] |
1911 | + else: |
1912 | + xlangs = [ x for x in nlangs if x not in olangs and x not in whitelisted_new_langs ] |
1913 | + nlangs = [ x for x in nlangs if x in olangs or x in whitelisted_new_langs ] |
1914 | + elif tpt > 0: |
1915 | + nlangs = [ x for x in nlangs if x not in olangs ] |
1916 | + |
1917 | + # we need a patch |
1918 | + if tpt > 0: |
1919 | + pos = tpt + data[tpt:].find('[') |
1920 | + end = data[:pos + 1] |
1921 | + ndata = end[:] |
1922 | + else: |
1923 | + pos = data.find("'locales':") |
1924 | + begin = data[pos:] |
1925 | + end = data[:pos + begin.find('\n')] |
1926 | + ndata = end[:] |
1927 | + end = data[pos + data[pos:].find(']'):] |
1928 | + |
1929 | + # list of langs, folded |
1930 | + if len(nlangs) > 9: |
1931 | + ndata += '\n' + \ |
1932 | + '\n'.join(textwrap.wrap("'" + "', '".join(nlangs) + "'", |
1933 | + break_long_words=False, width=76, |
1934 | + drop_whitespace=False, |
1935 | + expand_tabs=False, |
1936 | + replace_whitespace=False, |
1937 | + initial_indent=' ', |
1938 | + subsequent_indent=' ', |
1939 | + break_on_hyphens=False)) + '\n ' |
1940 | + else: |
1941 | + ndata += "'%s'" % "', '".join(nlangs) |
1942 | + |
1943 | + ndata += end |
1944 | + |
1945 | + # write the patch |
1946 | + fromfile = "old/" + build_gyp_file |
1947 | + tofile = "new/" + build_gyp_file |
1948 | + fromdate = datetime.fromtimestamp(self.template.get_mtime(build_gyp_file)).strftime("%Y-%m-%d %H:%M:%S.%f000 +0000") |
1949 | + fromlines = [ x for x in re.split('(.*\n?)', data) if x != '' ] |
1950 | + todate = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f000 +0000") |
1951 | + tolines = [ x for x in re.split('(.*\n?)', ndata) if x != '' ] |
1952 | + patch = codecs.open(os.path.join(directory, "build.patch"), "wb", encoding="utf-8") |
1953 | + diff = unified_diff(fromlines, tolines, fromfile, tofile, fromdate, todate, n=3) |
1954 | + patch.write("diff -Nur %s %s\n" % (fromfile, tofile)) |
1955 | + patch.writelines(''.join(diff)) |
1956 | + |
1957 | + for grd in other_grd_files: |
1958 | + grd_out = os.path.join(directory, os.path.basename(grd)) |
1959 | + self.update_supported_langs_in_grd(grd, grd_out, langs) |
1960 | + if filecmp.cmp(grd, grd_out) == True: |
1961 | + os.unlink(grd_out) |
1962 | + continue # files are the same |
1963 | + # add it to the patch |
1964 | + fromfile = "old/" + grd |
1965 | + tofile = "new/" + grd |
1966 | + fromdate = datetime.fromtimestamp(self.template.get_mtime(grd)).strftime("%Y-%m-%d %H:%M:%S.%f000 +0000") |
1967 | + fromlines = codecs.open(grd, 'rb', encoding="utf-8").readlines() |
1968 | + todate = datetime.now().strftime("%Y-%m-%d %H:%M:%S.%f000 +0000") |
1969 | + tolines = codecs.open(grd_out, 'rb', encoding="utf-8").readlines() |
1970 | + diff = unified_diff(fromlines, tolines, fromfile, tofile, fromdate, todate, n=3) |
1971 | + patch.write("diff -Nur %s %s\n" % (fromfile, tofile)) |
1972 | + patch.writelines(''.join(diff)) |
1973 | + os.unlink(grd_out) |
1974 | + patch.close() |
1975 | + return nnlangs |
1976 | + |
1977 | +def usage(): |
1978 | + print """ |
1979 | +Usage: %s [options] [grd_file [more_grd_files]] |
1980 | + |
1981 | + Convert Chromium translation files (grd/xtb) into gettext files (pot/po) and back |
1982 | + |
1983 | + options could be: |
1984 | + -d | --debug debug mode |
1985 | + -v | --verbose verbose mode |
1986 | + -h | --help this help screen |
1987 | + |
1988 | + --export-gettext dir |
1989 | + export pot/po gettext files to dir |
1990 | + |
1991 | + --import-gettext dir[,dir2][...] |
1992 | + import gettext pot/po files from those directories. |
1993 | + Directories must be ordered from the oldest to |
1994 | + the freshest. Only strings different from the grit |
1995 | + (upstream) translations are considered. |
1996 | + |
1997 | + --import-grit-branch name:dir:grd1[,grd2,...]] |
1998 | + import the Grit files for this branch from this |
1999 | + directory. --import-grit-branch could be used several |
2000 | + times, and then, branches must be specified from the |
2001 | + freshest (trunk) to the more stable ones. |
2002 | + The default value is trunk:<cwd> |
2003 | + Note: must not be used along with --export-grit |
2004 | + |
2005 | + --export-grit dir |
2006 | + export grd/xtb grit files to dir |
2007 | + |
2008 | + --copy-grit dir copy the src grit files containing strings to dir |
2009 | + (useful to create diffs after --export-grit) |
2010 | + |
2011 | + --whitelisted-new-langs lang1[,lang2][..] |
2012 | + comma separated list of new langs that have to be enabled |
2013 | + (assuming they have some strings translated). The default |
2014 | + is to enable all new langs, but for stable builds, a good |
2015 | + enough coverage is preferred |
2016 | + |
2017 | + --create-patches dir |
2018 | + create unified patches per template in dir |
2019 | + (only useful after --export-grit) |
2020 | + |
2021 | + --build-gyp-file file |
2022 | + location of the build/common.gypi file, used only |
2023 | + with --create-patches to add all new langs |
2024 | + for which we merged translated strings |
2025 | + |
2026 | + --other-grd-files file1[,file2][..] |
2027 | + comma separated list of grd files to also patch |
2028 | + to add new langs for (see --build-gyp-file) |
2029 | + |
2030 | + --html-output produce nice some HTML as output (on stdout) |
2031 | + |
2032 | + --json-branches-info file |
2033 | + location of a json file containing the url, revision |
2034 | + and last date of both the upstream branch and |
2035 | + launchpad export. optionally used in the html output |
2036 | + |
2037 | + --map-template-names new1=old1[,new2=old2][...] |
2038 | + comma separated list of template names mappings. |
2039 | + It is useful when upstream renames a grd file in a branch |
2040 | + to preserve the old name in gettext for the older branches |
2041 | + |
2042 | + --landable-templates template1[,template2][...] |
2043 | + comma separated list of templates that are landable upstream |
2044 | + for all langs |
2045 | + |
2046 | + --unlandable-templates template1[,template2][...] |
2047 | + comma separated list of templates that are not landable upstream, |
2048 | + even for new langs |
2049 | + |
2050 | + --test-strcvt run the grit2gettext2grit checker |
2051 | + --test-conditions run the conditions evaluation checker |
2052 | + |
2053 | +""" % sys.argv[0].rpartition('/')[2] |
2054 | + |
2055 | +if '__main__' == __name__: |
2056 | + sys.stdout = codecs.getwriter('utf8')(sys.stdout) |
2057 | + try: |
2058 | + opts, args = getopt.getopt(sys.argv[1:], "dhv", |
2059 | + [ "test-strcvt", "test-conditions", "debug", "verbose", "help", "copy-grit=", |
2060 | + "import-grit-branch=", "export-gettext=", "import-gettext=", "export-grit=", |
2061 | + "create-patches=", "build-gyp-file=", "other-grd-files=", |
2062 | + "landable-templates=", "unlandable-templates=", "map-template-names=", |
2063 | + "whitelisted-new-langs=", "html-output", "json-branches-info=" ]) |
2064 | + except getopt.GetoptError, err: |
2065 | + print str(err) |
2066 | + usage() |
2067 | + sys.exit(2) |
2068 | + |
2069 | + verbose = False |
2070 | + debug = False |
2071 | + html_output = False |
2072 | + outdir = None |
2073 | + indir = None |
2074 | + export_gettext = None |
2075 | + import_gettext = None |
2076 | + export_grit = None |
2077 | + copy_grit = None |
2078 | + create_patches = None |
2079 | + build_gyp_file = None |
2080 | + json_info = None |
2081 | + other_grd_files = [] |
2082 | + whitelisted_new_langs = None |
2083 | + templatenames_mapping = {} |
2084 | + landable_templates = [] |
2085 | + unlandable_templates = [] |
2086 | + branches = None |
2087 | + nbranches = [] |
2088 | + for o, a in opts: |
2089 | + if o in ("-v", "--verbose"): |
2090 | + verbose = True |
2091 | + elif o in ("-h", "--help"): |
2092 | + usage() |
2093 | + sys.exit() |
2094 | + elif o in ("-d", "--debug"): |
2095 | + debug = True |
2096 | + elif o == "--import-grit-branch": |
2097 | + if branches is None: |
2098 | + branches = {} |
2099 | + branch, dir, grds = a.split(':') |
2100 | + for grd in grds.split(','): |
2101 | + name = os.path.basename(grd) |
2102 | + if name not in branches: |
2103 | + branches[name] = [] |
2104 | + branches[name].append({ 'branch': branch, 'dir': dir, 'grd': grd }) |
2105 | + if branch not in nbranches: |
2106 | + nbranches.append(branch) |
2107 | + elif o == "--export-gettext": |
2108 | + export_gettext = a |
2109 | + elif o == "--import-gettext": |
2110 | + import_gettext = a.split(",") |
2111 | + elif o == "--export-grit": |
2112 | + export_grit = a |
2113 | + elif o == "--copy-grit": |
2114 | + copy_grit = a |
2115 | + elif o == "--whitelisted-new-langs": |
2116 | + whitelisted_new_langs = a.split(",") |
2117 | + elif o == "--create-patches": |
2118 | + create_patches = a |
2119 | + elif o == "--build-gyp-file": |
2120 | + build_gyp_file = a |
2121 | + elif o == "--other-grd-files": |
2122 | + other_grd_files = a.split(',') |
2123 | + elif o == "--html-output": |
2124 | + html_output = True |
2125 | + elif o == "--json-branches-info": |
2126 | + json_info = a |
2127 | + elif o == "--landable-templates": |
2128 | + landable_templates = a.split(",") |
2129 | + elif o == "--unlandable-templates": |
2130 | + unlandable_templates = a.split(",") |
2131 | + elif o == "--map-template-names": |
2132 | + for c in a.split(','): |
2133 | + x = c.split('=') |
2134 | + templatenames_mapping[x[0]] = x[1] |
2135 | + elif o == "--test-strcvt": |
2136 | + StringCvt().test() |
2137 | + sys.exit() |
2138 | + elif o == "--test-conditions": |
2139 | + EvalConditions().test() |
2140 | + sys.exit() |
2141 | + else: |
2142 | + assert False, "unhandled option" |
2143 | + |
2144 | + if branches is None and len(args) != 0: |
2145 | + branches = {} |
2146 | + for arg in args: |
2147 | + branches[os.path.basename(arg)] = [ { 'branch': 'default', 'dir': os.getcwd(), 'grd': arg } ] |
2148 | + if branches is None: |
2149 | + print "Please specify at least one grd file or use --import-grit-branch" |
2150 | + usage() |
2151 | + sys.exit(2) |
2152 | + |
2153 | + # re-map the templates, if needed |
2154 | + for grd in templatenames_mapping.keys(): |
2155 | + new = os.path.basename(grd) |
2156 | + old = os.path.basename(templatenames_mapping[grd]) |
2157 | + if new in branches: |
2158 | + if old not in branches: |
2159 | + branches[old] = [] |
2160 | + for branch in branches[new]: |
2161 | + branch['mapped_grd'] = old |
2162 | + branches[old].extend(branches[new]) |
2163 | + # re-sort the branches |
2164 | + branches[old] = sorted(branches[old], lambda x,y: cmp(nbranches.index(x['branch']), nbranches.index(y['branch']))) |
2165 | + del(branches[new]) |
2166 | + |
2167 | + if html_output: |
2168 | + print """\ |
2169 | +<html> |
2170 | +<head><meta charset="UTF-8"> |
2171 | +</head><body> |
2172 | +<style type="text/css"> |
2173 | +body { |
2174 | + font-family: UbuntuBeta,Ubuntu,"Bitstream Vera Sans","DejaVu Sans",Tahoma,sans-serif; |
2175 | +} |
2176 | +div#legend { |
2177 | + float: left; |
2178 | +} |
2179 | +fieldset { |
2180 | + border-width: 1px; |
2181 | + border-color: #f0f0f0; |
2182 | +} |
2183 | +div#legend fieldset, div#branches fieldset { |
2184 | + border-width: 0px; |
2185 | +} |
2186 | +legend { |
2187 | + font-size: 80%; |
2188 | +} |
2189 | +div#branches { |
2190 | + float: left; |
2191 | + padding-left: 40px; |
2192 | +} |
2193 | +div#branches td { |
2194 | + padding-right: 5px; |
2195 | +} |
2196 | +div#stats { |
2197 | + padding-top: 5px; |
2198 | + clear: both; |
2199 | +} |
2200 | +a { |
2201 | + text-decoration: none; |
2202 | +} |
2203 | +a.l:link, a.l:visited { |
2204 | + color: black; |
2205 | +} |
2206 | +.error { |
2207 | + font-size: 90%; |
2208 | +} |
2209 | +div.error a { |
2210 | + font-family: monospace; |
2211 | + font-size: 120%; |
2212 | +} |
2213 | +table { |
2214 | + border-collapse: collapse; |
2215 | + border-spacing: 1px; |
2216 | + font-size: 0.9em; |
2217 | +} |
2218 | +th { |
2219 | + font-weight: bold; |
2220 | + color: #666; |
2221 | + padding-right: 5px; |
2222 | +} |
2223 | +th, td { |
2224 | + border: 1px #d2d2d2; |
2225 | + border-style: solid; |
2226 | + padding-left: 4px; |
2227 | + padding-top: 0px; |
2228 | + padding-bottom: 0px; |
2229 | +} |
2230 | +td.d { |
2231 | + font-size: 90%; |
2232 | + text-align: right; |
2233 | +} |
2234 | +td.n { |
2235 | + background: #FFA; |
2236 | +} |
2237 | +.lang { |
2238 | + font-weight: bold; |
2239 | + padding-left: 0.5em; |
2240 | + padding-right: 0.5em; |
2241 | + white-space: nowrap; |
2242 | +} |
2243 | +.progress_bar { |
2244 | + width: 100px; overflow: hidden; position: relative; padding: 0px; |
2245 | +} |
2246 | +.pb_label { |
2247 | + text-align: center; width: 100%; |
2248 | + position: absolute; z-index: 1001; left: 4px; top: -2px; color: white; font-size: 0.7em; |
2249 | +} |
2250 | +.pb_label2 { |
2251 | + text-align: center; width: 100%; |
2252 | + position: absolute; z-index: 1000; left: 5px; top: -1px; color: black; font-size: 0.7em; |
2253 | +} |
2254 | +.green_gradient { |
2255 | + height: 1em; position: relative; float: left; |
2256 | + background: #00ff00; |
2257 | + background: -moz-linear-gradient(top, #00ff00, #007700); |
2258 | + background: -webkit-gradient(linear, left top, left bottom, from(#00ff00), to(#007700)); |
2259 | + filter: progid:DXImageTransform.Microsoft.Gradient(StartColorStr='#00ff00', EndColorStr='#007700', GradientType=0); |
2260 | +} |
2261 | +.red_gradient { |
2262 | + height: 1em; position: relative; float: left; |
2263 | + background: #ff8888; |
2264 | + background: -moz-linear-gradient(top, #ff8888, #771111); |
2265 | + background: -webkit-gradient(linear, left top, left bottom, from(#ff8888), to(#771111)); |
2266 | + filter: progid:DXImageTransform.Microsoft.Gradient(StartColorStr='#ff8888', EndColorStr='#771111', GradientType=0); |
2267 | +} |
2268 | +.blue_gradient { |
2269 | + height: 1em; position: relative; float: left; |
2270 | + background: #62b0dd; |
2271 | + background: -moz-linear-gradient(top, #62b0dd, #1f3d4a); |
2272 | + background: -webkit-gradient(linear, left top, left bottom, from(#62b0dd), to(#1f3d4a)); |
2273 | + filter: progid:DXImageTransform.Microsoft.Gradient(StartColorStr='#62b0dd', EndColorStr='#1f3d4a', GradientType=0); |
2274 | +} |
2275 | +.purple_gradient { |
2276 | + height: 1em; position: relative; float: left; |
2277 | + background: #b8a4ba; |
2278 | + background: -moz-linear-gradient(top, #b8a4ba, #5c3765); |
2279 | + background: -webkit-gradient(linear, left top, left bottom, from(#b8a4ba), to(#5c3765)); |
2280 | + filter: progid:DXImageTransform.Microsoft.Gradient(StartColorStr='#b8a4ba', EndColorStr='#5c3765', GradientType=0); |
2281 | +} |
2282 | +</style> |
2283 | +<script type="text/javascript" language="javascript"> |
2284 | +function progress_bar(where, red, green, purple, blue) { |
2285 | + var total = green + red + blue + purple; |
2286 | + if (total == 0) |
2287 | + total = 1; |
2288 | + var d = document.getElementById(where); |
2289 | + var v = 100 * (1 - (red / total)); |
2290 | + if (total != 1) { |
2291 | + d.innerHTML += '<div class="pb_label">' + v.toFixed(1) + "%</div>"; |
2292 | + d.innerHTML += '<div class="pb_label2">' + v.toFixed(1) + "%</div>"; |
2293 | + } |
2294 | + else |
2295 | + d.style.width = "25px"; |
2296 | + var pgreen = parseInt(100 * green / total); |
2297 | + var pblue = parseInt(100 * blue / total); |
2298 | + var ppurple = parseInt(100 * purple / total); |
2299 | + var pred = parseInt(100 * red / total); |
2300 | + if (pgreen + pblue + ppurple + pred != 100) { |
2301 | + if (red > 0) |
2302 | + pred = 100 - pgreen - pblue - ppurple; |
2303 | + else if (purple > 0) |
2304 | + ppurple = 100 - pgreen - pblue; |
2305 | + else if (blue > 0) |
2306 | + pblue = 100 - pgreen; |
2307 | + else |
2308 | + pgreen = 100; |
2309 | + } |
2310 | + if (green > 0) |
2311 | + d.innerHTML += '<div class="green_gradient" style="width:' + pgreen + '%;"></div>'; |
2312 | + if (blue > 0) |
2313 | + d.innerHTML += '<div class="blue_gradient" style="width:' + pblue + '%;"></div>'; |
2314 | + if (purple > 0) |
2315 | + d.innerHTML += '<div class="purple_gradient" style="width:' + ppurple + '%;"></div>'; |
2316 | + if (red > 0) |
2317 | + d.innerHTML += '<div class="red_gradient" style="width:' + pred + '%;"></div>'; |
2318 | + return true; |
2319 | +} |
2320 | + |
2321 | +function toggle(e) { |
2322 | + var elt = document.getElementById(e + "-t"); |
2323 | + var text = document.getElementById(e); |
2324 | + if (elt.style.display == "block") { |
2325 | + elt.style.display = "none"; |
2326 | + text.innerHTML = "+"; |
2327 | + } |
2328 | + else { |
2329 | + elt.style.display = "block"; |
2330 | + text.innerHTML = "-"; |
2331 | + } |
2332 | +} |
2333 | + |
2334 | +function time_delta(date, e) { |
2335 | + var now = new Date(); |
2336 | + var d = new Date(date); |
2337 | + var delta = (now - d) / 1000; |
2338 | + var elt = document.getElementById(e); |
2339 | + if (delta >= 3600) { |
2340 | + var h = parseInt(delta / 3600); |
2341 | + var m = parseInt((delta - h * 3600) / 60); |
2342 | + elt.innerHTML = '(' + h + 'h ' + m + 'min ago)'; |
2343 | + return; |
2344 | + } |
2345 | + if (delta >= 60) { |
2346 | + var m = parseInt(delta / 60); |
2347 | + elt.innerHTML = '(' + m + 'min ago)'; |
2348 | + return; |
2349 | + } |
2350 | + elt.innerHTML = '(seconds ago)'; |
2351 | +} |
2352 | + |
2353 | +</script> |
2354 | +""" |
2355 | + |
2356 | + prefix = os.path.commonprefix([ branches[x][0]['grd'] for x in branches.keys() ]) |
2357 | + changes = 0 |
2358 | + langs = [] |
2359 | + mapped_langs = {} |
2360 | + cvts = {} |
2361 | + for grd in branches.keys(): |
2362 | + cvts[grd] = Converter(branches[grd][0]['grd'], |
2363 | + lang_mapping = lang_mapping, |
2364 | + template_mapping = templatenames_mapping, |
2365 | + debug = debug, |
2366 | + html_output = html_output, |
2367 | + branches = branches[grd]) |
2368 | + |
2369 | + if cvts[grd].get_supported_strings_count() == 0: |
2370 | + if debug: |
2371 | + print "no string found in %s" % grd |
2372 | + if export_grit is not None and copy_grit is None: |
2373 | + directory = os.path.join(export_grit, os.path.dirname(branches[grd][0]['grd'])[len(prefix):]) |
2374 | + if not os.path.isdir(directory): |
2375 | + os.makedirs(directory, 0755) |
2376 | + shutil.copy2(branches[grd][0]['grd'], directory) |
2377 | + continue |
2378 | + |
2379 | + if copy_grit is not None: |
2380 | + cvts[grd].copy_grit_files(copy_grit) |
2381 | + |
2382 | + if import_gettext is not None: |
2383 | + for directory in import_gettext: |
2384 | + cvts[grd].import_gettext_po_files(directory) |
2385 | + langs.extend(cvts[grd].translations.keys()) |
2386 | + |
2387 | + if export_gettext is not None: |
2388 | + cvts[grd].export_gettext_files(export_gettext) |
2389 | + changes += cvts[grd].template_changes + cvts[grd].translations_changes |
2390 | + |
2391 | + # as we need to add all supported langs to the <outputs> section of all grd files, |
2392 | + # we have to wait for all the 'po' files to be imported and merged before we export |
2393 | + # the grit files and create the patches. |
2394 | + |
2395 | + # supported langs |
2396 | + langs.append('en-US') # special case, it's not translated, but needs to be here |
2397 | + for lang in [ 'no' ]: # workaround for cases like the infamous no->nb mapping |
2398 | + while lang in langs: |
2399 | + langs.remove(lang) |
2400 | + langs.append(lang_mapping[lang]) |
2401 | + r = {} |
2402 | + langs = sorted([ r.setdefault(e, e) for e in langs if e not in r ]) |
2403 | + |
2404 | + for grd in branches.keys(): |
2405 | + if export_grit is not None: |
2406 | + cvts[grd].export_grit_files(os.path.join(export_grit, os.path.dirname(branches[grd][0]['grd'])[len(prefix):]), langs) |
2407 | + for lang in cvts[grd].template.mapped_langs: |
2408 | + mapped_langs[lang] = cvts[grd].template.mapped_langs[lang]['gettext'] |
2409 | + if create_patches is not None: |
2410 | + cvts[grd].create_patches(create_patches) |
2411 | + |
2412 | + # patch the build/common.gypi file if we have to |
2413 | + nlangs = None |
2414 | + if create_patches is not None and build_gyp_file is not None: |
2415 | + nlangs = cvts[branches.keys()[0]].create_build_gyp_patch(create_patches, build_gyp_file, other_grd_files, langs, |
2416 | + whitelisted_new_langs) |
2417 | + |
2418 | + if create_patches is None: |
2419 | + # no need to display the stats |
2420 | + exit(1 if changes > 0 else 0) |
2421 | + |
2422 | + # display some stats |
2423 | + html_js = "" |
2424 | + if html_output: |
2425 | + print """ |
2426 | +<p> |
2427 | +<div> |
2428 | +<div id="legend"> |
2429 | +<fieldset><legend>Legend</legend> |
2430 | +<table border="0"> |
2431 | +<tr><td><div id='green_l' class='progress_bar'></td><td>translated upstream</td></tr> |
2432 | +<tr><td><div id='blue_l' class='progress_bar'></td><td>translations updated in Launchpad</td></tr> |
2433 | +<tr><td><div id='purple_l' class='progress_bar'></td><td>translated in Launchpad</td></tr> |
2434 | +<tr><td><div id='red_l' class='progress_bar'></td><td>untranslated</td></tr> |
2435 | +</table> |
2436 | +</fieldset> |
2437 | +</div> |
2438 | +""" |
2439 | + html_js += "progress_bar('%s', %d, %d, %d, %d);\n" % ('green_l', 0, 1, 0, 0) |
2440 | + html_js += "progress_bar('%s', %d, %d, %d, %d);\n" % ('blue_l', 0, 0, 0, 1) |
2441 | + html_js += "progress_bar('%s', %d, %d, %d, %d);\n" % ('purple_l', 0, 0, 1, 0) |
2442 | + html_js += "progress_bar('%s', %d, %d, %d, %d);\n" % ('red_l', 1, 0, 0, 0) |
2443 | + if json_info: |
2444 | + now = datetime.utcfromtimestamp(os.path.getmtime(json_info)).strftime("%a %b %e %H:%M:%S UTC %Y") |
2445 | + binfo = json.loads(open(json_info, "r").read()) |
2446 | + print """ |
2447 | +<div id="branches"> |
2448 | +<fieldset><legend>Last update info</legend> |
2449 | +<table border="0"> |
2450 | +<tr><th>Branch</th><th>Revision</th><th>Date</th></tr> |
2451 | +<tr><td><a href="%s">Upstream</a></td><td>r%s</td><td>%s <em id='em-u'></em> </td></tr> |
2452 | +<tr><td><a href="%s">Launchpad export</a></td><td>r%s</td><td>%s <em id='em-lp'></em> </td></tr> |
2453 | +<tr><td>This page</a></td><td>-</td><td>%s <em id='em-now'></em> </td></tr> |
2454 | +</table> |
2455 | +</fieldset> |
2456 | +</div> |
2457 | +""" % (binfo['upstream']['url'], binfo['upstream']['revision'], binfo['upstream']['date'], |
2458 | + binfo['launchpad-export']['url'], binfo['launchpad-export']['revision'], |
2459 | + binfo['launchpad-export']['date'], now) |
2460 | + html_js += "time_delta('%s', '%s');\n" % (binfo['upstream']['date'], 'em-u') |
2461 | + html_js += "time_delta('%s', '%s');\n" % (binfo['launchpad-export']['date'], 'em-lp') |
2462 | + html_js += "time_delta('%s', '%s');\n" % (now, 'em-now') |
2463 | + print """ |
2464 | +<div id="stats"> |
2465 | +<table border="0"> |
2466 | +<tr><th rowspan="2">Rank</th><th rowspan="2">Lang</th><th colspan='5'>TOTAL</th><th colspan='5'>""" |
2467 | + print ("</th><th colspan='5'>".join([ "%s (<a href='http://git.chromium.org/gitweb/?p=chromium.git;a=history;f=%s;hb=HEAD'>+</a>)" \ |
2468 | + % (os.path.splitext(grd)[0], branches[grd][0]['grd']) \ |
2469 | + for grd in sorted(branches.keys()) ])) + "</th></tr><tr>" |
2470 | + j = 0 |
2471 | + for grd in [ 'TOTAL' ] + sorted(branches.keys()): |
2472 | + print """ |
2473 | +<th>Status</th> |
2474 | +<th><div id='%s_t%d' class='progress_bar'></th> |
2475 | +<th><div id='%s_t%d' class='progress_bar'></th> |
2476 | +<th><div id='%s_t%d' class='progress_bar'></th> |
2477 | +<th><div id='%s_t%d' class='progress_bar'></th>""" % ('red', j, 'green', j, 'purple', j, 'blue', j) |
2478 | + html_js += "progress_bar('%s_t%d', %d, %d, %d, %d);\n" % ('green', j, 0, 1, 0, 0) |
2479 | + html_js += "progress_bar('%s_t%d', %d, %d, %d, %d);\n" % ('blue', j, 0, 0, 0, 1) |
2480 | + html_js += "progress_bar('%s_t%d', %d, %d, %d, %d);\n" % ('purple', j, 0, 0, 1, 0) |
2481 | + html_js += "progress_bar('%s_t%d', %d, %d, %d, %d);\n" % ('red', j, 1, 0, 0, 0) |
2482 | + j += 1 |
2483 | + print "</tr>" |
2484 | + else: |
2485 | + print """\ |
2486 | + +----------------------- % translated |
2487 | + | +----------------- untranslated |
2488 | + | | +------------ translated upstream |
2489 | + | | | +------- translated in Launchpad |
2490 | + | | | | +-- translations updated in Launchpad |
2491 | + | | | | | |
2492 | + V V V V V""" |
2493 | + print "-- lang -- " + \ |
2494 | + ' '.join([ (" %s " % os.path.splitext(grd)[0]).center(25, "-") \ |
2495 | + for grd in [ 'TOTAL' ] + sorted(branches.keys()) ]) |
2496 | + totals = {} |
2497 | + for lang in langs: |
2498 | + klang = lang |
2499 | + if lang == 'nb': |
2500 | + klang = 'no' |
2501 | + totals[klang] = { 'total': 0, 'missing': 0, 'translated_upstream': 0, 'new': 0, 'updated': 0, 'lskipped': 0 } |
2502 | + for grd in branches.keys(): |
2503 | + tot, lskipped = cvts[grd].template.get_supported_strings_count(klang) |
2504 | + totals[klang]['lskipped'] += lskipped |
2505 | + totals[klang]['total'] += tot |
2506 | + totals[klang]['missing'] += tot |
2507 | + if klang in cvts[grd].template.stats: |
2508 | + totals[klang]['missing'] -= cvts[grd].template.stats[klang]['translated_upstream'] + \ |
2509 | + cvts[grd].template.stats[klang]['new'] + cvts[grd].template.stats[klang]['updated'] |
2510 | + totals[klang]['translated_upstream'] += cvts[grd].template.stats[klang]['translated_upstream'] |
2511 | + totals[klang]['new'] += cvts[grd].template.stats[klang]['new'] |
2512 | + totals[klang]['updated'] += cvts[grd].template.stats[klang]['updated'] |
2513 | + |
2514 | + rank = 0 |
2515 | + p_rank = 0 |
2516 | + p_score = -1 |
2517 | + t_landable = 0 |
2518 | + for lang in sorted(totals, lambda x, y: cmp("%05d %05d %s" % (totals[x]['missing'], totals[x]['total'] - totals[x]['updated'] - totals[x]['new'], x), |
2519 | + "%05d %05d %s" % (totals[y]['missing'], totals[y]['total'] - totals[y]['updated'] - totals[y]['new'], y))): |
2520 | + if lang == 'en-US': |
2521 | + continue |
2522 | + rank += 1 |
2523 | + if p_score != totals[lang]['missing']: |
2524 | + p_score = totals[lang]['missing'] |
2525 | + p_rank = rank |
2526 | + rlang = lang |
2527 | + if lang in lang_mapping: |
2528 | + rlang = lang_mapping[lang] |
2529 | + if html_output: |
2530 | + s = "<tr><td>%s</td><td class='lang'><a class='l' href='%s'>%s</a></td>" % \ |
2531 | + ("#%d" % p_rank, 'https://translations.launchpad.net/chromium-browser/translations/+lang/' + \ |
2532 | + mapped_langs[lang], rlang) |
2533 | + s += "<td><div id='%s' class='progress_bar'></div></td>" % rlang |
2534 | + s += "<td class='d'>%d</td><td class='d'>%d</td><td class='d'>%d</td><td class='d'>%d</td>" % \ |
2535 | + (totals[lang]['missing'], totals[lang]['translated_upstream'], |
2536 | + totals[lang]['new'], totals[lang]['updated']) |
2537 | + html_js += "progress_bar('%s', %d, %d, %d, %d);\n" % \ |
2538 | + (rlang, totals[lang]['missing'], totals[lang]['translated_upstream'], |
2539 | + totals[lang]['new'], totals[lang]['updated']) |
2540 | + else: |
2541 | + s = "%-3s %-6s " % ("#%d" % p_rank, rlang) |
2542 | + s += "%3d%% %4d %4d %4d %4d" % \ |
2543 | + (100.0 * float(totals[lang]['total'] - totals[lang]['missing']) / float(totals[lang]['total']), |
2544 | + totals[lang]['missing'], totals[lang]['translated_upstream'], |
2545 | + totals[lang]['new'], totals[lang]['updated']) |
2546 | + j = 0 |
2547 | + for grd in sorted(branches.keys()): |
2548 | + j += 1 |
2549 | + tplt = os.path.splitext(grd)[0].replace('_', '-') |
2550 | + total, lskipped = cvts[grd].template.get_supported_strings_count(lang) |
2551 | + if lang in cvts[grd].template.stats: |
2552 | + missing = total - cvts[grd].template.stats[lang]['translated_upstream'] - \ |
2553 | + cvts[grd].template.stats[lang]['new'] - cvts[grd].template.stats[lang]['updated'] |
2554 | + if html_output: |
2555 | + if len(unlandable_templates) == 0 and len(landable_templates) == 0: |
2556 | + landable = False |
2557 | + else: |
2558 | + landable = (nlangs is not None and lang in nlangs and tplt not in unlandable_templates) or \ |
2559 | + (nlangs is not None and lang not in nlangs and tplt in landable_templates) |
2560 | + if landable: |
2561 | + t_landable += cvts[grd].template.stats[lang]['new'] + cvts[grd].template.stats[lang]['updated'] |
2562 | + s += "<td><div id='%s_%d' class='progress_bar'></div></td>" % (rlang, j) |
2563 | + s += "<td class='d'>%d</td><td class='d'>%d</td><td class='d%s'>%d</td><td class='d%s'>%d</td>" % \ |
2564 | + (missing, |
2565 | + cvts[grd].template.stats[lang]['translated_upstream'], |
2566 | + " n" if landable and cvts[grd].template.stats[lang]['new'] > 0 else "", |
2567 | + cvts[grd].template.stats[lang]['new'], |
2568 | + " n" if landable and cvts[grd].template.stats[lang]['updated'] > 0 else "", |
2569 | + cvts[grd].template.stats[lang]['updated']) |
2570 | + html_js += "progress_bar('%s_%d', %d, %d, %d, %d);\n" % \ |
2571 | + (rlang, j, missing, |
2572 | + cvts[grd].template.stats[lang]['translated_upstream'], |
2573 | + cvts[grd].template.stats[lang]['new'], |
2574 | + cvts[grd].template.stats[lang]['updated']) |
2575 | + else: |
2576 | + if float(total) > 0: |
2577 | + pct = 100.0 * float(total - missing) / float(total) |
2578 | + else: |
2579 | + pct = 0 |
2580 | + s += " %3d%% %4d %4d %4d %4d" % \ |
2581 | + (pct, missing, |
2582 | + cvts[grd].template.stats[lang]['translated_upstream'], |
2583 | + cvts[grd].template.stats[lang]['new'], |
2584 | + cvts[grd].template.stats[lang]['updated']) |
2585 | + else: |
2586 | + if html_output: |
2587 | + s += "<td><div id='%s_%d' class='progress_bar'></div></td>" % (rlang, j) |
2588 | + s += "<td class='d'>%d</td><td class='d'>%d</td><td class='d'>%d</td><td class='d'>%d</td>" % \ |
2589 | + (total, 0, 0, 0) |
2590 | + html_js += "progress_bar('%s_%d', %d, %d, %d, %d);\n" % \ |
2591 | + (rlang, j, total, 0, 0, 0) |
2592 | + else: |
2593 | + s += " %3d%% %4d %4d %4d %4d" % (0, total, 0, 0, 0) |
2594 | + if html_output: |
2595 | + s += "</tr>" |
2596 | + print s |
2597 | + if html_output: |
2598 | + landable_sum = "" |
2599 | + if t_landable > 0: |
2600 | + landable_sum = """<p> |
2601 | +<div name='landable'> |
2602 | +<table border="0"><tr><td class="d n">%d strings are landable upstream</td></tr></table></div> |
2603 | +""" % t_landable |
2604 | + print """\ |
2605 | +</table> |
2606 | +%s</div> |
2607 | +</div> |
2608 | +<script type="text/javascript" language="javascript"> |
2609 | +%s |
2610 | +</script> |
2611 | +</body> |
2612 | +</html>""" % (landable_sum, html_js) |
2613 | + exit(1 if changes > 0 else 0) |
2614 | + |
2615 | |
2616 | === added file 'create-patches.sh' |
2617 | --- create-patches.sh 1970-01-01 00:00:00 +0000 |
2618 | +++ create-patches.sh 2024-02-28 12:58:47 +0000 |
2619 | @@ -0,0 +1,185 @@ |
2620 | +#!/bin/sh |
2621 | + |
2622 | +# Create the translation patches (grit format) based on the last |
2623 | +# translation export (gettext format) |
2624 | +# (c) 2010-2011, Fabien Tassin <fta@ubuntu.com> |
2625 | + |
2626 | +# location of chromium2pot.py (lp:~chromium-team/chromium-browser/chromium-translations-tools.head) |
2627 | +# (must already exist) |
2628 | +BIN_DIR=/data/bot/chromium-translations-tools.head |
2629 | + |
2630 | +# Launchpad translation export (must already exist, will be pulled here) |
2631 | +LPE_DIR=/data/bot/upstream/chromium-translations-exports.head |
2632 | + |
2633 | +# local svn branches (updated by drobotik) |
2634 | +SRC_TRUNK_DIR=/data/bot/upstream/chromium-browser.svn/src |
2635 | +SRC_DEV_DIR=/data/bot/upstream/chromium-dev.svn/src |
2636 | +SRC_BETA_DIR=/data/bot/upstream/chromium-beta.svn/src |
2637 | +SRC_STABLE_DIR=/data/bot/upstream/chromium-stable.svn/src |
2638 | + |
2639 | +#### |
2640 | + |
2641 | +OUT_DIR=$(mktemp -d) |
2642 | + |
2643 | +# List of options per branch |
2644 | +NEW_OPTS="--map-template-names ui/base/strings/ui_strings.grd=ui/base/strings/app_strings.grd" |
2645 | +OLD_OPTS="" |
2646 | + |
2647 | +OPTS_TRUNK=$NEW_OPTS |
2648 | +OPTS_DEV=$NEW_OPTS |
2649 | +OPTS_BETA=$NEW_OPTS |
2650 | +OPTS_STABLE=$OLD_OPTS |
2651 | + |
2652 | +# List of templates to send to Launchpad |
2653 | +NEW_TEMPLATES=$(cat - <<EOF |
2654 | +ui/base/strings/ui_strings.grd |
2655 | +EOF |
2656 | +) |
2657 | +OLD_TEMPLATES=$(cat - <<EOF |
2658 | +ui/base/strings/app_strings.grd |
2659 | +EOF |
2660 | +) |
2661 | +TEMPLATES_TRUNK=$NEW_TEMPLATES |
2662 | +TEMPLATES_DEV=$NEW_TEMPLATES |
2663 | +TEMPLATES_BETA=$NEW_TEMPLATES |
2664 | +TEMPLATES_STABLE=$OLD_TEMPLATES |
2665 | + |
2666 | +TEMPLATES=$(cat - <<EOF |
2667 | +chrome/app/chromium_strings.grd |
2668 | +chrome/app/generated_resources.grd |
2669 | +chrome/app/policy/policy_templates.grd |
2670 | +webkit/glue/inspector_strings.grd |
2671 | +webkit/glue/webkit_strings.grd |
2672 | +EOF |
2673 | +) |
2674 | + |
2675 | +# List of other templates to update for new langs, but that are |
2676 | +# not sent to Launchpad |
2677 | +NEW_OTHER_TEMPLATES=$(cat - <<EOF |
2678 | +ui/base/strings/app_locale_settings.grd |
2679 | +EOF |
2680 | +) |
2681 | +OLD_OTHER_TEMPLATES=$(cat - <<EOF |
2682 | +app/resources/app_locale_settings.grd |
2683 | +EOF |
2684 | +) |
2685 | +OTHER_TEMPLATES_TRUNK=$NEW_OTHER_TEMPLATES |
2686 | +OTHER_TEMPLATES_DEV=$NEW_OTHER_TEMPLATES |
2687 | +OTHER_TEMPLATES_BETA=$NEW_OTHER_TEMPLATES |
2688 | +OTHER_TEMPLATES_STABLE=$NEW_OTHER_TEMPLATES |
2689 | +# Common to all branches |
2690 | +OTHER_TEMPLATES=$(cat - <<EOF |
2691 | +chrome/app/resources/locale_settings.grd |
2692 | +chrome/app/resources/locale_settings_linux.grd |
2693 | +chrome/app/resources/locale_settings_cros.grd |
2694 | +EOF |
2695 | +) |
2696 | + |
2697 | +###### |
2698 | + |
2699 | +TEMPLATES=$(echo $TEMPLATES | tr '[ \n]' ' ') |
2700 | + |
2701 | +(cd $LPE_DIR ; bzr pull -q) |
2702 | + |
2703 | +space_list () { |
2704 | + local V1="$1" |
2705 | + local V2="$2" |
2706 | + echo "$V1 $V2" | tr '[ \n]' ' ' | sed -e 's/ $//' |
2707 | +} |
2708 | + |
2709 | +comma_list () { |
2710 | + local V1="$1" |
2711 | + local V2="$2" |
2712 | + |
2713 | + echo "$V1 $V2" | tr '[ \n]' ',' | sed -e 's/,$//' |
2714 | +} |
2715 | + |
2716 | +get_branches_info () { |
2717 | + local BRANCH=$1 |
2718 | + local SRC_DIR=$2 |
2719 | + local JSON=$3 |
2720 | + |
2721 | + # upstream url, revision & last change date |
2722 | + UURL=$(cd $SRC_DIR; svn info | grep '^URL: ' | sed -e 's/.*: //') |
2723 | + UREV="$(cd $SRC_DIR; svn info | grep '^Last Changed Rev:' | sed -e 's/.*: //') ($(cut -d= -f2 $SRC_DIR/chrome/VERSION | sed -e 's,$,.,' | tr -d '\n' | sed -e 's/.$//'))" |
2724 | + UDATE=$(date -d "$(cd $SRC_DIR; svn info | grep '^Last Changed Date:' | sed -e 's/.*: //')" --utc) |
2725 | + |
2726 | + # Launchpad url, revision & last change date |
2727 | + LURL=$(cd $LPE_DIR; bzr info | grep 'parent branch' | sed -e 's,.*: bzr+ssh://bazaar,https://code,') |
2728 | + LREV=$(cd $LPE_DIR; bzr revno) |
2729 | + LDATE=$(date -d "$(cd $LPE_DIR; bzr info -v | grep 'latest revision' | sed -e 's/.*: //')" --utc) |
2730 | + |
2731 | + cat - <<EOF > $JSON |
2732 | +{ |
2733 | + "upstream": { |
2734 | + "revision": "$UREV", |
2735 | + "url": "$UURL", |
2736 | + "date": "$UDATE" |
2737 | + }, |
2738 | + "launchpad-export": { |
2739 | + "revision": $LREV, |
2740 | + "url": "$LURL", |
2741 | + "date": "$LDATE" |
2742 | + } |
2743 | +} |
2744 | +EOF |
2745 | +} |
2746 | + |
2747 | +create_patch () { |
2748 | + local BRANCH=$1 |
2749 | + local SRC_DIR=$2 |
2750 | + local BRANCH_OPTS="$3" |
2751 | + local TEMPLATES="$4" |
2752 | + local OTHER_TEMPLATES="$5" |
2753 | + |
2754 | + LOG=converter-output.html |
2755 | + DLOG=converter-diffstat.html |
2756 | + |
2757 | + set -e |
2758 | + cd $SRC_DIR |
2759 | + mkdir -p $OUT_DIR/$BRANCH/new |
2760 | + |
2761 | + get_branches_info $BRANCH $SRC_DIR $OUT_DIR/$BRANCH/new/revisions.json |
2762 | + |
2763 | + local OPTS="" |
2764 | + if [ $BRANCH = trunk ] ; then |
2765 | + OPTS="--landable-templates chromium-strings,inspector-strings --unlandable-templates policy-templates" |
2766 | + fi |
2767 | + |
2768 | + # Generate the new files, using the new template and the translations exported by launchpad |
2769 | + $BIN_DIR/chromium2pot.py \ |
2770 | + --html-output \ |
2771 | + --json-branches-info $OUT_DIR/$BRANCH/new/revisions.json \ |
2772 | + --create-patches $OUT_DIR/$BRANCH/new/patches \ |
2773 | + --import-gettext $LPE_DIR \ |
2774 | + --export-grit $OUT_DIR/$BRANCH/new/patched-files \ |
2775 | + --build-gyp-file build/common.gypi \ |
2776 | + --other-grd-files $OTHER_TEMPLATES \ |
2777 | + $OPTS \ |
2778 | + $BRANCH_OPTS \ |
2779 | + $TEMPLATES >> $OUT_DIR/$BRANCH/new/$LOG 2>&1 |
2780 | + echo >> $OUT_DIR/$BRANCH/new/$LOG |
2781 | + |
2782 | + ( cd "$OUT_DIR/$BRANCH/new/patches" ; for i in * ; do mv $i $i.txt ; done ) |
2783 | + echo "<pre>" > $OUT_DIR/$BRANCH/new/$DLOG |
2784 | + ( cd "$OUT_DIR/$BRANCH/new" ; find patches -type f | xargs --verbose -n 1 diffstat -p 1 >> $DLOG 2>&1 ) |
2785 | + perl -i -pe 's,^(diffstat -p 1 )(\S+)(.*),$1<a href="$2">$2</a>$3,;' $OUT_DIR/$BRANCH/new/$DLOG |
2786 | + echo "</pre>" >> $OUT_DIR/$BRANCH/new/$DLOG |
2787 | + |
2788 | + # get the old files |
2789 | + mkdir $OUT_DIR/$BRANCH/old |
2790 | + lftp -e "lcd $OUT_DIR/$BRANCH/old; cd public_html/chromium/translations/$BRANCH; mirror; quit" sftp://people.ubuntu.com > /dev/null 2>&1 |
2791 | + set +e |
2792 | + (cd $OUT_DIR/$BRANCH ; diff -Nur old new > diff.patch; cd old ; patch -p 1 < ../diff.patch > /dev/null 2>&1 ) |
2793 | + set -e |
2794 | + |
2795 | + lftp -e "lcd $OUT_DIR/$BRANCH/old; cd public_html/chromium/translations/$BRANCH; mirror --delete -R; quit" sftp://people.ubuntu.com > /dev/null 2>&1 |
2796 | + set +e |
2797 | +} |
2798 | + |
2799 | +create_patch "trunk" $SRC_TRUNK_DIR "$OPTS_TRUNK" "$(space_list "$TEMPLATES" "$TEMPLATES_TRUNK")" $(comma_list "$OTHER_TEMPLATES_TRUNK" "$OTHER_TEMPLATES") |
2800 | +create_patch "dev" $SRC_DEV_DIR "$OPTS_DEV" "$(space_list "$TEMPLATES" "$TEMPLATES_DEV")" $(comma_list "$OTHER_TEMPLATES_DEV" "$OTHER_TEMPLATES") |
2801 | +create_patch "beta" $SRC_BETA_DIR "$OPTS_BETA" "$(space_list "$TEMPLATES" "$TEMPLATES_BETA")" $(comma_list "$OTHER_TEMPLATES_BETA" "$OTHER_TEMPLATES") |
2802 | +create_patch "stable" $SRC_STABLE_DIR "$OPTS_STABLE" "$(space_list "$TEMPLATES" "$TEMPLATES_STABLE")" $(comma_list "$OTHER_TEMPLATES_STABLE" "$OTHER_TEMPLATES") |
2803 | + |
2804 | +rm -rf $OUT_DIR |
2805 | |
2806 | === added file 'desktop2gettext.py' |
2807 | --- desktop2gettext.py 1970-01-01 00:00:00 +0000 |
2808 | +++ desktop2gettext.py 2024-02-28 12:58:47 +0000 |
2809 | @@ -0,0 +1,378 @@ |
2810 | +#!/usr/bin/python |
2811 | +# -*- coding: utf-8 -*- |
2812 | + |
2813 | +# (c) 2010-2011, Fabien Tassin <fta@ubuntu.com> |
2814 | + |
2815 | +# Convert a desktop file to Gettext files and back |
2816 | + |
2817 | +import sys, getopt, os, codecs, re, time |
2818 | +from datetime import datetime |
2819 | + |
2820 | +class DesktopFile(dict): |
2821 | + """ Read and write a desktop file """ |
2822 | + def __init__(self, desktop = None, src_pkg = None, verbose = False): |
2823 | + self.changed = False |
2824 | + self.data = [] |
2825 | + self.headers = {} |
2826 | + self.template = {} |
2827 | + self.translations = {} |
2828 | + self.src_pkg = src_pkg |
2829 | + self.mtime = None |
2830 | + self.verbose = verbose |
2831 | + if desktop is not None: |
2832 | + self.read_desktop_file(desktop) |
2833 | + |
2834 | + def read_desktop_file(self, filename): |
2835 | + self.data = [] |
2836 | + self.template = {} |
2837 | + self.mtime = os.path.getmtime(filename) |
2838 | + fd = codecs.open(filename, "rb", encoding="utf-8") |
2839 | + section = None |
2840 | + for line in fd.readlines(): |
2841 | + m = re.match(r'^\[(.*?)\]', line) |
2842 | + if m is not None: |
2843 | + section = m.group(1) |
2844 | + assert section not in self.template, "Duplicate section [%s]" % section |
2845 | + self.template[section] = {} |
2846 | + m = re.match(r'^(Name|GenericName|Comment)(\[\S+\]|)=(.*)', line) |
2847 | + if m is None: |
2848 | + self.data.append(line) |
2849 | + continue |
2850 | + assert section is not None, "Found a '%s' outside a section" % m.group(1) |
2851 | + entry = m.group(1) |
2852 | + value = m.group(3) |
2853 | + if m.group(2) == "": |
2854 | + # master string |
2855 | + self.data.append(line) |
2856 | + assert entry not in self.template[section], \ |
2857 | + "Duplicate entry '%s' in section [%s]" % (entry, section) |
2858 | + self.template[section][entry] = value |
2859 | + if value not in self.translations: |
2860 | + self.translations[value] = {} |
2861 | + else: |
2862 | + # translation |
2863 | + lang = m.group(2)[1:-1] |
2864 | + string = self.template[section][entry] |
2865 | + assert entry in self.template[section], \ |
2866 | + "Translation found for lang '%s' in section [%s] before master entry '%s'" % \ |
2867 | + (lang, section, entry) |
2868 | + if lang not in self.translations[string]: |
2869 | + self.translations[string][lang] = value |
2870 | + fd.close() |
2871 | + |
2872 | + def dump(self): |
2873 | + for section in sorted(self.template.keys()): |
2874 | + print "[%s]:" % section |
2875 | + for entry in sorted(self.template[section].keys()): |
2876 | + print " '%s': '%s'" % (entry, self.template[section][entry]) |
2877 | |
2878 | + for string in sorted(self.translations.keys()): |
2879 | + print "'%s':" % string |
2880 | + for lang in sorted(self.translations[string].keys()): |
2881 | + print " '%s' => '%s'" % (lang, self.translations[string][lang]) |
2882 | + |
2883 | + def write_desktop(self, file): |
2884 | + fd = codecs.open(file, "wb", encoding="utf-8") |
2885 | + for ent in self.data: |
2886 | + fd.write(ent) |
2887 | + m = re.match(r'^(Name|GenericName|Comment)=(.*)', ent) |
2888 | + if m is None: |
2889 | + continue |
2890 | + k = m.group(2) |
2891 | + if k not in self.translations: |
2892 | + continue |
2893 | + for lang in sorted(self.translations[k].keys()): |
2894 | + fd.write("%s[%s]=%s\n" % (m.group(1), lang, self.translations[k][lang])) |
2895 | + |
2896 | + def write_gettext_header(self, fd, mtime = None, last_translator = None, lang_team = None): |
2897 | + mtime = "YEAR-MO-DA HO:MI+ZONE" if mtime is None else \ |
2898 | + datetime.fromtimestamp(mtime).strftime("%Y-%m-%d %H:%M+0000") |
2899 | + last_translator = "FULL NAME <EMAIL@ADDRESS>" if last_translator is None else \ |
2900 | + last_translator |
2901 | + lang_team = "LANGUAGE <LL@li.org>" if lang_team is None else lang_team |
2902 | + fd.write("""\ |
2903 | +# %s desktop file. |
2904 | +# Copyright (C) 2010-2011 Fabien Tassin |
2905 | +# This file is distributed under the same license as the %s package. |
2906 | +# Fabien Tassin <fta@ubuntu.com>, 2010-2011. |
2907 | +# |
2908 | +msgid "" |
2909 | +msgstr "" |
2910 | +"Project-Id-Version: %s\\n" |
2911 | +"Report-Msgid-Bugs-To: https://bugs.launchpad.net/ubuntu/+source/%s/+filebug\\n" |
2912 | +"POT-Creation-Date: %s\\n" |
2913 | +"PO-Revision-Date: %s\\n" |
2914 | +"Last-Translator: %s\\n" |
2915 | +"Language-Team: %s\\n" |
2916 | +"MIME-Version: 1.0\\n" |
2917 | +"Content-Type: text/plain; charset=UTF-8\\n" |
2918 | +"Content-Transfer-Encoding: 8bit\\n" |
2919 | + |
2920 | +""" % (self.src_pkg, self.src_pkg, self.src_pkg, self.src_pkg, |
2921 | + datetime.fromtimestamp(self.mtime).strftime("%Y-%m-%d %H:%M+0000"), |
2922 | + mtime, last_translator, lang_team)) |
2923 | + |
2924 | + def write_gettext_string(self, fd, string, translation = None, usage = None): |
2925 | + if translation is None: |
2926 | + translation = "" |
2927 | + if usage is not None: |
2928 | + fd.write("#. %s\n" % usage) |
2929 | + fd.write('msgid "%s"\nmsgstr "%s"\n\n' % (string, translation)) |
2930 | + |
2931 | + def get_usage(self, string): |
2932 | + res = [] |
2933 | + for section in sorted(self.template.keys()): |
2934 | + for entry in sorted(self.template[section].keys()): |
2935 | + if self.template[section][entry] == string: |
2936 | + res.append("[%s] %s" % (section, entry)) |
2937 | + return ", ".join(res) |
2938 | + |
2939 | + def read_gettext_string(self, fd): |
2940 | + string = {} |
2941 | + cur = None |
2942 | + while 1: |
2943 | + s = fd.readline() |
2944 | + if len(s) == 0 or s == "\n": |
2945 | + break # EOF or end of block |
2946 | + if s.rfind('\n') == len(s) - 1: |
2947 | + s = s[:-1] # chomp |
2948 | + if s.find("# ") == 0 or s == "#": # translator-comment |
2949 | + if 'comment' not in string: |
2950 | + string['comment'] = '' |
2951 | + string['comment'] += s[2:] |
2952 | + continue |
2953 | + if s.find("#:") == 0: # reference |
2954 | + if 'reference' not in string: |
2955 | + string['reference'] = '' |
2956 | + string['reference'] += s[2:] |
2957 | + if s[2:].find(" id: ") == 0: |
2958 | + string['id'] = s[7:].split(' ')[0] |
2959 | + continue |
2960 | + if s.find("#.") == 0: # extracted-comments |
2961 | + if 'extracted' not in string: |
2962 | + string['extracted'] = '' |
2963 | + string['extracted'] += s[2:] |
2964 | + if s[2:].find(" - condition: ") == 0: |
2965 | + if 'conditions' not in string: |
2966 | + string['conditions'] = [] |
2967 | + string['conditions'].append(s[16:]) |
2968 | + continue |
2969 | + if s.find("#~") == 0: # obsolete messages |
2970 | + continue |
2971 | + if s.find("#") == 0: # something else |
2972 | + print "%s not expected. Skip" % repr(s) |
2973 | + continue # not supported/expected |
2974 | + if s.find("msgid ") == 0: |
2975 | + cur = "string" |
2976 | + if cur not in string: |
2977 | + string[cur] = u"" |
2978 | + else: |
2979 | + string[cur] += "\n" |
2980 | + string[cur] += s[6:] |
2981 | + continue |
2982 | + if s.find("msgstr ") == 0: |
2983 | + cur = "translation" |
2984 | + if cur not in string: |
2985 | + string[cur] = u"" |
2986 | + else: |
2987 | + string[cur] += "\n" |
2988 | + string[cur] += s[7:] |
2989 | + continue |
2990 | + if s.find('"') == 0: |
2991 | + if cur is None: |
2992 | + print "'%s' not expected here. Skip" % s |
2993 | + continue |
2994 | + string[cur] += "\n" + s |
2995 | + continue |
2996 | + print "'%s' not expected here. Skip" % s |
2997 | + return None if string == {} else string |
2998 | + |
2999 | + def merge_gettext_string(self, string, lang): |
3000 | + msg = string['string'][1:-1] |
3001 | + if msg == "": # header |
3002 | + self.headers[lang] = {} |
3003 | + map(lambda x: self.headers[lang].setdefault(x.split(": ")[0], x.split(": ")[1]), |
3004 | + string['translation'][4:-3].replace('\\n"\n"', '\n').replace('"\n"', '').split('\n')) |
3005 | + return |
3006 | + if msg not in self.translations: |
3007 | + return # obsolete string |
3008 | + translation = string['translation'][1:-1] |
3009 | + if translation == "": # no translation |
3010 | + return |
3011 | + if lang not in self.translations[msg]: |
3012 | + if self.verbose: |
3013 | + print "merge translation for lang '%s' string '%s'" % (lang, msg) |
3014 | + self.translations[msg][lang] = translation |
3015 | + elif self.translations[msg][lang] != translation: |
3016 | + if self.verbose: |
3017 | + print "update translation for lang '%s' string '%s'" % (lang, msg) |
3018 | + self.translations[msg][lang] = translation |
3019 | + |
3020 | + def read_gettext_file(self, filename): |
3021 | + fd = codecs.open(filename, "rb", encoding="utf-8") |
3022 | + strings = [] |
3023 | + while 1: |
3024 | + string = self.read_gettext_string(fd) |
3025 | + if string is None: |
3026 | + break |
3027 | + strings.append(string) |
3028 | + fd.close() |
3029 | + return strings |
3030 | + |
3031 | + def get_langs(self): |
3032 | + langs = [] |
3033 | + for st in self.translations: |
3034 | + for lang in self.translations[st]: |
3035 | + if lang not in langs: |
3036 | + langs.append(lang) |
3037 | + return sorted(langs) |
3038 | + |
3039 | + def export_gettext_file(self, directory, template_name = "desktop_file", lang = None): |
3040 | + filename = os.path.join(directory, "%s.pot" % template_name if lang is None else "%s.po" % lang) |
3041 | + # if there's already a file with this name, compare its content and only update it |
3042 | + # when it's needed |
3043 | + update = False |
3044 | + if not os.path.exists(filename): |
3045 | + update = True |
3046 | + else: |
3047 | + # compare the strings |
3048 | + strings = self.read_gettext_file(filename)[1:] |
3049 | + old = sorted(map(lambda x: x['string'][1:-1], strings)) |
3050 | + new = sorted(self.translations.keys()) |
3051 | + if old != new: |
3052 | + update = True |
3053 | + if self.verbose: |
3054 | + print "strings differ for %s. Update" % filename |
3055 | + if not update: |
3056 | + # compare string descriptions |
3057 | + old = map(lambda x: x['extracted'][1:], |
3058 | + sorted(strings, lambda a, b: cmp(a['string'][1:-1], b['string'][1:-1]))) |
3059 | + new = map(lambda x: self.get_usage(x), sorted(self.translations.keys())) |
3060 | + if old != new: |
3061 | + update = True |
3062 | + if self.verbose: |
3063 | + print "string descriptions differ for %s. Update" % filename |
3064 | + if not update and lang is not None: |
3065 | + # compare translations |
3066 | + old = map(lambda x: x['translation'][1:-1], |
3067 | + sorted(strings, lambda a, b: cmp(a['string'][1:-1], b['string'][1:-1]))) |
3068 | + new = map(lambda x: self.translations[x][lang] if lang in self.translations[x] else "", |
3069 | + sorted(self.translations.keys())) |
3070 | + if old != new: |
3071 | + update = True |
3072 | + if self.verbose: |
3073 | + print "translations differ for %s. Update" % filename |
3074 | + if not update: |
3075 | + return |
3076 | + if self.verbose: |
3077 | + print "update %s" % filename |
3078 | + fd = codecs.open(filename, 'wb', encoding='utf-8') |
3079 | + last_translator = None |
3080 | + lang_team = None |
3081 | + if lang is None: |
3082 | + self.mtime = time.time() |
3083 | + mtime = None |
3084 | + else: |
3085 | + mtime = time.time() |
3086 | + if lang in self.headers and 'Last-Translator' in self.headers[lang]: |
3087 | + last_translator = self.headers[lang]['Last-Translator'] |
3088 | + if lang in self.headers and 'Language-Team' in self.headers[lang]: |
3089 | + lang_team = self.headers[lang]['Language-Team'] |
3090 | + else: |
3091 | + lang_team = "%s <%s@li.org>" % (lang, lang) |
3092 | + self.write_gettext_header(fd, mtime = mtime, last_translator = last_translator, lang_team = lang_team) |
3093 | + for string in sorted(self.translations.keys()): |
3094 | + val = self.translations[string][lang] \ |
3095 | + if lang is not None and lang in self.translations[string] else None |
3096 | + self.write_gettext_string(fd, string, val, usage = self.get_usage(string)) |
3097 | + fd.close() |
3098 | + self.changed = True |
3099 | + |
3100 | + def export_gettext_files(self, directory): |
3101 | + if not os.path.isdir(directory): |
3102 | + os.makedirs(directory, 0755) |
3103 | + self.export_gettext_file(directory) |
3104 | + for lang in self.get_langs(): |
3105 | + self.export_gettext_file(directory, lang = lang) |
3106 | + |
3107 | + def import_gettext_files(self, directory): |
3108 | + """ Import strings from gettext 'po' files, ignore the 'pot'. |
3109 | + Only merge strings matching our desktop file """ |
3110 | + assert os.path.isdir(directory) |
3111 | + for file in os.listdir(directory): |
3112 | + lang, ext = os.path.splitext(file) |
3113 | + if ext != '.po': |
3114 | + continue |
3115 | + for string in self.read_gettext_file(os.path.join(directory, file)): |
3116 | + self.merge_gettext_string(string, lang) |
3117 | + |
3118 | +def usage(): |
3119 | + appname = sys.argv[0].rpartition('/')[2] |
3120 | + print """ |
3121 | +Usage: %s [options] --import-desktop master.desktop |
3122 | + [--export-gettext to-launchpad] |
3123 | + [--import-gettext from-launchpad] |
3124 | + [--export-desktop improved.desktop] |
3125 | + [--project-name somename] |
3126 | + |
3127 | + Convert a desktop file to Gettext files and back |
3128 | + |
3129 | + options could be: |
3130 | + -v | --verbose verbose mode |
3131 | + --import-desktop file master desktop file (mandatory) |
3132 | + --import-gettext dir GetText files to merge |
3133 | + --export-gettext dir merged GetText files |
3134 | + --export-desktop file improved desktop file |
3135 | + --project-name name project or source package name |
3136 | + |
3137 | +""" % appname |
3138 | + |
3139 | +if '__main__' == __name__: |
3140 | + sys.stdout = codecs.getwriter('utf8')(sys.stdout) |
3141 | + try: |
3142 | + opts, args = getopt.getopt(sys.argv[1:], "dhv", |
3143 | + [ "verbose", "project-name=", |
3144 | + "import-desktop=", "export-desktop=", |
3145 | + "import-gettext=", "export-gettext=" ]) |
3146 | + except getopt.GetoptError, err: |
3147 | + print str(err) |
3148 | + usage() |
3149 | + sys.exit(2) |
3150 | + |
3151 | + verbose = False |
3152 | + desktop_in = None |
3153 | + desktop_out = None |
3154 | + gettext_in = None |
3155 | + gettext_out = None |
3156 | + project_name = "misconfigured-project" |
3157 | + for o, a in opts: |
3158 | + if o in ("-v", "--verbose"): |
3159 | + verbose = True |
3160 | + elif o in ("-h", "--help"): |
3161 | + usage() |
3162 | + sys.exit() |
3163 | + elif o == "--project-name": |
3164 | + project_name = a |
3165 | + elif o == "--import-desktop": |
3166 | + desktop_in = a |
3167 | + elif o == "--export-desktop": |
3168 | + desktop_out = a |
3169 | + elif o == "--import-gettext": |
3170 | + gettext_in = a |
3171 | + elif o == "--export-gettext": |
3172 | + gettext_out = a |
3173 | + |
3174 | + if desktop_in is None: |
3175 | + print "Error: --import-desktop is mandatory" |
3176 | + usage() |
3177 | + sys.exit(2) |
3178 | + |
3179 | + df = DesktopFile(desktop_in, src_pkg = project_name, verbose = verbose) |
3180 | + if gettext_in is not None: |
3181 | + df.import_gettext_files(gettext_in) |
3182 | + if gettext_out is not None: |
3183 | + df.export_gettext_files(gettext_out) |
3184 | + if desktop_out is not None: |
3185 | + if desktop_in != desktop_out or df.changed: |
3186 | + df.write_desktop(desktop_out) |
3187 | + exit(1 if df.changed else 0) |
3188 | |
3189 | === added file 'update-inspector.py' |
3190 | --- update-inspector.py 1970-01-01 00:00:00 +0000 |
3191 | +++ update-inspector.py 2024-02-28 12:58:47 +0000 |
3192 | @@ -0,0 +1,149 @@ |
3193 | +#!/usr/bin/python |
3194 | +# -*- coding: utf-8 -*- |
3195 | + |
3196 | +# (c) 2010, Fabien Tassin <fta@ubuntu.com> |
3197 | + |
3198 | +# Helper to merge the localizedStrings.js strings from Wekbit inspector into |
3199 | +# the inspector_strings.grd Grit template |
3200 | + |
3201 | +import os, re, sys, codecs |
3202 | +from optparse import OptionParser |
3203 | + |
3204 | +class JS2Grit: |
3205 | + def __init__(self, grd = None, js = None): |
3206 | + self.js_file = None |
3207 | + self.js_strings = [] |
3208 | + self.grd_file = None |
3209 | + self.order = [] |
3210 | + self.data = {} |
3211 | + self.missing = [] |
3212 | + self.obsolete = [] |
3213 | + self.merged = [] |
3214 | + if grd is not None: |
3215 | + self.import_grd(grd) |
3216 | + if js is not None: |
3217 | + self.import_js(js) |
3218 | + |
3219 | + def xml2js(self, s): |
3220 | + ''' |
3221 | + '<ph name="ERRORS_COUNT">%1$d<ex>2</ex></ph> errors, <ph name="WARNING_COUNT">%2$d<ex>1</ex></ph> warning' |
3222 | + => '%d errors, %d warnings' |
3223 | + ''' |
3224 | + s2 = re.sub('<ex>.*?</ex>', '', s) |
3225 | + s2 = re.sub('<ph name=".*?">%\d+\$(.*?)</ph>', r'%\1', s2) |
3226 | + s2 = re.sub('<', '<', s2) |
3227 | + s2 = re.sub('>', '>', s2) |
3228 | + s2 = re.sub('\'\'\'', '', s2) |
3229 | + return re.sub('<ph name=".*?">(.*?)</ph>', r'\1', s2) |
3230 | + |
3231 | + def js2xml(self, s): |
3232 | + ''' |
3233 | + '%d errors, %d warnings' |
3234 | + => '<ph name="XXX">%1$d<ex>XXX</ex></ph> errors, <ph name="XXX">%2$d<ex>XXX</ex></ph> warning' |
3235 | + ''' |
3236 | + s = re.sub('<', '<', s) |
3237 | + s = re.sub('>', '>', s) |
3238 | + s = re.sub('^ ', '\'\'\' ', s) |
3239 | + phs = [ x for x in re.split(r'(%\d*\.?\d*[dfs])', s) if x.find('%') == 0 and x.find('%%') != 0 ] |
3240 | + if len(phs) > 1: |
3241 | + phs = re.split(r'(%\d*\.?\d*[dfs])', s) |
3242 | + j = 1 |
3243 | + for i, part in enumerate(phs): |
3244 | + if part.find('%') == 0 and part.find('%%') != 0: |
3245 | + phs[i] = '<ph name="XXX">%%%d$%s<ex>XXX</ex></ph>' % (j, part[1:]) |
3246 | + j += 1 |
3247 | + elif len(phs) == 1: |
3248 | + phs = re.split(r'(%\d*\.?\d*[dfs])', s) |
3249 | + for i, part in enumerate(phs): |
3250 | + if part.find('%') == 0 and part.find('%%') != 0: |
3251 | + phs[i] = '<ph name="XXX">%s<ex>XXX</ex></ph>' % part |
3252 | + else: |
3253 | + return s |
3254 | + return ''.join(phs) |
3255 | + |
3256 | + def import_grd(self, file): |
3257 | + self.order = [] |
3258 | + self.data = {} |
3259 | + self.grd_file = file |
3260 | + fd = codecs.open(file, 'rb', encoding='utf-8') |
3261 | + file = fd.read() |
3262 | + for s in re.finditer('<message name="(.*?)" desc="(.*?)">\n\s+(.*?)\n\s+</message>', file, re.S): |
3263 | + key = self.xml2js(s.group(3)) |
3264 | + self.order.append(key) |
3265 | + self.data[key] = { 'code': s.group(1), 'desc': s.group(2), 'string': s.group(3) } |
3266 | + fd.close() |
3267 | + return self.order, self.data |
3268 | + |
3269 | + def import_js(self, file): |
3270 | + self.js_strings = [] |
3271 | + self.js_file = file |
3272 | + fd = codecs.open(file, 'rb', encoding='utf-16') |
3273 | + file = fd.read() |
3274 | + for s in re.finditer('localizedStrings\["(.*?)"\] = "(.*?)";', file, re.S): |
3275 | + self.js_strings.append(s.group(1)) |
3276 | + fd.close() |
3277 | + return self.js_strings |
3278 | + |
3279 | + def merge_strings(self): |
3280 | + self.merged = [] |
3281 | + self.missing = [ s for s in self.js_strings if s not in self.order ] |
3282 | + self.obsolete = [ s for s in self.order if s not in self.js_strings ] |
3283 | + for s in self.js_strings: |
3284 | + if s in self.order: |
3285 | + self.merged.append(self.data[s]) |
3286 | + else: |
3287 | + self.merged.append({ 'code': 'IDS_XXX', 'desc': 'XXX', 'string': self.js2xml(s), 'key': s }) |
3288 | + |
3289 | + def get_new_strings_count(self): |
3290 | + return len(self.missing) |
3291 | + |
3292 | + def get_obsolete_strings_count(self): |
3293 | + return len(self.obsolete) |
3294 | + |
3295 | + def get_strings_count(self): |
3296 | + return len(self.js_strings) |
3297 | + |
3298 | + def export_grd(self, grd): |
3299 | + fdi = codecs.open(self.grd_file, 'rb', encoding='utf-8') |
3300 | + data = fdi.read() |
3301 | + fdi.close() |
3302 | + |
3303 | + fdo = codecs.open(grd, 'wb', encoding='utf-8') |
3304 | + |
3305 | + # copy the header |
3306 | + pos = data.find('<messages') |
3307 | + pos += data[pos:].find('\n') + 1 |
3308 | + fdo.write(data[:pos]) |
3309 | + |
3310 | + # write the merged strings |
3311 | + for s in self.merged: |
3312 | + if 'key' in s and s['key'] != s['string']: |
3313 | + fdo.write(" <!-- XXX: '%s' -->\n" % s['key']) |
3314 | + fdo.write(' <message name="%s" desc="%s">\n %s\n </message>\n' % \ |
3315 | + (s['code'], s['desc'], s['string'])) |
3316 | + # copy the footer |
3317 | + pos = data.find('</messages>') |
3318 | + pos -= pos - data[:pos].rfind('\n') - 1 |
3319 | + fdo.write(data[pos:]) |
3320 | + fdo.close() |
3321 | + |
3322 | +if '__main__' == __name__: |
3323 | + sys.stdout = codecs.getwriter('utf8')(sys.stdout) |
3324 | + |
3325 | + parser = OptionParser(usage = 'Usage: %prog --grd inspector_strings.grd --js localizedStrings.js -o foo.grd') |
3326 | + parser.add_option("-j", "--js", dest="js", |
3327 | + help="read js strings from FILE", metavar="FILE") |
3328 | + parser.add_option("-g", "--grd", dest="grd", |
3329 | + help="read grd template from FILE", metavar="FILE") |
3330 | + parser.add_option("-o", "--output", dest="output", |
3331 | + help="write merged grd template to FILE", metavar="FILE") |
3332 | + (options, args) = parser.parse_args() |
3333 | + |
3334 | + if options.grd is None or options.js is None or options.output is None: |
3335 | + parser.error("One of --grd, --js or --output is missing") |
3336 | + js2grd = JS2Grit(grd = options.grd, js = options.js) |
3337 | + print "Found %d strings in the js file" % js2grd.get_strings_count() |
3338 | + js2grd.merge_strings() |
3339 | + js2grd.export_grd(options.output) |
3340 | + print "Merged %d new strings, dropped %d obsolete strings" % \ |
3341 | + (js2grd.get_new_strings_count(), js2grd.get_obsolete_strings_count()) |
3342 | |
3343 | === added file 'update-pot.sh' |
3344 | --- update-pot.sh 1970-01-01 00:00:00 +0000 |
3345 | +++ update-pot.sh 2024-02-28 12:58:47 +0000 |
3346 | @@ -0,0 +1,87 @@ |
3347 | +#!/bin/sh |
3348 | + |
3349 | +# Update the gettext bzr branch (imported by lp rosetta) |
3350 | +# based on a merge of all the templates in the 4 chromium |
3351 | +# channels. |
3352 | +# (c) 2010-2011, Fabien Tassin <fta@ubuntu.com> |
3353 | + |
3354 | +PROJECT=chromium-browser |
3355 | +PKG_DIR=/data/bot/chromium-browser.head |
3356 | +BIN_DIR=/data/bot/chromium-translations-tools.head |
3357 | +OUT_DIR=/data/bot/upstream/chromium-translations.head |
3358 | +LPE_DIR=/data/bot/upstream/chromium-translations-exports.head |
3359 | + |
3360 | +SRC_TRUNK_DIR=/data/bot/upstream/chromium-browser.svn/src |
3361 | +SRC_DEV_DIR=/data/bot/upstream/chromium-dev.svn/src |
3362 | +SRC_BETA_DIR=/data/bot/upstream/chromium-beta.svn/src |
3363 | +SRC_STABLE_DIR=/data/bot/upstream/chromium-stable.svn/src |
3364 | + |
3365 | +###### |
3366 | + |
3367 | +NEW_TEMPLATES="chrome/app/chromium_strings.grd,chrome/app/generated_resources.grd,ui/base/strings/ui_strings.grd,chrome/app/policy/policy_templates.grd,webkit/glue/inspector_strings.grd,webkit/glue/webkit_strings.grd" |
3368 | +TEMPLATES="chrome/app/chromium_strings.grd,chrome/app/generated_resources.grd,ui/base/strings/app_strings.grd,chrome/app/policy/policy_templates.grd,webkit/glue/inspector_strings.grd,webkit/glue/webkit_strings.grd" |
3369 | + |
3370 | +OPTS="--map-template-names ui/base/strings/ui_strings.grd=ui/base/strings/app_strings.grd" |
3371 | +IMPORT="--import-gettext $OUT_DIR,$LPE_DIR" |
3372 | +BRANCH_TRUNK="--import-grit-branch trunk:$SRC_TRUNK_DIR:$NEW_TEMPLATES" |
3373 | +BRANCH_DEV="--import-grit-branch dev:$SRC_DEV_DIR:$NEW_TEMPLATES" |
3374 | +BRANCH_BETA="--import-grit-branch beta:$SRC_BETA_DIR:$NEW_TEMPLATES" |
3375 | +BRANCH_STABLE="--import-grit-branch stable:$SRC_STABLE_DIR:$TEMPLATES" |
3376 | + |
3377 | +BRANCHES="$BRANCH_TRUNK $BRANCH_DEV $BRANCH_BETA $BRANCH_STABLE" |
3378 | + |
3379 | +(cd $LPE_DIR ; bzr pull -q) |
3380 | +(cd $BIN_DIR ; bzr pull -q) |
3381 | + |
3382 | +cd $SRC_TRUNK_DIR |
3383 | +$BIN_DIR/chromium2pot.py $BRANCHES $IMPORT $OPTS --export-gettext $OUT_DIR $NEW_TEMPLATES |
3384 | +RET=$? |
3385 | +cd $OUT_DIR |
3386 | +set -e |
3387 | +for f in */*.pot */*.po ; do |
3388 | + msgfmt -c $f |
3389 | +done |
3390 | +set +e |
3391 | +rm -f messages.mo |
3392 | + |
3393 | +# desktop file |
3394 | +DF_DIR="desktop_file" |
3395 | +DF=$PROJECT.desktop |
3396 | +DF_ARGS="-v --import-desktop $DF_DIR/$DF --project-name $PROJECT" |
3397 | +if [ ! -d $DF_DIR ] ; then |
3398 | + mkdir $DF_DIR |
3399 | + RET=1 |
3400 | +fi |
3401 | +if [ ! -e $DF_DIR/$DF ] ; then # no desktop file yet |
3402 | + cp -va $PKG_DIR/debian/$DF $DF_DIR |
3403 | + $BIN_DIR/desktop2gettext.py $DF_ARGS --export-gettext $DF_DIR |
3404 | + bzr add $DF_DIR |
3405 | + RET=1 |
3406 | +fi |
3407 | +if [ -d $LPE_DIR/$DF_DIR ] ; then |
3408 | + $BIN_DIR/desktop2gettext.py $DF_ARGS --import-gettext $LPE_DIR/$DF_DIR --export-gettext $DF_DIR --export-desktop $DF_DIR/$DF |
3409 | + R=$? |
3410 | + if [ $R = 1 ] ; then |
3411 | + RET=1 |
3412 | + fi |
3413 | + diff -u $PKG_DIR/debian/$DF $DF_DIR/$DF |
3414 | +fi |
3415 | + |
3416 | +if [ $RET = 0 ] ; then |
3417 | + # no changes |
3418 | + exit 0 |
3419 | +fi |
3420 | + |
3421 | +REV=$(svn info $SRC_TRUNK_DIR | grep 'Last Changed Rev:' | cut -d' ' -f4) |
3422 | +VERSION=$(cut -d= -f2 $SRC_TRUNK_DIR/chrome/VERSION | sed -e 's,$,.,' | tr -d '\n' | sed -e 's/.$//') |
3423 | +MSG="Strings update for $VERSION r$REV" |
3424 | + |
3425 | +if [ "Z$1" != Z ] ; then |
3426 | + MSG=$1 |
3427 | +fi |
3428 | + |
3429 | +cd $OUT_DIR |
3430 | +bzr add |
3431 | +bzr commit -q -m "* $MSG" |
3432 | +bzr push -q > /dev/null |
3433 | +exit 0 |