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: Purged obsolete distinguishing between crowdin_{get|post} Created Sept. 26, 2017, 2:46 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
99 def crowdin_url(project_name, action, key, get={}):
100 """Create a valid url for a crowdin endpoint."""
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))
Sebastian Noack 2017/09/27 04:04:53 Is there any way that there will be a query part h
tlucas 2017/09/27 10:51:31 Discussion with Vasily: On 2017/09/26 13:11:36, V
Sebastian Noack 2017/09/28 20:25:21 So it seems Vasily agrees with me. Anyway, if you
107 query.update(get)
108
109 return urlparse.urlunparse((
110 scheme, netloc, path, params, urllib.urlencode(query), fragment
111 ))
112 98
113 99
114 def crowdin_request(project_name, action, key, get={}, post_data=None, 100 def crowdin_request(project_name, action, key, get={}, post_data=None,
tlucas 2017/09/26 14:49:05 crowdin_request can perfectly handle both get and
115 headers={}, raises=False): 101 headers={}, raw=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:
Sebastian Noack 2017/09/27 04:04:52 Note that `result.find('<success') < 0` is equival
Sebastian Noack 2017/09/27 04:04:53 Why is a flag necessary to opt-in for error checki
tlucas 2017/09/27 10:51:31 After further investigation, it seems like relying
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 122
136 123
137 class OrderedDict(dict): 124 class OrderedDict(dict):
138 def __init__(self): 125 def __init__(self):
139 self.__order = [] 126 self.__order = []
140 127
141 def __setitem__(self, key, value): 128 def __setitem__(self, key, value):
142 self.__order.append(key) 129 self.__order.append(key)
(...skipping 188 matching lines...) Expand 10 before | Expand all | Expand 10 after
331 for match in re.finditer(r'&amp;lang=([\w\-]+)"', firefoxLocales): 318 for match in re.finditer(r'&amp;lang=([\w\-]+)"', firefoxLocales):
332 locales.add(mapLocale('BCP-47', match.group(1))) 319 locales.add(mapLocale('BCP-47', match.group(1)))
333 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()
334 for match in re.finditer(r'<tr>.*?</tr>', langPacks, re.S): 321 for match in re.finditer(r'<tr>.*?</tr>', langPacks, re.S):
335 if match.group(0).find('Install Language Pack') >= 0: 322 if match.group(0).find('Install Language Pack') >= 0:
336 match2 = re.search(r'lang="([\w\-]+)"', match.group(0)) 323 match2 = re.search(r'lang="([\w\-]+)"', match.group(0))
337 if match2: 324 if match2:
338 locales.add(mapLocale('BCP-47', match2.group(1))) 325 locales.add(mapLocale('BCP-47', match2.group(1)))
339 326
340 allowed = set() 327 allowed = set()
341 allowedLocales = crowdin_request(projectName, 'supported-languages', key, 328 allowedLocales = crowdin_request(projectName, 'supported-languages', key)
342 {'json': 1})
Sebastian Noack 2017/09/27 04:04:53 I wonder what would happen if we always set json=1
tlucas 2017/09/27 10:51:31 There would be only one case where we explicitly e
343 329
344 for locale in allowedLocales: 330 for locale in allowedLocales:
345 allowed.add(locale['crowdin_code']) 331 allowed.add(locale['crowdin_code'])
346 if not allowed.issuperset(locales): 332 if not allowed.issuperset(locales):
347 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)
348 334
349 locales = list(locales & allowed) 335 locales = list(locales & allowed)
350 locales.sort() 336 locales.sort()
351 params = urllib.urlencode([('languages[]', locale) for locale in locales]) 337 params = urllib.urlencode([('languages[]', locale) for locale in locales])
352 338
353 crowdin_request(projectName, 'edit-project', key, post_data=params, 339 crowdin_request(projectName, 'edit-project', key, post_data=params)
354 raises=True) 340
355 341
356 342 def crowdin_prepare_upload(files):
357 def crowdin_body_headers(files): 343 """Create a post body and matching headers, which Crowdin can handle."""
358 """Create a post body and according headers, which Crowdin can handle."""
359 boundary = '----------ThIs_Is_tHe_bouNdaRY_$' 344 boundary = '----------ThIs_Is_tHe_bouNdaRY_$'
360 body = '' 345 body = ''
361 for file, data in files: 346 for name, data in files:
362 body += '--%s\r\n' % boundary 347 mimetype = mimetypes.guess_type(name)[0]
363 body += 'Content-Disposition: form-data; name="files[%s]"; filename="%s" \r\n' % (file, file) 348 body += (
364 body += 'Content-Type: application/octet-stream\r\n' 349 '--{boundary}\r\n'
365 body += 'Content-Transfer-Encoding: binary\r\n' 350 'Content-Disposition: form-data; name="files[{name}]"; '
366 body += '\r\n' + data + '\r\n' 351 'filename="{name}"\r\n'
367 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)
368 357
369 body = body.encode('utf-8') 358 body = body.encode('utf-8')
370 return ( 359 return (
371 StringIO(body), 360 StringIO(body),
372 { 361 {
373 'Content-Type': 'multipart/form-data; boundary=%s' % boundary, 362 'Content-Type': ('multipart/form-data; boundary=' + boundary),
374 'Content-Length': len(body) 363 'Content-Length': len(body)
375 } 364 },
376 ) 365 )
377 366
378 367
379 def updateTranslationMaster(localeConfig, metadata, dir, projectName, key): 368 def updateTranslationMaster(localeConfig, metadata, dir, projectName, key):
380 result = crowdin_request(projectName, 'info', key, {'json': 1}) 369 result = crowdin_request(projectName, 'info', key)
381 370
382 existing = set(map(lambda f: f['name'], result['files'])) 371 existing = set(map(lambda f: f['name'], result['files']))
383 add = [] 372 add = []
384 update = [] 373 update = []
385 for file in os.listdir(dir): 374 for file in os.listdir(dir):
386 path = os.path.join(dir, file) 375 path = os.path.join(dir, file)
387 if os.path.isfile(path): 376 if os.path.isfile(path):
388 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'): 377 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'):
389 data = preprocessChromeLocale(path, metadata, True) 378 data = preprocessChromeLocale(path, metadata, True)
390 newName = file 379 newName = file
391 elif localeConfig['file_format'] == 'chrome-json': 380 elif localeConfig['file_format'] == 'chrome-json':
392 fileHandle = codecs.open(path, 'rb', encoding='utf-8') 381 fileHandle = codecs.open(path, 'rb', encoding='utf-8')
393 data = json.dumps({file: {'message': fileHandle.read()}}) 382 data = json.dumps({file: {'message': fileHandle.read()}})
394 fileHandle.close() 383 fileHandle.close()
395 newName = file + '.json' 384 newName = file + '.json'
396 else: 385 else:
397 data = toJSON(path) 386 data = toJSON(path)
398 newName = file + '.json' 387 newName = file + '.json'
399 388
400 if data: 389 if data:
401 if newName in existing: 390 if newName in existing:
402 update.append((newName, data)) 391 update.append((newName, data))
403 existing.remove(newName) 392 existing.remove(newName)
404 else: 393 else:
405 add.append((newName, data)) 394 add.append((newName, data))
406 395
407 if len(add): 396 if len(add):
408 data = {'titles[{}]'.format(name): re.sub(r'\.json', '', name) 397 query = {'titles[{}]'.format(name): os.path.splitext(name)[0]
Sebastian Noack 2017/09/27 04:04:53 `re.sub(r'\.json', '', name)` is equivalent to `na
tlucas 2017/09/27 10:51:31 I honestly don't know what Wladimir's intention wa
Sebastian Noack 2017/09/28 20:25:21 Yes, if removing each occurrence of the sub-string
409 for name, data in add} 398 for name, _ in add}
410 data['type'] = 'chrome' 399 query['type'] = 'chrome'
411 data, headers = crowdin_body_headers(add) 400 data, headers = crowdin_prepare_upload(add)
412 crowdin_request(projectName, 'add-file', key, post_data=data, 401 crowdin_request(projectName, 'add-file', key, query, post_data=data,
413 headers=headers, raises=True) 402 headers=headers)
414 if len(update): 403 if len(update):
415 data, headers = crowdin_body_headers(update) 404 data, headers = crowdin_prepare_upload(update)
416 crowdin_request(projectName, 'update-file', key, post_data=data, 405 crowdin_request(projectName, 'update-file', key, post_data=data,
417 headers=headers, raises=True) 406 headers=headers)
418 for file in existing: 407 for file in existing:
419 crowdin_request(projectName, 'delete-file', key, {'file': file}) 408 crowdin_request(projectName, 'delete-file', key, {'file': file})
420 409
421 410
422 def uploadTranslations(localeConfig, metadata, dir, locale, projectName, key): 411 def uploadTranslations(localeConfig, metadata, dir, locale, projectName, key):
423 files = [] 412 files = []
424 for file in os.listdir(dir): 413 for file in os.listdir(dir):
425 path = os.path.join(dir, file) 414 path = os.path.join(dir, file)
426 if os.path.isfile(path): 415 if os.path.isfile(path):
427 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'): 416 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'):
428 data = preprocessChromeLocale(path, metadata, False) 417 data = preprocessChromeLocale(path, metadata, False)
429 newName = file 418 newName = file
430 elif localeConfig['file_format'] == 'chrome-json': 419 elif localeConfig['file_format'] == 'chrome-json':
431 fileHandle = codecs.open(path, 'rb', encoding='utf-8') 420 fileHandle = codecs.open(path, 'rb', encoding='utf-8')
432 data = json.dumps({file: {'message': fileHandle.read()}}) 421 data = json.dumps({file: {'message': fileHandle.read()}})
433 fileHandle.close() 422 fileHandle.close()
434 newName = file + '.json' 423 newName = file + '.json'
435 else: 424 else:
436 data = toJSON(path) 425 data = toJSON(path)
437 newName = file + '.json' 426 newName = file + '.json'
438 427
439 if data: 428 if data:
440 files.append((newName, data)) 429 files.append((newName, data))
441 if len(files): 430 if len(files):
442 language = mapLocale(localeConfig['name_format'], locale) 431 language = mapLocale(localeConfig['name_format'], locale)
443 data, headers = crowdin_body_headers(files) 432 data, headers = crowdin_prepare_upload(files)
444 crowdin_request(projectName, 'upload-translation', key, 433 crowdin_request(projectName, 'upload-translation', key,
445 {'language': language}, post_data=data, 434 {'language': language}, post_data=data,
446 headers=headers, raises=True) 435 headers=headers)
447 436
448 437
449 def getTranslations(localeConfig, projectName, key): 438 def getTranslations(localeConfig, projectName, key):
450 # let crowdin build the project 439 """Download all available translations from crowdin.
451 crowdin_request(projectName, 'export', key, raises=True) 440
452 441 Trigger crowdin to build the available export, wait for crowdin to
453 result = crowdin_request(projectName, 'download/all.zip', key) 442 finish the job and download the generated zip afterwards.
tlucas 2017/09/27 10:51:31 This will be the only call expecting the response
443 """
444 crowdin_request(projectName, 'export', key)
445
446 result = crowdin_request(projectName, 'download/all.zip', key, raw=True)
454 zip = ZipFile(StringIO(result)) 447 zip = ZipFile(StringIO(result))
455 dirs = {} 448 dirs = {}
456 449
457 normalizedDefaultLocale = localeConfig['default_locale'] 450 normalizedDefaultLocale = localeConfig['default_locale']
458 if localeConfig['name_format'] == 'ISO-15897': 451 if localeConfig['name_format'] == 'ISO-15897':
459 normalizedDefaultLocale = normalizedDefaultLocale.replace('_', '-') 452 normalizedDefaultLocale = normalizedDefaultLocale.replace('_', '-')
460 normalizedDefaultLocale = mapLocale(localeConfig['name_format'], 453 normalizedDefaultLocale = mapLocale(localeConfig['name_format'],
461 normalizedDefaultLocale) 454 normalizedDefaultLocale)
462 455
463 for info in zip.infolist(): 456 for info in zip.infolist():
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after
511 504
512 # Remove any extra files 505 # Remove any extra files
513 for dir, files in dirs.iteritems(): 506 for dir, files in dirs.iteritems():
514 baseDir = os.path.join(localeConfig['base_path'], dir) 507 baseDir = os.path.join(localeConfig['base_path'], dir)
515 if not os.path.exists(baseDir): 508 if not os.path.exists(baseDir):
516 continue 509 continue
517 for file in os.listdir(baseDir): 510 for file in os.listdir(baseDir):
518 path = os.path.join(baseDir, file) 511 path = os.path.join(baseDir, file)
519 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:
520 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