Left: | ||
Right: |
LEFT | RIGHT |
---|---|
1 # This Source Code Form is subject to the terms of the Mozilla Public | 1 # This Source Code Form is subject to the terms of the Mozilla Public |
2 # License, v. 2.0. If a copy of the MPL was not distributed with this | 2 # License, v. 2.0. If a copy of the MPL was not distributed with this |
3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. | 3 # file, You can obtain one at http://mozilla.org/MPL/2.0/. |
4 | 4 |
5 import re | 5 import re |
6 import os | 6 import os |
7 import sys | 7 import sys |
8 import codecs | 8 import codecs |
9 import json | 9 import json |
10 import urlparse | 10 import urlparse |
11 import urllib | 11 import urllib |
12 import urllib2 | 12 import urllib2 |
13 import mimetypes | 13 import mimetypes |
14 from StringIO import StringIO | 14 from StringIO import StringIO |
15 from ConfigParser import SafeConfigParser | 15 from ConfigParser import SafeConfigParser |
16 from zipfile import ZipFile | 16 from zipfile import ZipFile |
17 from xml.parsers.expat import ParserCreate, XML_PARAM_ENTITY_PARSING_ALWAYS | 17 from xml.parsers.expat import ParserCreate, XML_PARAM_ENTITY_PARSING_ALWAYS |
18 | 18 |
19 langMappingGecko = { | 19 CROWDIN_LANG_MAPPING = { |
20 'bn-BD': 'bn', | |
21 'br': 'br-FR', | |
22 'dsb': 'dsb-DE', | |
23 'fj-FJ': 'fj', | |
24 'hsb': 'hsb-DE', | |
25 'hi-IN': 'hi', | |
26 'ml': 'ml-IN', | |
27 'nb-NO': 'nb', | |
28 'rm': 'rm-CH', | |
29 'ta-LK': 'ta', | |
30 'wo-SN': 'wo', | |
31 } | |
32 | |
33 langMappingChrome = { | |
34 'br': 'br-FR', | 20 'br': 'br-FR', |
35 'dsb': 'dsb-DE', | 21 'dsb': 'dsb-DE', |
36 'es': 'es-ES', | 22 'es': 'es-ES', |
37 'fur': 'fur-IT', | 23 'fur': 'fur-IT', |
38 'fy': 'fy-NL', | 24 'fy': 'fy-NL', |
39 'ga': 'ga-IE', | 25 'ga': 'ga-IE', |
40 'gu': 'gu-IN', | 26 'gu': 'gu-IN', |
41 'hsb': 'hsb-DE', | 27 'hsb': 'hsb-DE', |
42 'hy': 'hy-AM', | 28 'hy': 'hy-AM', |
43 'ml': 'ml-IN', | 29 'ml': 'ml-IN', |
44 'nn': 'nn-NO', | 30 'nn': 'nn-NO', |
45 'pa': 'pa-IN', | 31 'pa': 'pa-IN', |
46 'rm': 'rm-CH', | 32 'rm': 'rm-CH', |
47 'si': 'si-LK', | 33 'si': 'si-LK', |
48 'sv': 'sv-SE', | 34 'sv': 'sv-SE', |
49 'ur': 'ur-PK', | 35 'ur': 'ur-PK', |
50 } | 36 } |
51 | 37 |
52 chromeLocales = [ | |
53 'am', | |
54 'ar', | |
55 'bg', | |
56 'bn', | |
57 'ca', | |
58 'cs', | |
59 'da', | |
60 'de', | |
61 'el', | |
62 'en-GB', | |
63 'en-US', | |
64 'es-419', | |
tlucas
2017/10/02 09:25:43
iirc, removing the mapping for this will trigger a
Sebastian Noack
2017/10/02 23:01:33
The behavior of setuptrans is outdated. I initiall
| |
65 'es', | |
66 'et', | |
67 'fa', | |
68 'fi', | |
69 'fil', | |
70 'fr', | |
71 'gu', | |
72 'he', | |
73 'hi', | |
74 'hr', | |
75 'hu', | |
76 'id', | |
77 'it', | |
78 'ja', | |
79 'kn', | |
80 'ko', | |
81 'lt', | |
82 'lv', | |
83 'ml', | |
84 'mr', | |
85 'ms', | |
86 'nb', | |
87 'nl', | |
88 'pl', | |
89 'pt-BR', | |
90 'pt-PT', | |
91 'ro', | |
92 'ru', | |
93 'sk', | |
94 'sl', | |
95 'sr', | |
96 'sv', | |
97 'sw', | |
98 'ta', | |
99 'te', | |
100 'th', | |
101 'tr', | |
102 'uk', | |
103 'vi', | |
104 'zh-CN', | |
105 'zh-TW', | |
106 ] | |
107 | |
108 CROWDIN_AP_URL = 'https://api.crowdin.com/api/project' | 38 CROWDIN_AP_URL = 'https://api.crowdin.com/api/project' |
39 FIREFOX_RELEASES_URL = 'http://www.mozilla.org/en-US/firefox/all.html' | |
40 FIREFOX_LP_URL = 'https://addons.mozilla.org/en-US/firefox/language-tools/' | |
41 CHROMIUM_DEB_URL = 'https://packages.debian.org/sid/all/chromium-l10n/filelist' | |
109 | 42 |
110 | 43 |
111 def crowdin_request(project_name, action, key, get={}, post_data=None, | 44 def crowdin_request(project_name, action, key, get={}, post_data=None, |
112 headers={}, raw=False): | 45 headers={}, raw=False): |
113 """Perform a call to crowdin and raise an Exception on failure.""" | 46 """Perform a call to crowdin and raise an Exception on failure.""" |
114 request = urllib2.Request( | 47 request = urllib2.Request( |
115 '{}/{}/{}?{}'.format(CROWDIN_AP_URL, | 48 '{}/{}/{}?{}'.format(CROWDIN_AP_URL, |
116 urllib.quote(project_name), | 49 urllib.quote(project_name), |
117 urllib.quote(action), | 50 urllib.quote(action), |
118 urllib.urlencode(dict(get, key=key, json=1))), | 51 urllib.urlencode(dict(get, key=key, json=1))), |
(...skipping 28 matching lines...) Expand all Loading... | |
147 yield (key, self[key]) | 80 yield (key, self[key]) |
148 done.add(key) | 81 done.add(key) |
149 | 82 |
150 | 83 |
151 def escapeEntity(value): | 84 def escapeEntity(value): |
152 return value.replace('&', '&').replace('<', '<').replace('>', '>') .replace('"', '"') | 85 return value.replace('&', '&').replace('<', '<').replace('>', '>') .replace('"', '"') |
153 | 86 |
154 | 87 |
155 def unescapeEntity(value): | 88 def unescapeEntity(value): |
156 return value.replace('&', '&').replace('<', '<').replace('>', '>') .replace('"', '"') | 89 return value.replace('&', '&').replace('<', '<').replace('>', '>') .replace('"', '"') |
157 | |
158 | |
159 def mapLocale(type, locale): | |
160 mapping = langMappingChrome if type == 'ISO-15897' else langMappingGecko | |
161 return mapping.get(locale, locale) | |
162 | 90 |
163 | 91 |
164 def parseDTDString(data, path): | 92 def parseDTDString(data, path): |
165 result = [] | 93 result = [] |
166 currentComment = [None] | 94 currentComment = [None] |
167 | 95 |
168 parser = ParserCreate() | 96 parser = ParserCreate() |
169 parser.UseForeignDTD(True) | 97 parser.UseForeignDTD(True) |
170 parser.SetParamEntityParsing(XML_PARAM_ENTITY_PARSING_ALWAYS) | 98 parser.SetParamEntityParsing(XML_PARAM_ENTITY_PARSING_ALWAYS) |
171 | 99 |
(...skipping 136 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
308 for key, value in parsed.iteritems(): | 236 for key, value in parsed.iteritems(): |
309 if 'description' in value: | 237 if 'description' in value: |
310 del value['description'] | 238 del value['description'] |
311 | 239 |
312 file = codecs.open(path, 'wb', encoding='utf-8') | 240 file = codecs.open(path, 'wb', encoding='utf-8') |
313 json.dump(parsed, file, ensure_ascii=False, sort_keys=True, indent=2, separa tors=(',', ': ')) | 241 json.dump(parsed, file, ensure_ascii=False, sort_keys=True, indent=2, separa tors=(',', ': ')) |
314 file.close() | 242 file.close() |
315 | 243 |
316 | 244 |
317 def setupTranslations(localeConfig, projectName, key): | 245 def setupTranslations(localeConfig, projectName, key): |
318 # Make a new set from the locales list, mapping to Crowdin friendly format | 246 locales = set() |
319 locales = {mapLocale(localeConfig['name_format'], locale) | 247 |
320 for locale in localeConfig['locales']} | 248 # Languages supported by Firefox |
321 | 249 data = urllib2.urlopen(FIREFOX_RELEASES_URL).read() |
322 # Fill up with locales that we don't have but the browser supports | 250 for match in re.finditer(r'&lang=([\w\-]+)"', data): |
323 if 'chrome' in localeConfig['target_platforms']: | 251 locales.add(match.group(1)) |
324 for locale in chromeLocales: | 252 |
325 locales.add(mapLocale('ISO-15897', locale)) | 253 # Languages supported by Firefox Language Packs |
326 | 254 data = urllib2.urlopen(FIREFOX_LP_URL).read() |
327 if 'gecko' in localeConfig['target_platforms']: | 255 for match in re.finditer(r'<tr>.*?</tr>', data, re.S): |
328 firefoxLocales = urllib2.urlopen('http://www.mozilla.org/en-US/firefox/a ll.html').read() | 256 if match.group(0).find('Install Language Pack') >= 0: |
329 for match in re.finditer(r'&lang=([\w\-]+)"', firefoxLocales): | 257 match2 = re.search(r'lang="([\w\-]+)"', match.group(0)) |
330 locales.add(mapLocale('BCP-47', match.group(1))) | 258 if match2: |
331 langPacks = urllib2.urlopen('https://addons.mozilla.org/en-US/firefox/la nguage-tools/').read() | 259 locales.add(match2.group(1)) |
332 for match in re.finditer(r'<tr>.*?</tr>', langPacks, re.S): | 260 |
333 if match.group(0).find('Install Language Pack') >= 0: | 261 # Languages supported by Chrome (excluding es-419) |
334 match2 = re.search(r'lang="([\w\-]+)"', match.group(0)) | 262 data = urllib2.urlopen(CHROMIUM_DEB_URL).read() |
335 if match2: | 263 for match in re.finditer(r'locales/(?!es-419)([\w\-]+)\.pak', data): |
336 locales.add(mapLocale('BCP-47', match2.group(1))) | 264 locales.add(match.group(1)) |
337 | 265 |
338 allowed = set() | 266 # We don't translate indvidual dialects of languages |
339 allowedLocales = crowdin_request(projectName, 'supported-languages', key) | 267 # other than English, Spanish, Portuguese and Chinese. |
340 | 268 for locale in list(locales): |
341 for locale in allowedLocales: | 269 prefix = locale.split('-')[0] |
342 allowed.add(locale['crowdin_code']) | 270 if prefix not in {'en', 'es', 'pt', 'zh'}: |
271 locales.remove(locale) | |
272 locales.add(prefix) | |
273 | |
274 # Add languages with existing translations. | |
275 locales.update(localeConfig['locales']) | |
276 | |
277 # Don't add the language we translate from as target translation. | |
278 locales.remove(localeConfig['default_locale'].replace('_', '-')) | |
279 | |
280 # Convert to locales understood by Crowdin. | |
281 locales = {CROWDIN_LANG_MAPPING.get(locale, locale) for locale in locales} | |
282 allowed = {locale['crowdin_code'] for locale in | |
283 crowdin_request(projectName, 'supported-languages', key)} | |
343 if not allowed.issuperset(locales): | 284 if not allowed.issuperset(locales): |
344 print "Warning, following locales aren't allowed by server: " + ', '.joi n(locales - allowed) | 285 print "Warning, following locales aren't allowed by server: " + ', '.joi n(locales - allowed) |
345 | 286 |
346 locales = list(locales & allowed) | 287 locales = sorted(locales & allowed) |
347 locales.sort() | |
348 params = urllib.urlencode([('languages[]', locale) for locale in locales]) | 288 params = urllib.urlencode([('languages[]', locale) for locale in locales]) |
349 | |
350 crowdin_request(projectName, 'edit-project', key, post_data=params) | 289 crowdin_request(projectName, 'edit-project', key, post_data=params) |
351 | 290 |
352 | 291 |
353 def crowdin_prepare_upload(files): | 292 def crowdin_prepare_upload(files): |
354 """Create a post body and matching headers, which Crowdin can handle.""" | 293 """Create a post body and matching headers, which Crowdin can handle.""" |
355 boundary = '----------ThIs_Is_tHe_bouNdaRY_$' | 294 boundary = '----------ThIs_Is_tHe_bouNdaRY_$' |
356 body = '' | 295 body = '' |
357 for name, data in files: | 296 for name, data in files: |
358 body += ( | 297 body += ( |
359 '--{boundary}\r\n' | 298 '--{boundary}\r\n' |
(...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
433 data = json.dumps({file: {'message': fileHandle.read()}}) | 372 data = json.dumps({file: {'message': fileHandle.read()}}) |
434 fileHandle.close() | 373 fileHandle.close() |
435 newName = file + '.json' | 374 newName = file + '.json' |
436 else: | 375 else: |
437 data = toJSON(path) | 376 data = toJSON(path) |
438 newName = file + '.json' | 377 newName = file + '.json' |
439 | 378 |
440 if data: | 379 if data: |
441 files.append((newName, data)) | 380 files.append((newName, data)) |
442 if len(files): | 381 if len(files): |
443 language = mapLocale(localeConfig['name_format'], locale) | 382 language = CROWDIN_LANG_MAPPING.get(locale, locale) |
444 data, headers = crowdin_prepare_upload(files) | 383 data, headers = crowdin_prepare_upload(files) |
445 crowdin_request(projectName, 'upload-translation', key, | 384 crowdin_request(projectName, 'upload-translation', key, |
446 {'language': language}, post_data=data, | 385 {'language': language}, post_data=data, |
447 headers=headers) | 386 headers=headers) |
448 | 387 |
449 | 388 |
450 def getTranslations(localeConfig, projectName, key): | 389 def getTranslations(localeConfig, projectName, key): |
451 """Download all available translations from crowdin. | 390 """Download all available translations from crowdin. |
452 | 391 |
453 Trigger crowdin to build the available export, wait for crowdin to | 392 Trigger crowdin to build the available export, wait for crowdin to |
454 finish the job and download the generated zip afterwards. | 393 finish the job and download the generated zip afterwards. |
455 """ | 394 """ |
456 crowdin_request(projectName, 'export', key) | 395 crowdin_request(projectName, 'export', key) |
457 | 396 |
458 result = crowdin_request(projectName, 'download/all.zip', key, raw=True) | 397 result = crowdin_request(projectName, 'download/all.zip', key, raw=True) |
459 zip = ZipFile(StringIO(result)) | 398 zip = ZipFile(StringIO(result)) |
460 dirs = {} | 399 dirs = {} |
461 | 400 |
462 normalizedDefaultLocale = localeConfig['default_locale'] | 401 normalizedDefaultLocale = localeConfig['default_locale'] |
463 if localeConfig['name_format'] == 'ISO-15897': | 402 if localeConfig['name_format'] == 'ISO-15897': |
464 normalizedDefaultLocale = normalizedDefaultLocale.replace('_', '-') | 403 normalizedDefaultLocale = normalizedDefaultLocale.replace('_', '-') |
465 normalizedDefaultLocale = mapLocale(localeConfig['name_format'], | 404 normalizedDefaultLocale = CROWDIN_LANG_MAPPING.get(normalizedDefaultLocale, |
466 normalizedDefaultLocale) | 405 normalizedDefaultLocale) |
467 | 406 |
468 for info in zip.infolist(): | 407 for info in zip.infolist(): |
469 if not info.filename.endswith('.json'): | 408 if not info.filename.endswith('.json'): |
470 continue | 409 continue |
471 | 410 |
472 dir, file = os.path.split(info.filename) | 411 dir, file = os.path.split(info.filename) |
473 if not re.match(r'^[\w\-]+$', dir) or dir == normalizedDefaultLocale: | 412 if not re.match(r'^[\w\-]+$', dir) or dir == normalizedDefaultLocale: |
474 continue | 413 continue |
475 if localeConfig['file_format'] == 'chrome-json' and file.count('.') == 1 : | 414 if localeConfig['file_format'] == 'chrome-json' and file.count('.') == 1 : |
476 origFile = file | 415 origFile = file |
477 else: | 416 else: |
478 origFile = re.sub(r'\.json$', '', file) | 417 origFile = re.sub(r'\.json$', '', file) |
479 if (localeConfig['file_format'] == 'gecko-dtd' and | 418 if (localeConfig['file_format'] == 'gecko-dtd' and |
480 not origFile.endswith('.dtd') and | 419 not origFile.endswith('.dtd') and |
481 not origFile.endswith('.properties')): | 420 not origFile.endswith('.properties')): |
482 continue | 421 continue |
483 | 422 |
484 if localeConfig['name_format'] == 'ISO-15897': | 423 for key, value in CROWDIN_LANG_MAPPING.iteritems(): |
485 mapping = langMappingChrome | |
486 else: | |
487 mapping = langMappingGecko | |
488 | |
489 for key, value in mapping.iteritems(): | |
490 if value == dir: | 424 if value == dir: |
491 dir = key | 425 dir = key |
492 if localeConfig['name_format'] == 'ISO-15897': | 426 if localeConfig['name_format'] == 'ISO-15897': |
493 dir = dir.replace('-', '_') | 427 dir = dir.replace('-', '_') |
494 | 428 |
495 data = zip.open(info.filename).read() | 429 data = zip.open(info.filename).read() |
496 if data == '[]': | 430 if data == '[]': |
497 continue | 431 continue |
498 | 432 |
499 if not dir in dirs: | 433 if not dir in dirs: |
(...skipping 16 matching lines...) Expand all Loading... | |
516 | 450 |
517 # Remove any extra files | 451 # Remove any extra files |
518 for dir, files in dirs.iteritems(): | 452 for dir, files in dirs.iteritems(): |
519 baseDir = os.path.join(localeConfig['base_path'], dir) | 453 baseDir = os.path.join(localeConfig['base_path'], dir) |
520 if not os.path.exists(baseDir): | 454 if not os.path.exists(baseDir): |
521 continue | 455 continue |
522 for file in os.listdir(baseDir): | 456 for file in os.listdir(baseDir): |
523 path = os.path.join(baseDir, file) | 457 path = os.path.join(baseDir, file) |
524 if os.path.isfile(path) and (file.endswith('.json') or file.endswith ('.properties') or file.endswith('.dtd')) and not file in files: | 458 if os.path.isfile(path) and (file.endswith('.json') or file.endswith ('.properties') or file.endswith('.dtd')) and not file in files: |
525 os.remove(path) | 459 os.remove(path) |
LEFT | RIGHT |