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: Added comment Created Sept. 26, 2017, 9:20 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
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 """Perform a call to crowdin and raise an Exception on failure."""
102 103 request = urllib2.Request(
103 get.update({'key': key}) 104 '{}/{}/{}?{}'.format(CROWDIN_AP_URL,
Vasily Kuznetsov 2017/09/26 11:05:52 Why not just `get['key'] = key`? Seems simpler.
tlucas 2017/09/26 12:13:13 Done.
104 105 urllib.quote(project_name),
105 scheme, netloc, path, params, query, fragment = urlparse.urlparse(url) 106 urllib.quote(action),
106 107 urllib.urlencode(dict(get, key=key, json=1))),
107 query = dict(urlparse.parse_qsl(query)) 108 post_data,
Vasily Kuznetsov 2017/09/26 11:05:52 Could there be anything in this query? It seems li
tlucas 2017/09/26 12:13:13 No, you are right - currently, there can't be quer
Vasily Kuznetsov 2017/09/26 13:11:36 Acknowledged.
108 query.update(get) 109 headers,
109 110 )
110 return urlparse.urlunparse(( 111
111 scheme, netloc, path, params, urllib.urlencode(query), fragment 112 try:
112 )) 113 result = urllib2.urlopen(request).read()
114 except urllib2.HTTPError as e:
115 raise Exception('Server returned HTTP Error {}:\n{}'.format(e.code,
116 e.read()))
117
118 if not raw:
119 return json.loads(result)
120
121 return result
113 122
114 123
115 class OrderedDict(dict): 124 class OrderedDict(dict):
116 def __init__(self): 125 def __init__(self):
117 self.__order = [] 126 self.__order = []
118 127
119 def __setitem__(self, key, value): 128 def __setitem__(self, key, value):
120 self.__order.append(key) 129 self.__order.append(key)
121 dict.__setitem__(self, key, value) 130 dict.__setitem__(self, key, value)
122 131
(...skipping 186 matching lines...) Expand 10 before | Expand all | Expand 10 after
309 for match in re.finditer(r'&lang=([\w\-]+)"', firefoxLocales): 318 for match in re.finditer(r'&lang=([\w\-]+)"', firefoxLocales):
310 locales.add(mapLocale('BCP-47', match.group(1))) 319 locales.add(mapLocale('BCP-47', match.group(1)))
311 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()
312 for match in re.finditer(r'<tr>.*?</tr>', langPacks, re.S): 321 for match in re.finditer(r'<tr>.*?</tr>', langPacks, re.S):
313 if match.group(0).find('Install Language Pack') >= 0: 322 if match.group(0).find('Install Language Pack') >= 0:
314 match2 = re.search(r'lang="([\w\-]+)"', match.group(0)) 323 match2 = re.search(r'lang="([\w\-]+)"', match.group(0))
315 if match2: 324 if match2:
316 locales.add(mapLocale('BCP-47', match2.group(1))) 325 locales.add(mapLocale('BCP-47', match2.group(1)))
317 326
318 allowed = set() 327 allowed = set()
319 allowedLocales = json.load(urllib2.urlopen( 328 allowedLocales = crowdin_request(projectName, 'supported-languages', key)
320 crowdin_url(projectName, 'supported-languages', key, {'json': 1})))
321 329
322 for locale in allowedLocales: 330 for locale in allowedLocales:
323 allowed.add(locale['crowdin_code']) 331 allowed.add(locale['crowdin_code'])
324 if not allowed.issuperset(locales): 332 if not allowed.issuperset(locales):
325 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)
326 334
327 locales = list(locales & allowed) 335 locales = list(locales & allowed)
328 locales.sort() 336 locales.sort()
329 params = urllib.urlencode([('languages[]', locale) for locale in locales]) 337 params = urllib.urlencode([('languages[]', locale) for locale in locales])
330 338
331 result = urllib2.urlopen( 339 crowdin_request(projectName, 'edit-project', key, post_data=params)
332 crowdin_url(projectName, 'edit-project', key), data=params 340
333 ).read() 341
334 342 def crowdin_prepare_upload(files):
335 if result.find('<success') < 0: 343 """Create a post body and matching headers, which Crowdin can handle."""
336 raise Exception('Server indicated that the operation was not successful\ n' + result)
337
338
339 def postFiles(files, url):
340 boundary = '----------ThIs_Is_tHe_bouNdaRY_$' 344 boundary = '----------ThIs_Is_tHe_bouNdaRY_$'
341 body = '' 345 body = ''
342 for file, data in files: 346 for name, data in files:
343 body += '--%s\r\n' % boundary 347 mimetype = mimetypes.guess_type(name)[0]
344 body += 'Content-Disposition: form-data; name="files[%s]"; filename="%s" \r\n' % (file, file) 348 body += (
345 body += 'Content-Type: application/octet-stream\r\n' 349 '--{boundary}\r\n'
346 body += 'Content-Transfer-Encoding: binary\r\n' 350 'Content-Disposition: form-data; name="files[{name}]"; '
347 body += '\r\n' + data + '\r\n' 351 'filename="{name}"\r\n'
348 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)
349 357
350 body = body.encode('utf-8') 358 body = body.encode('utf-8')
351 request = urllib2.Request(url, StringIO(body)) 359 return (
352 request.add_header('Content-Type', 'multipart/form-data; boundary=%s' % boun dary) 360 StringIO(body),
353 request.add_header('Content-Length', len(body)) 361 {
354 result = urllib2.urlopen(request).read() 362 'Content-Type': ('multipart/form-data; boundary=' + boundary),
355 if result.find('<success') < 0: 363 'Content-Length': len(body)
356 raise Exception('Server indicated that the operation was not successful\ n' + result) 364 },
365 )
357 366
358 367
359 def updateTranslationMaster(localeConfig, metadata, dir, projectName, key): 368 def updateTranslationMaster(localeConfig, metadata, dir, projectName, key):
360 result = json.load(urllib2.urlopen( 369 result = crowdin_request(projectName, 'info', key)
361 crowdin_url(projectName, 'info', key, {'json': 1})))
362 370
363 existing = set(map(lambda f: f['name'], result['files'])) 371 existing = set(map(lambda f: f['name'], result['files']))
364 add = [] 372 add = []
365 update = [] 373 update = []
366 for file in os.listdir(dir): 374 for file in os.listdir(dir):
367 path = os.path.join(dir, file) 375 path = os.path.join(dir, file)
368 if os.path.isfile(path): 376 if os.path.isfile(path):
369 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'): 377 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'):
370 data = preprocessChromeLocale(path, metadata, True) 378 data = preprocessChromeLocale(path, metadata, True)
371 newName = file 379 newName = file
372 elif localeConfig['file_format'] == 'chrome-json': 380 elif localeConfig['file_format'] == 'chrome-json':
373 fileHandle = codecs.open(path, 'rb', encoding='utf-8') 381 fileHandle = codecs.open(path, 'rb', encoding='utf-8')
374 data = json.dumps({file: {'message': fileHandle.read()}}) 382 data = json.dumps({file: {'message': fileHandle.read()}})
375 fileHandle.close() 383 fileHandle.close()
376 newName = file + '.json' 384 newName = file + '.json'
377 else: 385 else:
378 data = toJSON(path) 386 data = toJSON(path)
379 newName = file + '.json' 387 newName = file + '.json'
380 388
381 if data: 389 if data:
382 if newName in existing: 390 if newName in existing:
383 update.append((newName, data)) 391 update.append((newName, data))
384 existing.remove(newName) 392 existing.remove(newName)
385 else: 393 else:
386 add.append((newName, data)) 394 add.append((newName, data))
387 395
388 if len(add): 396 if len(add):
389 data = {'titles[{}]'.format(name): re.sub(r'\.json', '', name) 397 query = {'titles[{}]'.format(name): os.path.splitext(name)[0]
390 for name, data in add} 398 for name, _ in add}
391 data.update({'type': 'chrome'}) 399 query['type'] = 'chrome'
Vasily Kuznetsov 2017/09/26 11:05:52 Also could be just `data['type'] = 'chrome'`.
tlucas 2017/09/26 12:13:13 Done.
392 postFiles(add, crowdin_url(projectName, 'add-file', key, data)) 400 data, headers = crowdin_prepare_upload(add)
401 crowdin_request(projectName, 'add-file', key, query, post_data=data,
402 headers=headers)
393 if len(update): 403 if len(update):
394 postFiles(update, crowdin_url(projectName, 'update-file', key)) 404 data, headers = crowdin_prepare_upload(update)
405 crowdin_request(projectName, 'update-file', key, post_data=data,
406 headers=headers)
395 for file in existing: 407 for file in existing:
396 result = urllib2.urlopen( 408 crowdin_request(projectName, 'delete-file', key, {'file': file})
397 crowdin_url(projectName, 'delete-file', key, {'file': file})
398 ).read()
399 if result.find('<success') < 0:
400 raise Exception('Server indicated that the operation was not success ful\n' + result)
401 409
402 410
403 def uploadTranslations(localeConfig, metadata, dir, locale, projectName, key): 411 def uploadTranslations(localeConfig, metadata, dir, locale, projectName, key):
404 files = [] 412 files = []
405 for file in os.listdir(dir): 413 for file in os.listdir(dir):
406 path = os.path.join(dir, file) 414 path = os.path.join(dir, file)
407 if os.path.isfile(path): 415 if os.path.isfile(path):
408 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'): 416 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'):
409 data = preprocessChromeLocale(path, metadata, False) 417 data = preprocessChromeLocale(path, metadata, False)
410 newName = file 418 newName = file
411 elif localeConfig['file_format'] == 'chrome-json': 419 elif localeConfig['file_format'] == 'chrome-json':
412 fileHandle = codecs.open(path, 'rb', encoding='utf-8') 420 fileHandle = codecs.open(path, 'rb', encoding='utf-8')
413 data = json.dumps({file: {'message': fileHandle.read()}}) 421 data = json.dumps({file: {'message': fileHandle.read()}})
414 fileHandle.close() 422 fileHandle.close()
415 newName = file + '.json' 423 newName = file + '.json'
416 else: 424 else:
417 data = toJSON(path) 425 data = toJSON(path)
418 newName = file + '.json' 426 newName = file + '.json'
419 427
420 if data: 428 if data:
421 files.append((newName, data)) 429 files.append((newName, data))
422 if len(files): 430 if len(files):
423 language = mapLocale(localeConfig['name_format'], locale) 431 language = mapLocale(localeConfig['name_format'], locale)
424 postFiles(files, 432 data, headers = crowdin_prepare_upload(files)
Vasily Kuznetsov 2017/09/26 11:05:52 It would probably be more readable if this was spl
tlucas 2017/09/26 12:13:13 Done.
425 crowdin_url(projectName, 'upload-translation', key, 433 crowdin_request(projectName, 'upload-translation', key,
426 {'language': language})) 434 {'language': language}, post_data=data,
435 headers=headers)
427 436
428 437
429 def getTranslations(localeConfig, projectName, key): 438 def getTranslations(localeConfig, projectName, key):
430 result = urllib2.urlopen(crowdin_url(projectName, 'export', key)).read() 439 """Download all available translations from crowdin.
Vasily Kuznetsov 2017/09/26 11:05:52 This pattern of `urllib2.urlopen(crowdin_url(....)
tlucas 2017/09/26 12:13:13 Yes - most of the time, the result was used to det
431 if result.find('<success') < 0: 440
432 raise Exception('Server indicated that the operation was not successful\ n' + result) 441 Trigger crowdin to build the available export, wait for crowdin to
433 442 finish the job and download the generated zip afterwards.
434 result = urllib2.urlopen( 443 """
435 crowdin_url(projectName, 'download/all.zip', key)).read() 444 crowdin_request(projectName, 'export', key)
445
446 result = crowdin_request(projectName, 'download/all.zip', key, raw=True)
436 zip = ZipFile(StringIO(result)) 447 zip = ZipFile(StringIO(result))
437 dirs = {} 448 dirs = {}
438 449
439 normalizedDefaultLocale = localeConfig['default_locale'] 450 normalizedDefaultLocale = localeConfig['default_locale']
440 if localeConfig['name_format'] == 'ISO-15897': 451 if localeConfig['name_format'] == 'ISO-15897':
441 normalizedDefaultLocale = normalizedDefaultLocale.replace('_', '-') 452 normalizedDefaultLocale = normalizedDefaultLocale.replace('_', '-')
442 normalizedDefaultLocale = mapLocale(localeConfig['name_format'], 453 normalizedDefaultLocale = mapLocale(localeConfig['name_format'],
443 normalizedDefaultLocale) 454 normalizedDefaultLocale)
444 455
445 for info in zip.infolist(): 456 for info in zip.infolist():
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after
493 504
494 # Remove any extra files 505 # Remove any extra files
495 for dir, files in dirs.iteritems(): 506 for dir, files in dirs.iteritems():
496 baseDir = os.path.join(localeConfig['base_path'], dir) 507 baseDir = os.path.join(localeConfig['base_path'], dir)
497 if not os.path.exists(baseDir): 508 if not os.path.exists(baseDir):
498 continue 509 continue
499 for file in os.listdir(baseDir): 510 for file in os.listdir(baseDir):
500 path = os.path.join(baseDir, file) 511 path = os.path.join(baseDir, file)
501 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:
502 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