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: Always require a json-response Created Sept. 27, 2017, 10:49 a.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={}):
kzar 2017/09/28 09:48:29 IIRC default parameter values in Python are shared
tlucas 2017/09/28 10:48:40 As discussed in IRC: You are correct, but the impl
100 """Create a valid url for a crowdin endpoint."""
101 url = CROWDIN_AP_URL.format(project_name, action)
102 get['key'] = key
103 get['json'] = 1
104
105 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url)
106
107 query = dict(urlparse.parse_qsl(query))
108 query.update(get)
109
110 return urlparse.urlunparse((
111 scheme, netloc, path, params, urllib.urlencode(query), fragment
112 ))
113 98
114 99
115 def crowdin_request(project_name, action, key, get={}, post_data=None, 100 def crowdin_request(project_name, action, key, get={}, post_data=None,
116 headers={}, raw=False): 101 headers={}, raw=False):
117 """Perform a call to crowdin and raise an Exception on failure.""" 102 """Perform a call to crowdin and raise an Exception on failure."""
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 try: 112 try:
125 result = urllib2.urlopen(request).read() 113 result = urllib2.urlopen(request).read()
126 except urllib2.HTTPError as e: 114 except urllib2.HTTPError as e:
127 err = e.read() 115 raise Exception('Server returned HTTP Error {}:\n{}'.format(e.code,
128 raise Exception( 116 e.read()))
kzar 2017/09/28 09:48:29 Why catch this and raise it again, does it make th
tlucas 2017/09/28 10:48:40 It does - e.g. if urllib2 raised an HTTPError(400)
kzar 2017/09/28 11:34:44 Cool fair enough. Mind making it display the statu
129 'Server indicated that the operation was not successful\n' + err
130 )
131 117
132 if not raw: 118 if not raw:
133 return json.loads(result) 119 return json.loads(result)
134 120
135 return result 121 return result
136 122
137 123
138 class OrderedDict(dict): 124 class OrderedDict(dict):
139 def __init__(self): 125 def __init__(self):
140 self.__order = [] 126 self.__order = []
(...skipping 205 matching lines...) Expand 10 before | Expand all | Expand 10 after
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 340
355 341
356 def crowdin_body_headers(files): 342 def crowdin_prepare_upload(files):
357 """Create a post body and according headers, which Crowdin can handle.""" 343 """Create a post body and matching headers, which Crowdin can handle."""
358 boundary = '----------ThIs_Is_tHe_bouNdaRY_$' 344 boundary = '----------ThIs_Is_tHe_bouNdaRY_$'
359 body = '' 345 body = ''
360 for file, data in files: 346 for name, data in files:
361 body += '--%s\r\n' % boundary 347 mimetype = mimetypes.guess_type(name)[0]
362 body += 'Content-Disposition: form-data; name="files[%s]"; filename="%s" \r\n' % (file, file) 348 body += (
363 body += 'Content-Type: application/octet-stream\r\n' 349 '--{boundary}\r\n'
364 body += 'Content-Transfer-Encoding: binary\r\n' 350 'Content-Disposition: form-data; name="files[{name}]"; '
365 body += '\r\n' + data + '\r\n' 351 'filename="{name}"\r\n'
366 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)
367 357
368 body = body.encode('utf-8') 358 body = body.encode('utf-8')
369 return ( 359 return (
370 StringIO(body), 360 StringIO(body),
371 { 361 {
372 'Content-Type': 'multipart/form-data; ; charset=utf-8; boundary=' + boundary, 362 'Content-Type': ('multipart/form-data; boundary=' + boundary),
kzar 2017/09/28 09:48:29 This line seems too long, does this code pass lint
tlucas 2017/09/28 10:48:40 It's about time to get rid of tox.ini 'ignore' ent
373 'Content-Length': len(body) 363 'Content-Length': len(body)
374 } 364 },
375 ) 365 )
376 366
377 367
378 def updateTranslationMaster(localeConfig, metadata, dir, projectName, key): 368 def updateTranslationMaster(localeConfig, metadata, dir, projectName, key):
379 result = crowdin_request(projectName, 'info', key) 369 result = crowdin_request(projectName, 'info', key)
380 370
381 existing = set(map(lambda f: f['name'], result['files'])) 371 existing = set(map(lambda f: f['name'], result['files']))
382 add = [] 372 add = []
383 update = [] 373 update = []
384 for file in os.listdir(dir): 374 for file in os.listdir(dir):
(...skipping 12 matching lines...) Expand all
397 newName = file + '.json' 387 newName = file + '.json'
398 388
399 if data: 389 if data:
400 if newName in existing: 390 if newName in existing:
401 update.append((newName, data)) 391 update.append((newName, data))
402 existing.remove(newName) 392 existing.remove(newName)
403 else: 393 else:
404 add.append((newName, data)) 394 add.append((newName, data))
405 395
406 if len(add): 396 if len(add):
407 data = {'titles[{}]'.format(name): re.sub(r'\.json', '', name) 397 query = {'titles[{}]'.format(name): os.path.splitext(name)[0]
408 for name, data in add} 398 for name, _ in add}
409 data['type'] = 'chrome' 399 query['type'] = 'chrome'
410 data, headers = crowdin_body_headers(add) 400 data, headers = crowdin_prepare_upload(add)
411 crowdin_request(projectName, 'add-file', key, post_data=data, 401 crowdin_request(projectName, 'add-file', key, query, post_data=data,
412 headers=headers) 402 headers=headers)
413 if len(update): 403 if len(update):
414 data, headers = crowdin_body_headers(update) 404 data, headers = crowdin_prepare_upload(update)
415 crowdin_request(projectName, 'update-file', key, post_data=data, 405 crowdin_request(projectName, 'update-file', key, post_data=data,
416 headers=headers) 406 headers=headers)
417 for file in existing: 407 for file in existing:
418 crowdin_request(projectName, 'delete-file', key, {'file': file}) 408 crowdin_request(projectName, 'delete-file', key, {'file': file})
419 409
420 410
421 def uploadTranslations(localeConfig, metadata, dir, locale, projectName, key): 411 def uploadTranslations(localeConfig, metadata, dir, locale, projectName, key):
422 files = [] 412 files = []
423 for file in os.listdir(dir): 413 for file in os.listdir(dir):
424 path = os.path.join(dir, file) 414 path = os.path.join(dir, file)
425 if os.path.isfile(path): 415 if os.path.isfile(path):
426 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'): 416 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'):
427 data = preprocessChromeLocale(path, metadata, False) 417 data = preprocessChromeLocale(path, metadata, False)
428 newName = file 418 newName = file
429 elif localeConfig['file_format'] == 'chrome-json': 419 elif localeConfig['file_format'] == 'chrome-json':
430 fileHandle = codecs.open(path, 'rb', encoding='utf-8') 420 fileHandle = codecs.open(path, 'rb', encoding='utf-8')
431 data = json.dumps({file: {'message': fileHandle.read()}}) 421 data = json.dumps({file: {'message': fileHandle.read()}})
432 fileHandle.close() 422 fileHandle.close()
433 newName = file + '.json' 423 newName = file + '.json'
434 else: 424 else:
435 data = toJSON(path) 425 data = toJSON(path)
436 newName = file + '.json' 426 newName = file + '.json'
437 427
438 if data: 428 if data:
439 files.append((newName, data)) 429 files.append((newName, data))
440 if len(files): 430 if len(files):
441 language = mapLocale(localeConfig['name_format'], locale) 431 language = mapLocale(localeConfig['name_format'], locale)
442 data, headers = crowdin_body_headers(files) 432 data, headers = crowdin_prepare_upload(files)
443 crowdin_request(projectName, 'upload-translation', key, 433 crowdin_request(projectName, 'upload-translation', key,
444 {'language': language}, post_data=data, 434 {'language': language}, post_data=data,
445 headers=headers) 435 headers=headers)
446 436
447 437
448 def getTranslations(localeConfig, projectName, key): 438 def getTranslations(localeConfig, projectName, key):
449 # let crowdin build the project 439 """Download all available translations from crowdin.
kzar 2017/09/28 09:48:30 Maybe this should be a docstring for the function
tlucas 2017/09/28 10:48:40 Done.
440
441 Trigger crowdin to build the available export, wait for crowdin to
442 finish the job and download the generated zip afterwards.
443 """
450 crowdin_request(projectName, 'export', key) 444 crowdin_request(projectName, 'export', key)
451 445
452 result = crowdin_request(projectName, 'download/all.zip', key, raw=True) 446 result = crowdin_request(projectName, 'download/all.zip', key, raw=True)
453 zip = ZipFile(StringIO(result)) 447 zip = ZipFile(StringIO(result))
454 dirs = {} 448 dirs = {}
455 449
456 normalizedDefaultLocale = localeConfig['default_locale'] 450 normalizedDefaultLocale = localeConfig['default_locale']
457 if localeConfig['name_format'] == 'ISO-15897': 451 if localeConfig['name_format'] == 'ISO-15897':
458 normalizedDefaultLocale = normalizedDefaultLocale.replace('_', '-') 452 normalizedDefaultLocale = normalizedDefaultLocale.replace('_', '-')
459 normalizedDefaultLocale = mapLocale(localeConfig['name_format'], 453 normalizedDefaultLocale = mapLocale(localeConfig['name_format'],
(...skipping 50 matching lines...) Expand 10 before | Expand all | Expand 10 after
510 504
511 # Remove any extra files 505 # Remove any extra files
512 for dir, files in dirs.iteritems(): 506 for dir, files in dirs.iteritems():
513 baseDir = os.path.join(localeConfig['base_path'], dir) 507 baseDir = os.path.join(localeConfig['base_path'], dir)
514 if not os.path.exists(baseDir): 508 if not os.path.exists(baseDir):
515 continue 509 continue
516 for file in os.listdir(baseDir): 510 for file in os.listdir(baseDir):
517 path = os.path.join(baseDir, file) 511 path = os.path.join(baseDir, file)
518 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:
519 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