Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code

Delta Between Two Patch Sets: localeTools.py

Issue 29556601: Issue 5777 - Update crowdin interface (Closed)
Left Patch Set: Refactoring postFiles, addressing comments Created Sept. 26, 2017, 2:38 p.m.
Right Patch Set: Mimetype, PEP-8 Created Sept. 29, 2017, 9:07 a.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « no previous file | no next file » | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
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 from StringIO import StringIO 14 from StringIO import StringIO
14 from ConfigParser import SafeConfigParser 15 from ConfigParser import SafeConfigParser
15 from zipfile import ZipFile 16 from zipfile import ZipFile
16 from xml.parsers.expat import ParserCreate, XML_PARAM_ENTITY_PARSING_ALWAYS 17 from xml.parsers.expat import ParserCreate, XML_PARAM_ENTITY_PARSING_ALWAYS
17 18
18 langMappingGecko = { 19 langMappingGecko = {
19 'bn-BD': 'bn', 20 'bn-BD': 'bn',
20 'br': 'br-FR', 21 'br': 'br-FR',
21 'dsb': 'dsb-DE', 22 'dsb': 'dsb-DE',
22 'fj-FJ': 'fj', 23 'fj-FJ': 'fj',
(...skipping 63 matching lines...) Expand 10 before | Expand all | Expand 10 after
86 'ta', 87 'ta',
87 'te', 88 'te',
88 'th', 89 'th',
89 'tr', 90 'tr',
90 'uk', 91 'uk',
91 'vi', 92 'vi',
92 'zh-CN', 93 'zh-CN',
93 'zh-TW', 94 'zh-TW',
94 ] 95 ]
95 96
96 CROWDIN_AP_URL = 'https://api.crowdin.com/api/project/{}/{}' 97 CROWDIN_AP_URL = 'https://api.crowdin.com/api/project'
97 98
98 99
99 def crowdin_url(project_name, action, key, get={}): 100 def crowdin_request(project_name, action, key, get={}, post_data=None,
100 """Create a valid url for a crowdin endpoint.""" 101 headers={}, raw=False):
101 url = CROWDIN_AP_URL.format(project_name, action)
102 get['key'] = key
103
104 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
105
106 query = dict(urlparse.parse_qsl(query))
107 query.update(get)
108
109 return urlparse.urlunparse((
110 scheme, netloc, path, params, urllib.urlencode(query), fragment
111 ))
112
113
114 def _crowdin_request(project_name, action, key, get={}, post_data=None,
115 headers={}, raises=False):
116 """Perform a call to crowdin and raise an Exception on failure.""" 102 """Perform a call to crowdin and raise an Exception on failure."""
117
118 request = urllib2.Request( 103 request = urllib2.Request(
119 crowdin_url(project_name, action, key, get), 104 '{}/{}/{}?{}'.format(CROWDIN_AP_URL,
105 urllib.quote(project_name),
106 urllib.quote(action),
107 urllib.urlencode(dict(get, key=key, json=1))),
120 post_data, 108 post_data,
121 headers, 109 headers,
122 ) 110 )
123 111
124 result = urllib2.urlopen(request).read() 112 try:
125 113 result = urllib2.urlopen(request).read()
126 if raises and result.find('<success') < 0: 114 except urllib2.HTTPError as e:
127 raise Exception( 115 raise Exception('Server returned HTTP Error {}:\n{}'.format(e.code,
128 'Server indicated that the operation was not successful\n' + result 116 e.read()))
129 ) 117
130 118 if not raw:
131 if 'json' in get: 119 return json.loads(result)
132 result = json.loads(result)
133 120
134 return result 121 return result
135
136
137 def crowdin_get(*args, **kwargs):
138 return _crowdin_request(*args, **kwargs)
139
140
141 crowdin_post = _crowdin_request
142 122
143 123
144 class OrderedDict(dict): 124 class OrderedDict(dict):
145 def __init__(self): 125 def __init__(self):
146 self.__order = [] 126 self.__order = []
147 127
148 def __setitem__(self, key, value): 128 def __setitem__(self, key, value):
149 self.__order.append(key) 129 self.__order.append(key)
150 dict.__setitem__(self, key, value) 130 dict.__setitem__(self, key, value)
151 131
(...skipping 186 matching lines...) Expand 10 before | Expand all | Expand 10 after
338 for match in re.finditer(r'&amp;lang=([\w\-]+)"', firefoxLocales): 318 for match in re.finditer(r'&amp;lang=([\w\-]+)"', firefoxLocales):
339 locales.add(mapLocale('BCP-47', match.group(1))) 319 locales.add(mapLocale('BCP-47', match.group(1)))
340 langPacks = urllib2.urlopen('https://addons.mozilla.org/en-US/firefox/la nguage-tools/').read() 320 langPacks = urllib2.urlopen('https://addons.mozilla.org/en-US/firefox/la nguage-tools/').read()
341 for match in re.finditer(r'<tr>.*?</tr>', langPacks, re.S): 321 for match in re.finditer(r'<tr>.*?</tr>', langPacks, re.S):
342 if match.group(0).find('Install Language Pack') >= 0: 322 if match.group(0).find('Install Language Pack') >= 0:
343 match2 = re.search(r'lang="([\w\-]+)"', match.group(0)) 323 match2 = re.search(r'lang="([\w\-]+)"', match.group(0))
344 if match2: 324 if match2:
345 locales.add(mapLocale('BCP-47', match2.group(1))) 325 locales.add(mapLocale('BCP-47', match2.group(1)))
346 326
347 allowed = set() 327 allowed = set()
348 allowedLocales = crowdin_get(projectName, 'supported-languages', key, 328 allowedLocales = crowdin_request(projectName, 'supported-languages', key)
349 {'json': 1})
350 329
351 for locale in allowedLocales: 330 for locale in allowedLocales:
352 allowed.add(locale['crowdin_code']) 331 allowed.add(locale['crowdin_code'])
353 if not allowed.issuperset(locales): 332 if not allowed.issuperset(locales):
354 print "Warning, following locales aren't allowed by server: " + ', '.joi n(locales - allowed) 333 print "Warning, following locales aren't allowed by server: " + ', '.joi n(locales - allowed)
355 334
356 locales = list(locales & allowed) 335 locales = list(locales & allowed)
357 locales.sort() 336 locales.sort()
358 params = urllib.urlencode([('languages[]', locale) for locale in locales]) 337 params = urllib.urlencode([('languages[]', locale) for locale in locales])
359 338
360 crowdin_post(projectName, 'edit-project', key, post_data=params, 339 crowdin_request(projectName, 'edit-project', key, post_data=params)
361 raises=True) 340
362 341
363 342 def crowdin_prepare_upload(files):
364 def crowdin_body_headers(files): 343 """Create a post body and matching headers, which Crowdin can handle."""
365 """Create a post body and according headers, which Crowdin can handle."""
366 boundary = '----------ThIs_Is_tHe_bouNdaRY_$' 344 boundary = '----------ThIs_Is_tHe_bouNdaRY_$'
367 body = '' 345 body = ''
368 for file, data in files: 346 for name, data in files:
369 body += '--%s\r\n' % boundary 347 mimetype = mimetypes.guess_type(name)[0]
370 body += 'Content-Disposition: form-data; name="files[%s]"; filename="%s" \r\n' % (file, file) 348 body += (
371 body += 'Content-Type: application/octet-stream\r\n' 349 '--{boundary}\r\n'
372 body += 'Content-Transfer-Encoding: binary\r\n' 350 'Content-Disposition: form-data; name="files[{name}]"; '
373 body += '\r\n' + data + '\r\n' 351 'filename="{name}"\r\n'
374 body += '--%s--\r\n' % boundary 352 'Content-Type: {mimetype}; charset=utf-8\r\n'
353 'Content-Transfer-Encoding: binary\r\n'
354 '\r\n{data}\r\n'
355 '--{boundary}--\r\n'
356 ).format(boundary=boundary, name=name, data=data, mimetype=mimetype)
375 357
376 body = body.encode('utf-8') 358 body = body.encode('utf-8')
377 return ( 359 return (
378 StringIO(body), 360 StringIO(body),
379 { 361 {
380 'Content-Type': 'multipart/form-data; boundary=%s' % boundary, 362 'Content-Type': ('multipart/form-data; boundary=' + boundary),
381 'Content-Length': len(body) 363 'Content-Length': len(body)
382 } 364 },
383 ) 365 )
384 366
385 367
386 def updateTranslationMaster(localeConfig, metadata, dir, projectName, key): 368 def updateTranslationMaster(localeConfig, metadata, dir, projectName, key):
387 result = crowdin_get(projectName, 'info', key, {'json': 1}) 369 result = crowdin_request(projectName, 'info', key)
388 370
389 existing = set(map(lambda f: f['name'], result['files'])) 371 existing = set(map(lambda f: f['name'], result['files']))
390 add = [] 372 add = []
391 update = [] 373 update = []
392 for file in os.listdir(dir): 374 for file in os.listdir(dir):
393 path = os.path.join(dir, file) 375 path = os.path.join(dir, file)
394 if os.path.isfile(path): 376 if os.path.isfile(path):
395 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'): 377 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'):
396 data = preprocessChromeLocale(path, metadata, True) 378 data = preprocessChromeLocale(path, metadata, True)
397 newName = file 379 newName = file
398 elif localeConfig['file_format'] == 'chrome-json': 380 elif localeConfig['file_format'] == 'chrome-json':
399 fileHandle = codecs.open(path, 'rb', encoding='utf-8') 381 fileHandle = codecs.open(path, 'rb', encoding='utf-8')
400 data = json.dumps({file: {'message': fileHandle.read()}}) 382 data = json.dumps({file: {'message': fileHandle.read()}})
401 fileHandle.close() 383 fileHandle.close()
402 newName = file + '.json' 384 newName = file + '.json'
403 else: 385 else:
404 data = toJSON(path) 386 data = toJSON(path)
405 newName = file + '.json' 387 newName = file + '.json'
406 388
407 if data: 389 if data:
408 if newName in existing: 390 if newName in existing:
409 update.append((newName, data)) 391 update.append((newName, data))
410 existing.remove(newName) 392 existing.remove(newName)
411 else: 393 else:
412 add.append((newName, data)) 394 add.append((newName, data))
413 395
414 if len(add): 396 if len(add):
415 data = {'titles[{}]'.format(name): re.sub(r'\.json', '', name) 397 query = {'titles[{}]'.format(name): os.path.splitext(name)[0]
416 for name, data in add} 398 for name, _ in add}
417 data['type'] = 'chrome' 399 query['type'] = 'chrome'
418 data, headers = crowdin_body_headers(add) 400 data, headers = crowdin_prepare_upload(add)
419 crowdin_post(projectName, 'add-file', key, post_data=data, 401 crowdin_request(projectName, 'add-file', key, query, post_data=data,
420 headers=headers, raises=True) 402 headers=headers)
421 if len(update): 403 if len(update):
422 data, headers = crowdin_body_headers(update) 404 data, headers = crowdin_prepare_upload(update)
423 crowdin_post(projectName, 'update-file', key, post_data=data, 405 crowdin_request(projectName, 'update-file', key, post_data=data,
424 headers=headers, raises=True) 406 headers=headers)
425 for file in existing: 407 for file in existing:
426 crowdin_get(projectName, 'delete-file', key, {'file': file}) 408 crowdin_request(projectName, 'delete-file', key, {'file': file})
427 409
428 410
429 def uploadTranslations(localeConfig, metadata, dir, locale, projectName, key): 411 def uploadTranslations(localeConfig, metadata, dir, locale, projectName, key):
430 files = [] 412 files = []
431 for file in os.listdir(dir): 413 for file in os.listdir(dir):
432 path = os.path.join(dir, file) 414 path = os.path.join(dir, file)
433 if os.path.isfile(path): 415 if os.path.isfile(path):
434 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'): 416 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'):
435 data = preprocessChromeLocale(path, metadata, False) 417 data = preprocessChromeLocale(path, metadata, False)
436 newName = file 418 newName = file
437 elif localeConfig['file_format'] == 'chrome-json': 419 elif localeConfig['file_format'] == 'chrome-json':
438 fileHandle = codecs.open(path, 'rb', encoding='utf-8') 420 fileHandle = codecs.open(path, 'rb', encoding='utf-8')
439 data = json.dumps({file: {'message': fileHandle.read()}}) 421 data = json.dumps({file: {'message': fileHandle.read()}})
440 fileHandle.close() 422 fileHandle.close()
441 newName = file + '.json' 423 newName = file + '.json'
442 else: 424 else:
443 data = toJSON(path) 425 data = toJSON(path)
444 newName = file + '.json' 426 newName = file + '.json'
445 427
446 if data: 428 if data:
447 files.append((newName, data)) 429 files.append((newName, data))
448 if len(files): 430 if len(files):
449 language = mapLocale(localeConfig['name_format'], locale) 431 language = mapLocale(localeConfig['name_format'], locale)
450 data, headers = crowdin_body_headers(files) 432 data, headers = crowdin_prepare_upload(files)
451 crowdin_post(projectName, 'upload-translation', key, 433 crowdin_request(projectName, 'upload-translation', key,
452 {'language': language}, post_data=data, 434 {'language': language}, post_data=data,
453 headers=headers, raises=True) 435 headers=headers)
454 436
455 437
456 def getTranslations(localeConfig, projectName, key): 438 def getTranslations(localeConfig, projectName, key):
457 # let crowdin build the project 439 """Download all available translations from crowdin.
458 crowdin_get(projectName, 'export', key, raises=True) 440
459 441 Trigger crowdin to build the available export, wait for crowdin to
460 result = crowdin_get(projectName, 'download/all.zip', key) 442 finish the job and download the generated zip afterwards.
443 """
444 crowdin_request(projectName, 'export', key)
445
446 result = crowdin_request(projectName, 'download/all.zip', key, raw=True)
461 zip = ZipFile(StringIO(result)) 447 zip = ZipFile(StringIO(result))
462 dirs = {} 448 dirs = {}
463 449
464 normalizedDefaultLocale = localeConfig['default_locale'] 450 normalizedDefaultLocale = localeConfig['default_locale']
465 if localeConfig['name_format'] == 'ISO-15897': 451 if localeConfig['name_format'] == 'ISO-15897':
466 normalizedDefaultLocale = normalizedDefaultLocale.replace('_', '-') 452 normalizedDefaultLocale = normalizedDefaultLocale.replace('_', '-')
467 normalizedDefaultLocale = mapLocale(localeConfig['name_format'], 453 normalizedDefaultLocale = mapLocale(localeConfig['name_format'],
468 normalizedDefaultLocale) 454 normalizedDefaultLocale)
469 455
470 for info in zip.infolist(): 456 for info in zip.infolist():
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after
518 504
519 # Remove any extra files 505 # Remove any extra files
520 for dir, files in dirs.iteritems(): 506 for dir, files in dirs.iteritems():
521 baseDir = os.path.join(localeConfig['base_path'], dir) 507 baseDir = os.path.join(localeConfig['base_path'], dir)
522 if not os.path.exists(baseDir): 508 if not os.path.exists(baseDir):
523 continue 509 continue
524 for file in os.listdir(baseDir): 510 for file in os.listdir(baseDir):
525 path = os.path.join(baseDir, file) 511 path = os.path.join(baseDir, file)
526 if os.path.isfile(path) and (file.endswith('.json') or file.endswith ('.properties') or file.endswith('.dtd')) and not file in files: 512 if os.path.isfile(path) and (file.endswith('.json') or file.endswith ('.properties') or file.endswith('.dtd')) and not file in files:
527 os.remove(path) 513 os.remove(path)
LEFTRIGHT
« no previous file | no next file » | Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Toggle Comments ('s')

Powered by Google App Engine
This is Rietveld