 Issue 29561557:
  Issue 5763 - Target languages supported by Firefox  (Closed)
    
  
    Issue 29561557:
  Issue 5763 - Target languages supported by Firefox  (Closed) 
  | Left: | ||
| Right: | 
| OLD | NEW | 
|---|---|
| 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_AP_URL = 'https://api.crowdin.com/api/project' | 
| 20 'bn-BD': 'bn', | 20 | 
| 21 CROWDIN_LANG_MAPPING = { | |
| 21 'br': 'br-FR', | 22 'br': 'br-FR', | 
| 22 'dsb': 'dsb-DE', | 23 'dsb': 'dsb-DE', | 
| 23 'fj-FJ': 'fj', | 24 'es': 'es-ES', | 
| 25 'fur': 'fur-IT', | |
| 26 'fy': 'fy-NL', | |
| 27 'ga': 'ga-IE', | |
| 28 'gu': 'gu-IN', | |
| 24 'hsb': 'hsb-DE', | 29 'hsb': 'hsb-DE', | 
| 25 'hi-IN': 'hi', | 30 'hy': 'hy-AM', | 
| 26 'ml': 'ml-IN', | 31 'ml': 'ml-IN', | 
| 27 'nb-NO': 'nb', | 32 'nn': 'nn-NO', | 
| 33 'pa': 'pa-IN', | |
| 28 'rm': 'rm-CH', | 34 'rm': 'rm-CH', | 
| 29 'ta-LK': 'ta', | 35 'si': 'si-LK', | 
| 30 'wo-SN': 'wo', | 36 'sv': 'sv-SE', | 
| 37 'ur': 'ur-PK', | |
| 31 } | 38 } | 
| 32 | 39 | 
| 33 langMappingChrome = { | |
| 34 'es-419': 'es-MX', | |
| 35 'es': 'es-ES', | |
| 36 'sv': 'sv-SE', | |
| 37 'ml': 'ml-IN', | |
| 38 'gu': 'gu-IN', | |
| 39 } | |
| 40 | |
| 41 chromeLocales = [ | |
| 42 'am', | |
| 43 'ar', | |
| 44 'bg', | |
| 45 'bn', | |
| 46 'ca', | |
| 47 'cs', | |
| 48 'da', | |
| 49 'de', | |
| 50 'el', | |
| 51 'en-GB', | |
| 52 'en-US', | |
| 53 'es-419', | |
| 54 'es', | |
| 55 'et', | |
| 56 'fa', | |
| 57 'fi', | |
| 58 'fil', | |
| 59 'fr', | |
| 60 'gu', | |
| 61 'he', | |
| 62 'hi', | |
| 63 'hr', | |
| 64 'hu', | |
| 65 'id', | |
| 66 'it', | |
| 67 'ja', | |
| 68 'kn', | |
| 69 'ko', | |
| 70 'lt', | |
| 71 'lv', | |
| 72 'ml', | |
| 73 'mr', | |
| 74 'ms', | |
| 75 'nb', | |
| 76 'nl', | |
| 77 'pl', | |
| 78 'pt-BR', | |
| 79 'pt-PT', | |
| 80 'ro', | |
| 81 'ru', | |
| 82 'sk', | |
| 83 'sl', | |
| 84 'sr', | |
| 85 'sv', | |
| 86 'sw', | |
| 87 'ta', | |
| 88 'te', | |
| 89 'th', | |
| 90 'tr', | |
| 91 'uk', | |
| 92 'vi', | |
| 93 'zh-CN', | |
| 94 'zh-TW', | |
| 95 ] | |
| 96 | |
| 97 CROWDIN_AP_URL = 'https://api.crowdin.com/api/project' | |
| 98 | |
| 99 | 40 | 
| 100 def crowdin_request(project_name, action, key, get={}, post_data=None, | 41 def crowdin_request(project_name, action, key, get={}, post_data=None, | 
| 101 headers={}, raw=False): | 42 headers={}, raw=False): | 
| 102 """Perform a call to crowdin and raise an Exception on failure.""" | 43 """Perform a call to crowdin and raise an Exception on failure.""" | 
| 103 request = urllib2.Request( | 44 request = urllib2.Request( | 
| 104 '{}/{}/{}?{}'.format(CROWDIN_AP_URL, | 45 '{}/{}/{}?{}'.format(CROWDIN_AP_URL, | 
| 105 urllib.quote(project_name), | 46 urllib.quote(project_name), | 
| 106 urllib.quote(action), | 47 urllib.quote(action), | 
| 107 urllib.urlencode(dict(get, key=key, json=1))), | 48 urllib.urlencode(dict(get, key=key, json=1))), | 
| 108 post_data, | 49 post_data, | 
| (...skipping 29 matching lines...) Expand all Loading... | |
| 138 | 79 | 
| 139 | 80 | 
| 140 def escapeEntity(value): | 81 def escapeEntity(value): | 
| 141 return value.replace('&', '&').replace('<', '<').replace('>', '>') .replace('"', '"') | 82 return value.replace('&', '&').replace('<', '<').replace('>', '>') .replace('"', '"') | 
| 142 | 83 | 
| 143 | 84 | 
| 144 def unescapeEntity(value): | 85 def unescapeEntity(value): | 
| 145 return value.replace('&', '&').replace('<', '<').replace('>', '>') .replace('"', '"') | 86 return value.replace('&', '&').replace('<', '<').replace('>', '>') .replace('"', '"') | 
| 146 | 87 | 
| 147 | 88 | 
| 148 def mapLocale(type, locale): | |
| 149 mapping = langMappingChrome if type == 'ISO-15897' else langMappingGecko | |
| 150 return mapping.get(locale, locale) | |
| 151 | |
| 152 | |
| 153 def parseDTDString(data, path): | 89 def parseDTDString(data, path): | 
| 154 result = [] | 90 result = [] | 
| 155 currentComment = [None] | 91 currentComment = [None] | 
| 156 | 92 | 
| 157 parser = ParserCreate() | 93 parser = ParserCreate() | 
| 158 parser.UseForeignDTD(True) | 94 parser.UseForeignDTD(True) | 
| 159 parser.SetParamEntityParsing(XML_PARAM_ENTITY_PARSING_ALWAYS) | 95 parser.SetParamEntityParsing(XML_PARAM_ENTITY_PARSING_ALWAYS) | 
| 160 | 96 | 
| 161 def ExternalEntityRefHandler(context, base, systemId, publicId): | 97 def ExternalEntityRefHandler(context, base, systemId, publicId): | 
| 162 subparser = parser.ExternalEntityParserCreate(context, 'utf-8') | 98 subparser = parser.ExternalEntityParserCreate(context, 'utf-8') | 
| (...skipping 134 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 297 for key, value in parsed.iteritems(): | 233 for key, value in parsed.iteritems(): | 
| 298 if 'description' in value: | 234 if 'description' in value: | 
| 299 del value['description'] | 235 del value['description'] | 
| 300 | 236 | 
| 301 file = codecs.open(path, 'wb', encoding='utf-8') | 237 file = codecs.open(path, 'wb', encoding='utf-8') | 
| 302 json.dump(parsed, file, ensure_ascii=False, sort_keys=True, indent=2, separa tors=(',', ': ')) | 238 json.dump(parsed, file, ensure_ascii=False, sort_keys=True, indent=2, separa tors=(',', ': ')) | 
| 303 file.close() | 239 file.close() | 
| 304 | 240 | 
| 305 | 241 | 
| 306 def setupTranslations(localeConfig, projectName, key): | 242 def setupTranslations(localeConfig, projectName, key): | 
| 307 # Make a new set from the locales list, mapping to Crowdin friendly format | 243 locales = set() | 
| 308 locales = {mapLocale(localeConfig['name_format'], locale) | |
| 309 for locale in localeConfig['locales']} | |
| 310 | 244 | 
| 311 # Fill up with locales that we don't have but the browser supports | 245 # Languages supported by Firefox | 
| 312 if 'chrome' in localeConfig['target_platforms']: | 246 data = urllib2.urlopen('http://www.mozilla.org/en-US/firefox/all.html').read () | 
| 313 for locale in chromeLocales: | 247 for match in re.finditer(r'&lang=([\w\-]+)"', data): | 
| 314 locales.add(mapLocale('ISO-15897', locale)) | 248 locales.add(match.group(1)) | 
| 315 | 249 | 
| 316 if 'gecko' in localeConfig['target_platforms']: | 250 # Languages supported by Firefox Language Packs | 
| 317 firefoxLocales = urllib2.urlopen('http://www.mozilla.org/en-US/firefox/a ll.html').read() | 251 data = urllib2.urlopen('https://addons.mozilla.org/en-US/firefox/language-to ols/').read() | 
| 318 for match in re.finditer(r'&lang=([\w\-]+)"', firefoxLocales): | 252 for match in re.finditer(r'<tr>.*?</tr>', data, re.S): | 
| 319 locales.add(mapLocale('BCP-47', match.group(1))) | 253 if match.group(0).find('Install Language Pack') >= 0: | 
| 320 langPacks = urllib2.urlopen('https://addons.mozilla.org/en-US/firefox/la nguage-tools/').read() | 254 match2 = re.search(r'lang="([\w\-]+)"', match.group(0)) | 
| 321 for match in re.finditer(r'<tr>.*?</tr>', langPacks, re.S): | 255 if match2: | 
| 322 if match.group(0).find('Install Language Pack') >= 0: | 256 locales.add(match2.group(1)) | 
| 323 match2 = re.search(r'lang="([\w\-]+)"', match.group(0)) | |
| 324 if match2: | |
| 325 locales.add(mapLocale('BCP-47', match2.group(1))) | |
| 326 | 257 | 
| 327 allowed = set() | 258 # Languages supported by Chrome (excluding es-419) | 
| 328 allowedLocales = crowdin_request(projectName, 'supported-languages', key) | 259 data = urllib2.urlopen('https://packages.debian.org/sid/all/chromium-l10n/fi lelist').read() | 
| 260 for match in re.finditer(r'locales/(?!es-419)([\w\-]+)\.pak', data): | |
| 261 locales.add(match.group(1)) | |
| 329 | 262 | 
| 330 for locale in allowedLocales: | 263 # We don't translate indvidual dialects of languages | 
| 331 allowed.add(locale['crowdin_code']) | 264 # other than English, Spanish, Portuguese and Chinese. | 
| 265 for locale in list(locales): | |
| 266 prefix = locale.split('-')[0] | |
| 267 if prefix not in {'en', 'es', 'pt', 'zh'}: | |
| 268 locales.remove(locale) | |
| 269 locales.add(prefix) | |
| 270 | |
| 271 # Add languages with existing translations. | |
| 
tlucas
2017/10/04 09:37:39
flake8 fails on this line: W291 trailing whitespac
 
Sebastian Noack
2017/10/04 21:33:50
Done.
 | |
| 272 locales.update(localeConfig['locales']) | |
| 273 | |
| 274 # Don't add the language we translate from as target translation. | |
| 275 locales.remove(localeConfig['default_locale'].replace('_', '-')) | |
| 276 | |
| 277 # Convert to locales understood by Crowdin. | |
| 278 locales = {CROWDIN_LANG_MAPPING.get(locale, locale) for locale in locales} | |
| 279 allowed = {locale['crowdin_code'] for locale in | |
| 280 crowdin_request(projectName, 'supported-languages', key)} | |
| 332 if not allowed.issuperset(locales): | 281 if not allowed.issuperset(locales): | 
| 333 print "Warning, following locales aren't allowed by server: " + ', '.joi n(locales - allowed) | 282 print "Warning, following locales aren't allowed by server: " + ', '.joi n(locales - allowed) | 
| 334 | 283 | 
| 335 locales = list(locales & allowed) | 284 locales = sorted(locales & allowed) | 
| 336 locales.sort() | |
| 337 params = urllib.urlencode([('languages[]', locale) for locale in locales]) | 285 params = urllib.urlencode([('languages[]', locale) for locale in locales]) | 
| 338 | |
| 339 crowdin_request(projectName, 'edit-project', key, post_data=params) | 286 crowdin_request(projectName, 'edit-project', key, post_data=params) | 
| 340 | 287 | 
| 341 | 288 | 
| 342 def crowdin_prepare_upload(files): | 289 def crowdin_prepare_upload(files): | 
| 343 """Create a post body and matching headers, which Crowdin can handle.""" | 290 """Create a post body and matching headers, which Crowdin can handle.""" | 
| 344 boundary = '----------ThIs_Is_tHe_bouNdaRY_$' | 291 boundary = '----------ThIs_Is_tHe_bouNdaRY_$' | 
| 345 body = '' | 292 body = '' | 
| 346 for name, data in files: | 293 for name, data in files: | 
| 347 body += ( | 294 body += ( | 
| 348 '--{boundary}\r\n' | 295 '--{boundary}\r\n' | 
| (...skipping 73 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 422 data = json.dumps({file: {'message': fileHandle.read()}}) | 369 data = json.dumps({file: {'message': fileHandle.read()}}) | 
| 423 fileHandle.close() | 370 fileHandle.close() | 
| 424 newName = file + '.json' | 371 newName = file + '.json' | 
| 425 else: | 372 else: | 
| 426 data = toJSON(path) | 373 data = toJSON(path) | 
| 427 newName = file + '.json' | 374 newName = file + '.json' | 
| 428 | 375 | 
| 429 if data: | 376 if data: | 
| 430 files.append((newName, data)) | 377 files.append((newName, data)) | 
| 431 if len(files): | 378 if len(files): | 
| 432 language = mapLocale(localeConfig['name_format'], locale) | 379 language = CROWDIN_LANG_MAPPING.get(locale, locale) | 
| 433 data, headers = crowdin_prepare_upload(files) | 380 data, headers = crowdin_prepare_upload(files) | 
| 434 crowdin_request(projectName, 'upload-translation', key, | 381 crowdin_request(projectName, 'upload-translation', key, | 
| 435 {'language': language}, post_data=data, | 382 {'language': language}, post_data=data, | 
| 436 headers=headers) | 383 headers=headers) | 
| 437 | 384 | 
| 438 | 385 | 
| 439 def getTranslations(localeConfig, projectName, key): | 386 def getTranslations(localeConfig, projectName, key): | 
| 440 """Download all available translations from crowdin. | 387 """Download all available translations from crowdin. | 
| 441 | 388 | 
| 442 Trigger crowdin to build the available export, wait for crowdin to | 389 Trigger crowdin to build the available export, wait for crowdin to | 
| 443 finish the job and download the generated zip afterwards. | 390 finish the job and download the generated zip afterwards. | 
| 444 """ | 391 """ | 
| 445 crowdin_request(projectName, 'export', key) | 392 crowdin_request(projectName, 'export', key) | 
| 446 | 393 | 
| 447 result = crowdin_request(projectName, 'download/all.zip', key, raw=True) | 394 result = crowdin_request(projectName, 'download/all.zip', key, raw=True) | 
| 448 zip = ZipFile(StringIO(result)) | 395 zip = ZipFile(StringIO(result)) | 
| 449 dirs = {} | 396 dirs = {} | 
| 450 | 397 | 
| 451 normalizedDefaultLocale = localeConfig['default_locale'] | 398 normalizedDefaultLocale = localeConfig['default_locale'] | 
| 452 if localeConfig['name_format'] == 'ISO-15897': | 399 if localeConfig['name_format'] == 'ISO-15897': | 
| 453 normalizedDefaultLocale = normalizedDefaultLocale.replace('_', '-') | 400 normalizedDefaultLocale = normalizedDefaultLocale.replace('_', '-') | 
| 454 normalizedDefaultLocale = mapLocale(localeConfig['name_format'], | 401 normalizedDefaultLocale = CROWDIN_LANG_MAPPING.get(normalizedDefaultLocale, | 
| 455 normalizedDefaultLocale) | 402 normalizedDefaultLocale) | 
| 456 | 403 | 
| 457 for info in zip.infolist(): | 404 for info in zip.infolist(): | 
| 458 if not info.filename.endswith('.json'): | 405 if not info.filename.endswith('.json'): | 
| 459 continue | 406 continue | 
| 460 | 407 | 
| 461 dir, file = os.path.split(info.filename) | 408 dir, file = os.path.split(info.filename) | 
| 462 if not re.match(r'^[\w\-]+$', dir) or dir == normalizedDefaultLocale: | 409 if not re.match(r'^[\w\-]+$', dir) or dir == normalizedDefaultLocale: | 
| 463 continue | 410 continue | 
| 464 if localeConfig['file_format'] == 'chrome-json' and file.count('.') == 1 : | 411 if localeConfig['file_format'] == 'chrome-json' and file.count('.') == 1 : | 
| 465 origFile = file | 412 origFile = file | 
| 466 else: | 413 else: | 
| 467 origFile = re.sub(r'\.json$', '', file) | 414 origFile = re.sub(r'\.json$', '', file) | 
| 468 if (localeConfig['file_format'] == 'gecko-dtd' and | 415 if (localeConfig['file_format'] == 'gecko-dtd' and | 
| 469 not origFile.endswith('.dtd') and | 416 not origFile.endswith('.dtd') and | 
| 470 not origFile.endswith('.properties')): | 417 not origFile.endswith('.properties')): | 
| 471 continue | 418 continue | 
| 472 | 419 | 
| 473 if localeConfig['name_format'] == 'ISO-15897': | 420 for key, value in CROWDIN_LANG_MAPPING.iteritems(): | 
| 474 mapping = langMappingChrome | |
| 475 else: | |
| 476 mapping = langMappingGecko | |
| 477 | |
| 478 for key, value in mapping.iteritems(): | |
| 479 if value == dir: | 421 if value == dir: | 
| 480 dir = key | 422 dir = key | 
| 481 if localeConfig['name_format'] == 'ISO-15897': | 423 if localeConfig['name_format'] == 'ISO-15897': | 
| 482 dir = dir.replace('-', '_') | 424 dir = dir.replace('-', '_') | 
| 483 | 425 | 
| 484 data = zip.open(info.filename).read() | 426 data = zip.open(info.filename).read() | 
| 485 if data == '[]': | 427 if data == '[]': | 
| 486 continue | 428 continue | 
| 487 | 429 | 
| 488 if not dir in dirs: | 430 if not dir in dirs: | 
| (...skipping 16 matching lines...) Expand all Loading... | |
| 505 | 447 | 
| 506 # Remove any extra files | 448 # Remove any extra files | 
| 507 for dir, files in dirs.iteritems(): | 449 for dir, files in dirs.iteritems(): | 
| 508 baseDir = os.path.join(localeConfig['base_path'], dir) | 450 baseDir = os.path.join(localeConfig['base_path'], dir) | 
| 509 if not os.path.exists(baseDir): | 451 if not os.path.exists(baseDir): | 
| 510 continue | 452 continue | 
| 511 for file in os.listdir(baseDir): | 453 for file in os.listdir(baseDir): | 
| 512 path = os.path.join(baseDir, file) | 454 path = os.path.join(baseDir, file) | 
| 513 if os.path.isfile(path) and (file.endswith('.json') or file.endswith ('.properties') or file.endswith('.dtd')) and not file in files: | 455 if os.path.isfile(path) and (file.endswith('.json') or file.endswith ('.properties') or file.endswith('.dtd')) and not file in files: | 
| 514 os.remove(path) | 456 os.remove(path) | 
| OLD | NEW |