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: Outsource post / get Created Sept. 26, 2017, 12:11 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_post(post_data, project_name, action, key, get={}, raises=False):
115 """Perform a call to crowdin and raise an Exception on failure.""" 102 """Perform a call to crowdin and raise an Exception on failure."""
116 103 request = urllib2.Request(
117 result = urllib2.urlopen(crowdin_url(project_name, action, key, get), 104 '{}/{}/{}?{}'.format(CROWDIN_AP_URL,
118 data=post_data).read() 105 urllib.quote(project_name),
119 106 urllib.quote(action),
120 if raises and result.find('<success') < 0: 107 urllib.urlencode(dict(get, key=key, json=1))),
121 raise Exception( 108 post_data,
122 'Server indicated that the operation was not successful\n' + result 109 headers,
123 ) 110 )
111
112 try:
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)
124 120
125 return result 121 return result
126
127
128 def crowdin_get(*args, **kwargs):
Vasily Kuznetsov 2017/09/26 13:11:36 This get via post with None as a first argument lo
tlucas 2017/09/26 14:40:29 Done.
129 return crowdin_post(None, *args, **kwargs)
130 122
131 123
132 class OrderedDict(dict): 124 class OrderedDict(dict):
133 def __init__(self): 125 def __init__(self):
134 self.__order = [] 126 self.__order = []
135 127
136 def __setitem__(self, key, value): 128 def __setitem__(self, key, value):
137 self.__order.append(key) 129 self.__order.append(key)
138 dict.__setitem__(self, key, value) 130 dict.__setitem__(self, key, value)
139 131
(...skipping 186 matching lines...) Expand 10 before | Expand all | Expand 10 after
326 for match in re.finditer(r'&amp;lang=([\w\-]+)"', firefoxLocales): 318 for match in re.finditer(r'&amp;lang=([\w\-]+)"', firefoxLocales):
327 locales.add(mapLocale('BCP-47', match.group(1))) 319 locales.add(mapLocale('BCP-47', match.group(1)))
328 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()
329 for match in re.finditer(r'<tr>.*?</tr>', langPacks, re.S): 321 for match in re.finditer(r'<tr>.*?</tr>', langPacks, re.S):
330 if match.group(0).find('Install Language Pack') >= 0: 322 if match.group(0).find('Install Language Pack') >= 0:
331 match2 = re.search(r'lang="([\w\-]+)"', match.group(0)) 323 match2 = re.search(r'lang="([\w\-]+)"', match.group(0))
332 if match2: 324 if match2:
333 locales.add(mapLocale('BCP-47', match2.group(1))) 325 locales.add(mapLocale('BCP-47', match2.group(1)))
334 326
335 allowed = set() 327 allowed = set()
336 allowedLocales = json.loads(crowdin_get(projectName, 'supported-languages', 328 allowedLocales = crowdin_request(projectName, 'supported-languages', key)
Vasily Kuznetsov 2017/09/26 13:11:36 For extra style points you could check if there's
tlucas 2017/09/26 14:40:29 Nice idea, done.
337 key, {'json': 1}))
338 329
339 for locale in allowedLocales: 330 for locale in allowedLocales:
340 allowed.add(locale['crowdin_code']) 331 allowed.add(locale['crowdin_code'])
341 if not allowed.issuperset(locales): 332 if not allowed.issuperset(locales):
342 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)
343 334
344 locales = list(locales & allowed) 335 locales = list(locales & allowed)
345 locales.sort() 336 locales.sort()
346 params = urllib.urlencode([('languages[]', locale) for locale in locales]) 337 params = urllib.urlencode([('languages[]', locale) for locale in locales])
347 338
348 crowdin_post(params, projectName, 'edit-project', key, raises=True) 339 crowdin_request(projectName, 'edit-project', key, post_data=params)
349 340
350 341
351 def postFiles(files, url): 342 def crowdin_prepare_upload(files):
343 """Create a post body and matching headers, which Crowdin can handle."""
352 boundary = '----------ThIs_Is_tHe_bouNdaRY_$' 344 boundary = '----------ThIs_Is_tHe_bouNdaRY_$'
353 body = '' 345 body = ''
354 for file, data in files: 346 for name, data in files:
355 body += '--%s\r\n' % boundary 347 mimetype = mimetypes.guess_type(name)[0]
356 body += 'Content-Disposition: form-data; name="files[%s]"; filename="%s" \r\n' % (file, file) 348 body += (
357 body += 'Content-Type: application/octet-stream\r\n' 349 '--{boundary}\r\n'
358 body += 'Content-Transfer-Encoding: binary\r\n' 350 'Content-Disposition: form-data; name="files[{name}]"; '
359 body += '\r\n' + data + '\r\n' 351 'filename="{name}"\r\n'
360 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)
361 357
362 body = body.encode('utf-8') 358 body = body.encode('utf-8')
363 request = urllib2.Request(url, StringIO(body)) 359 return (
364 request.add_header('Content-Type', 'multipart/form-data; boundary=%s' % boun dary) 360 StringIO(body),
365 request.add_header('Content-Length', len(body)) 361 {
366 result = urllib2.urlopen(request).read() 362 'Content-Type': ('multipart/form-data; boundary=' + boundary),
Vasily Kuznetsov 2017/09/26 13:11:36 This could be refactored using `crowdin_post` to b
tlucas 2017/09/26 14:40:29 I refactored this function to only prepare a body
367 if result.find('<success') < 0: 363 'Content-Length': len(body)
368 raise Exception('Server indicated that the operation was not successful\ n' + result) 364 },
365 )
369 366
370 367
371 def updateTranslationMaster(localeConfig, metadata, dir, projectName, key): 368 def updateTranslationMaster(localeConfig, metadata, dir, projectName, key):
372 result = json.loads(crowdin_get(projectName, 'info', key, {'json': 1})) 369 result = crowdin_request(projectName, 'info', key)
373 370
374 existing = set(map(lambda f: f['name'], result['files'])) 371 existing = set(map(lambda f: f['name'], result['files']))
375 add = [] 372 add = []
376 update = [] 373 update = []
377 for file in os.listdir(dir): 374 for file in os.listdir(dir):
378 path = os.path.join(dir, file) 375 path = os.path.join(dir, file)
379 if os.path.isfile(path): 376 if os.path.isfile(path):
380 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'): 377 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'):
381 data = preprocessChromeLocale(path, metadata, True) 378 data = preprocessChromeLocale(path, metadata, True)
382 newName = file 379 newName = file
383 elif localeConfig['file_format'] == 'chrome-json': 380 elif localeConfig['file_format'] == 'chrome-json':
384 fileHandle = codecs.open(path, 'rb', encoding='utf-8') 381 fileHandle = codecs.open(path, 'rb', encoding='utf-8')
385 data = json.dumps({file: {'message': fileHandle.read()}}) 382 data = json.dumps({file: {'message': fileHandle.read()}})
386 fileHandle.close() 383 fileHandle.close()
387 newName = file + '.json' 384 newName = file + '.json'
388 else: 385 else:
389 data = toJSON(path) 386 data = toJSON(path)
390 newName = file + '.json' 387 newName = file + '.json'
391 388
392 if data: 389 if data:
393 if newName in existing: 390 if newName in existing:
394 update.append((newName, data)) 391 update.append((newName, data))
395 existing.remove(newName) 392 existing.remove(newName)
396 else: 393 else:
397 add.append((newName, data)) 394 add.append((newName, data))
398 395
399 if len(add): 396 if len(add):
400 data = {'titles[{}]'.format(name): re.sub(r'\.json', '', name) 397 query = {'titles[{}]'.format(name): os.path.splitext(name)[0]
401 for name, data in add} 398 for name, _ in add}
402 data['type'] = 'chrome' 399 query['type'] = 'chrome'
403 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)
404 if len(update): 403 if len(update):
405 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)
406 for file in existing: 407 for file in existing:
407 result = urllib2.urlopen( 408 crowdin_request(projectName, 'delete-file', key, {'file': file})
Vasily Kuznetsov 2017/09/26 13:11:36 Isn't this `crowdin_get`?
tlucas 2017/09/26 14:40:29 It is, missed that :) Done.
408 crowdin_url(projectName, 'delete-file', key, {'file': file})
409 ).read()
410 if result.find('<success') < 0:
411 raise Exception('Server indicated that the operation was not success ful\n' + result)
412 409
413 410
414 def uploadTranslations(localeConfig, metadata, dir, locale, projectName, key): 411 def uploadTranslations(localeConfig, metadata, dir, locale, projectName, key):
415 files = [] 412 files = []
416 for file in os.listdir(dir): 413 for file in os.listdir(dir):
417 path = os.path.join(dir, file) 414 path = os.path.join(dir, file)
418 if os.path.isfile(path): 415 if os.path.isfile(path):
419 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'): 416 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'):
420 data = preprocessChromeLocale(path, metadata, False) 417 data = preprocessChromeLocale(path, metadata, False)
421 newName = file 418 newName = file
422 elif localeConfig['file_format'] == 'chrome-json': 419 elif localeConfig['file_format'] == 'chrome-json':
423 fileHandle = codecs.open(path, 'rb', encoding='utf-8') 420 fileHandle = codecs.open(path, 'rb', encoding='utf-8')
424 data = json.dumps({file: {'message': fileHandle.read()}}) 421 data = json.dumps({file: {'message': fileHandle.read()}})
425 fileHandle.close() 422 fileHandle.close()
426 newName = file + '.json' 423 newName = file + '.json'
427 else: 424 else:
428 data = toJSON(path) 425 data = toJSON(path)
429 newName = file + '.json' 426 newName = file + '.json'
430 427
431 if data: 428 if data:
432 files.append((newName, data)) 429 files.append((newName, data))
433 if len(files): 430 if len(files):
434 language = mapLocale(localeConfig['name_format'], locale) 431 language = mapLocale(localeConfig['name_format'], locale)
435 url = crowdin_url(projectName, 'upload-translation', key, 432 data, headers = crowdin_prepare_upload(files)
436 {'language': language}) 433 crowdin_request(projectName, 'upload-translation', key,
437 postFiles(files, url) 434 {'language': language}, post_data=data,
435 headers=headers)
438 436
439 437
440 def getTranslations(localeConfig, projectName, key): 438 def getTranslations(localeConfig, projectName, key):
441 # let crowdin build the project 439 """Download all available translations from crowdin.
442 crowdin_get(projectName, 'export', key, raises=True) 440
443 441 Trigger crowdin to build the available export, wait for crowdin to
444 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)
445 zip = ZipFile(StringIO(result)) 447 zip = ZipFile(StringIO(result))
446 dirs = {} 448 dirs = {}
447 449
448 normalizedDefaultLocale = localeConfig['default_locale'] 450 normalizedDefaultLocale = localeConfig['default_locale']
449 if localeConfig['name_format'] == 'ISO-15897': 451 if localeConfig['name_format'] == 'ISO-15897':
450 normalizedDefaultLocale = normalizedDefaultLocale.replace('_', '-') 452 normalizedDefaultLocale = normalizedDefaultLocale.replace('_', '-')
451 normalizedDefaultLocale = mapLocale(localeConfig['name_format'], 453 normalizedDefaultLocale = mapLocale(localeConfig['name_format'],
452 normalizedDefaultLocale) 454 normalizedDefaultLocale)
453 455
454 for info in zip.infolist(): 456 for info in zip.infolist():
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after
502 504
503 # Remove any extra files 505 # Remove any extra files
504 for dir, files in dirs.iteritems(): 506 for dir, files in dirs.iteritems():
505 baseDir = os.path.join(localeConfig['base_path'], dir) 507 baseDir = os.path.join(localeConfig['base_path'], dir)
506 if not os.path.exists(baseDir): 508 if not os.path.exists(baseDir):
507 continue 509 continue
508 for file in os.listdir(baseDir): 510 for file in os.listdir(baseDir):
509 path = os.path.join(baseDir, file) 511 path = os.path.join(baseDir, file)
510 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:
511 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