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

Side by Side Diff: localeTools.py

Issue 29556601: Issue 5777 - Update crowdin interface (Closed)
Patch Set: Always require a json-response Created Sept. 27, 2017, 10:49 a.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
View unified diff | Download patch
« no previous file with comments | « no previous file | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
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 urllib 11 import urllib
11 import urllib2 12 import urllib2
12 from StringIO import StringIO 13 from StringIO import StringIO
13 from ConfigParser import SafeConfigParser 14 from ConfigParser import SafeConfigParser
14 from zipfile import ZipFile 15 from zipfile import ZipFile
15 from xml.parsers.expat import ParserCreate, XML_PARAM_ENTITY_PARSING_ALWAYS 16 from xml.parsers.expat import ParserCreate, XML_PARAM_ENTITY_PARSING_ALWAYS
16 17
17 langMappingGecko = { 18 langMappingGecko = {
18 'bn-BD': 'bn', 19 'bn-BD': 'bn',
19 'br': 'br-FR', 20 'br': 'br-FR',
(...skipping 65 matching lines...) Expand 10 before | Expand all | Expand 10 after
85 'ta', 86 'ta',
86 'te', 87 'te',
87 'th', 88 'th',
88 'tr', 89 'tr',
89 'uk', 90 'uk',
90 'vi', 91 'vi',
91 'zh-CN', 92 'zh-CN',
92 'zh-TW', 93 'zh-TW',
93 ] 94 ]
94 95
96 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
114
115 def crowdin_request(project_name, action, key, get={}, post_data=None,
116 headers={}, raw=False):
117 """Perform a call to crowdin and raise an Exception on failure."""
118 request = urllib2.Request(
119 crowdin_url(project_name, action, key, get),
120 post_data,
121 headers,
122 )
123
124 try:
125 result = urllib2.urlopen(request).read()
126 except urllib2.HTTPError as e:
127 err = e.read()
128 raise Exception(
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
132 if not raw:
133 return json.loads(result)
134
135 return result
136
95 137
96 class OrderedDict(dict): 138 class OrderedDict(dict):
97 def __init__(self): 139 def __init__(self):
98 self.__order = [] 140 self.__order = []
99 141
100 def __setitem__(self, key, value): 142 def __setitem__(self, key, value):
101 self.__order.append(key) 143 self.__order.append(key)
102 dict.__setitem__(self, key, value) 144 dict.__setitem__(self, key, value)
103 145
104 def iteritems(self): 146 def iteritems(self):
(...skipping 185 matching lines...) Expand 10 before | Expand all | Expand 10 after
290 for match in re.finditer(r'&lang=([\w\-]+)"', firefoxLocales): 332 for match in re.finditer(r'&lang=([\w\-]+)"', firefoxLocales):
291 locales.add(mapLocale('BCP-47', match.group(1))) 333 locales.add(mapLocale('BCP-47', match.group(1)))
292 langPacks = urllib2.urlopen('https://addons.mozilla.org/en-US/firefox/la nguage-tools/').read() 334 langPacks = urllib2.urlopen('https://addons.mozilla.org/en-US/firefox/la nguage-tools/').read()
293 for match in re.finditer(r'<tr>.*?</tr>', langPacks, re.S): 335 for match in re.finditer(r'<tr>.*?</tr>', langPacks, re.S):
294 if match.group(0).find('Install Language Pack') >= 0: 336 if match.group(0).find('Install Language Pack') >= 0:
295 match2 = re.search(r'lang="([\w\-]+)"', match.group(0)) 337 match2 = re.search(r'lang="([\w\-]+)"', match.group(0))
296 if match2: 338 if match2:
297 locales.add(mapLocale('BCP-47', match2.group(1))) 339 locales.add(mapLocale('BCP-47', match2.group(1)))
298 340
299 allowed = set() 341 allowed = set()
300 allowedLocales = json.load(urllib2.urlopen( 342 allowedLocales = crowdin_request(projectName, 'supported-languages', key)
301 'https://crowdin.com/languages/languages_list?callback=' 343
302 ))
303 for locale in allowedLocales: 344 for locale in allowedLocales:
304 allowed.add(locale['code']) 345 allowed.add(locale['crowdin_code'])
305 if not allowed.issuperset(locales): 346 if not allowed.issuperset(locales):
306 print "Warning, following locales aren't allowed by server: " + ', '.joi n(locales - allowed) 347 print "Warning, following locales aren't allowed by server: " + ', '.joi n(locales - allowed)
307 348
308 locales = list(locales & allowed) 349 locales = list(locales & allowed)
309 locales.sort() 350 locales.sort()
310 params = urllib.urlencode([('languages[]', locale) for locale in locales]) 351 params = urllib.urlencode([('languages[]', locale) for locale in locales])
311 result = urllib2.urlopen('http://api.crowdin.net/api/project/%s/edit-project ?key=%s' % (projectName, key), params).read() 352
312 if result.find('<success') < 0: 353 crowdin_request(projectName, 'edit-project', key, post_data=params)
313 raise Exception('Server indicated that the operation was not successful\ n' + result)
314 354
315 355
316 def postFiles(files, url): 356 def crowdin_body_headers(files):
357 """Create a post body and according headers, which Crowdin can handle."""
317 boundary = '----------ThIs_Is_tHe_bouNdaRY_$' 358 boundary = '----------ThIs_Is_tHe_bouNdaRY_$'
318 body = '' 359 body = ''
319 for file, data in files: 360 for file, data in files:
320 body += '--%s\r\n' % boundary 361 body += '--%s\r\n' % boundary
321 body += 'Content-Disposition: form-data; name="files[%s]"; filename="%s" \r\n' % (file, file) 362 body += 'Content-Disposition: form-data; name="files[%s]"; filename="%s" \r\n' % (file, file)
322 body += 'Content-Type: application/octet-stream\r\n' 363 body += 'Content-Type: application/octet-stream\r\n'
323 body += 'Content-Transfer-Encoding: binary\r\n' 364 body += 'Content-Transfer-Encoding: binary\r\n'
324 body += '\r\n' + data + '\r\n' 365 body += '\r\n' + data + '\r\n'
325 body += '--%s--\r\n' % boundary 366 body += '--%s--\r\n' % boundary
326 367
327 body = body.encode('utf-8') 368 body = body.encode('utf-8')
328 request = urllib2.Request(url, StringIO(body)) 369 return (
329 request.add_header('Content-Type', 'multipart/form-data; boundary=%s' % boun dary) 370 StringIO(body),
330 request.add_header('Content-Length', len(body)) 371 {
331 result = urllib2.urlopen(request).read() 372 'Content-Type': 'multipart/form-data; ; charset=utf-8; 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
332 if result.find('<success') < 0: 373 'Content-Length': len(body)
333 raise Exception('Server indicated that the operation was not successful\ n' + result) 374 }
375 )
334 376
335 377
336 def updateTranslationMaster(localeConfig, metadata, dir, projectName, key): 378 def updateTranslationMaster(localeConfig, metadata, dir, projectName, key):
337 result = json.load(urllib2.urlopen('http://api.crowdin.net/api/project/%s/in fo?key=%s&json=1' % (projectName, key))) 379 result = crowdin_request(projectName, 'info', key)
338 380
339 existing = set(map(lambda f: f['name'], result['files'])) 381 existing = set(map(lambda f: f['name'], result['files']))
340 add = [] 382 add = []
341 update = [] 383 update = []
342 for file in os.listdir(dir): 384 for file in os.listdir(dir):
343 path = os.path.join(dir, file) 385 path = os.path.join(dir, file)
344 if os.path.isfile(path): 386 if os.path.isfile(path):
345 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'): 387 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'):
346 data = preprocessChromeLocale(path, metadata, True) 388 data = preprocessChromeLocale(path, metadata, True)
347 newName = file 389 newName = file
348 elif localeConfig['file_format'] == 'chrome-json': 390 elif localeConfig['file_format'] == 'chrome-json':
349 fileHandle = codecs.open(path, 'rb', encoding='utf-8') 391 fileHandle = codecs.open(path, 'rb', encoding='utf-8')
350 data = json.dumps({file: {'message': fileHandle.read()}}) 392 data = json.dumps({file: {'message': fileHandle.read()}})
351 fileHandle.close() 393 fileHandle.close()
352 newName = file + '.json' 394 newName = file + '.json'
353 else: 395 else:
354 data = toJSON(path) 396 data = toJSON(path)
355 newName = file + '.json' 397 newName = file + '.json'
356 398
357 if data: 399 if data:
358 if newName in existing: 400 if newName in existing:
359 update.append((newName, data)) 401 update.append((newName, data))
360 existing.remove(newName) 402 existing.remove(newName)
361 else: 403 else:
362 add.append((newName, data)) 404 add.append((newName, data))
363 405
364 if len(add): 406 if len(add):
365 titles = urllib.urlencode([('titles[%s]' % name, re.sub(r'\.json', '', n ame)) for name, data in add]) 407 data = {'titles[{}]'.format(name): re.sub(r'\.json', '', name)
366 postFiles(add, 'http://api.crowdin.net/api/project/%s/add-file?key=%s&ty pe=chrome&%s' % (projectName, key, titles)) 408 for name, data in add}
409 data['type'] = 'chrome'
410 data, headers = crowdin_body_headers(add)
411 crowdin_request(projectName, 'add-file', key, post_data=data,
412 headers=headers)
367 if len(update): 413 if len(update):
368 postFiles(update, 'http://api.crowdin.net/api/project/%s/update-file?key =%s' % (projectName, key)) 414 data, headers = crowdin_body_headers(update)
415 crowdin_request(projectName, 'update-file', key, post_data=data,
416 headers=headers)
369 for file in existing: 417 for file in existing:
370 result = urllib2.urlopen('http://api.crowdin.net/api/project/%s/delete-f ile?key=%s&file=%s' % (projectName, key, file)).read() 418 crowdin_request(projectName, 'delete-file', key, {'file': file})
371 if result.find('<success') < 0:
372 raise Exception('Server indicated that the operation was not success ful\n' + result)
373 419
374 420
375 def uploadTranslations(localeConfig, metadata, dir, locale, projectName, key): 421 def uploadTranslations(localeConfig, metadata, dir, locale, projectName, key):
376 files = [] 422 files = []
377 for file in os.listdir(dir): 423 for file in os.listdir(dir):
378 path = os.path.join(dir, file) 424 path = os.path.join(dir, file)
379 if os.path.isfile(path): 425 if os.path.isfile(path):
380 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'): 426 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'):
381 data = preprocessChromeLocale(path, metadata, False) 427 data = preprocessChromeLocale(path, metadata, False)
382 newName = file 428 newName = file
383 elif localeConfig['file_format'] == 'chrome-json': 429 elif localeConfig['file_format'] == 'chrome-json':
384 fileHandle = codecs.open(path, 'rb', encoding='utf-8') 430 fileHandle = codecs.open(path, 'rb', encoding='utf-8')
385 data = json.dumps({file: {'message': fileHandle.read()}}) 431 data = json.dumps({file: {'message': fileHandle.read()}})
386 fileHandle.close() 432 fileHandle.close()
387 newName = file + '.json' 433 newName = file + '.json'
388 else: 434 else:
389 data = toJSON(path) 435 data = toJSON(path)
390 newName = file + '.json' 436 newName = file + '.json'
391 437
392 if data: 438 if data:
393 files.append((newName, data)) 439 files.append((newName, data))
394 if len(files): 440 if len(files):
395 postFiles(files, 'http://api.crowdin.net/api/project/%s/upload-translati on?key=%s&language=%s' % ( 441 language = mapLocale(localeConfig['name_format'], locale)
396 projectName, key, mapLocale(localeConfig['name_format'], locale)) 442 data, headers = crowdin_body_headers(files)
397 ) 443 crowdin_request(projectName, 'upload-translation', key,
444 {'language': language}, post_data=data,
445 headers=headers)
398 446
399 447
400 def getTranslations(localeConfig, projectName, key): 448 def getTranslations(localeConfig, projectName, key):
401 result = urllib2.urlopen('http://api.crowdin.net/api/project/%s/export?key=% s' % (projectName, key)).read() 449 # let crowdin build the project
kzar 2017/09/28 09:48:30 Maybe this should be a docstring for the function
tlucas 2017/09/28 10:48:40 Done.
402 if result.find('<success') < 0: 450 crowdin_request(projectName, 'export', key)
403 raise Exception('Server indicated that the operation was not successful\ n' + result)
404 451
405 result = urllib2.urlopen('http://api.crowdin.net/api/project/%s/download/all .zip?key=%s' % (projectName, key)).read() 452 result = crowdin_request(projectName, 'download/all.zip', key, raw=True)
406 zip = ZipFile(StringIO(result)) 453 zip = ZipFile(StringIO(result))
407 dirs = {} 454 dirs = {}
408 455
409 normalizedDefaultLocale = localeConfig['default_locale'] 456 normalizedDefaultLocale = localeConfig['default_locale']
410 if localeConfig['name_format'] == 'ISO-15897': 457 if localeConfig['name_format'] == 'ISO-15897':
411 normalizedDefaultLocale = normalizedDefaultLocale.replace('_', '-') 458 normalizedDefaultLocale = normalizedDefaultLocale.replace('_', '-')
412 normalizedDefaultLocale = mapLocale(localeConfig['name_format'], 459 normalizedDefaultLocale = mapLocale(localeConfig['name_format'],
413 normalizedDefaultLocale) 460 normalizedDefaultLocale)
414 461
415 for info in zip.infolist(): 462 for info in zip.infolist():
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after
463 510
464 # Remove any extra files 511 # Remove any extra files
465 for dir, files in dirs.iteritems(): 512 for dir, files in dirs.iteritems():
466 baseDir = os.path.join(localeConfig['base_path'], dir) 513 baseDir = os.path.join(localeConfig['base_path'], dir)
467 if not os.path.exists(baseDir): 514 if not os.path.exists(baseDir):
468 continue 515 continue
469 for file in os.listdir(baseDir): 516 for file in os.listdir(baseDir):
470 path = os.path.join(baseDir, file) 517 path = os.path.join(baseDir, file)
471 if os.path.isfile(path) and (file.endswith('.json') or file.endswith ('.properties') or file.endswith('.dtd')) and not file in files: 518 if os.path.isfile(path) and (file.endswith('.json') or file.endswith ('.properties') or file.endswith('.dtd')) and not file in files:
472 os.remove(path) 519 os.remove(path)
OLDNEW
« no previous file with comments | « no previous file | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld