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: Purged obsolete distinguishing between crowdin_{get|post} Created Sept. 26, 2017, 2:46 p.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={}):
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
113
114 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):
116 """Perform a call to crowdin and raise an Exception on failure."""
117
118 request = urllib2.Request(
119 crowdin_url(project_name, action, key, get),
120 post_data,
121 headers,
122 )
123
124 result = urllib2.urlopen(request).read()
125
126 if raises and result.find('<success') < 0:
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(
128 'Server indicated that the operation was not successful\n' + result
129 )
130
131 if 'json' in get:
132 result = json.loads(result)
133
134 return result
135
95 136
96 class OrderedDict(dict): 137 class OrderedDict(dict):
97 def __init__(self): 138 def __init__(self):
98 self.__order = [] 139 self.__order = []
99 140
100 def __setitem__(self, key, value): 141 def __setitem__(self, key, value):
101 self.__order.append(key) 142 self.__order.append(key)
102 dict.__setitem__(self, key, value) 143 dict.__setitem__(self, key, value)
103 144
104 def iteritems(self): 145 def iteritems(self):
(...skipping 185 matching lines...) Expand 10 before | Expand all | Expand 10 after
290 for match in re.finditer(r'&amp;lang=([\w\-]+)"', firefoxLocales): 331 for match in re.finditer(r'&amp;lang=([\w\-]+)"', firefoxLocales):
291 locales.add(mapLocale('BCP-47', match.group(1))) 332 locales.add(mapLocale('BCP-47', match.group(1)))
292 langPacks = urllib2.urlopen('https://addons.mozilla.org/en-US/firefox/la nguage-tools/').read() 333 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): 334 for match in re.finditer(r'<tr>.*?</tr>', langPacks, re.S):
294 if match.group(0).find('Install Language Pack') >= 0: 335 if match.group(0).find('Install Language Pack') >= 0:
295 match2 = re.search(r'lang="([\w\-]+)"', match.group(0)) 336 match2 = re.search(r'lang="([\w\-]+)"', match.group(0))
296 if match2: 337 if match2:
297 locales.add(mapLocale('BCP-47', match2.group(1))) 338 locales.add(mapLocale('BCP-47', match2.group(1)))
298 339
299 allowed = set() 340 allowed = set()
300 allowedLocales = json.load(urllib2.urlopen( 341 allowedLocales = crowdin_request(projectName, 'supported-languages', key,
301 'https://crowdin.com/languages/languages_list?callback=' 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
302 )) 343
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) 354 raises=True)
314 355
315 356
316 def postFiles(files, url): 357 def crowdin_body_headers(files):
358 """Create a post body and according headers, which Crowdin can handle."""
317 boundary = '----------ThIs_Is_tHe_bouNdaRY_$' 359 boundary = '----------ThIs_Is_tHe_bouNdaRY_$'
318 body = '' 360 body = ''
319 for file, data in files: 361 for file, data in files:
320 body += '--%s\r\n' % boundary 362 body += '--%s\r\n' % boundary
321 body += 'Content-Disposition: form-data; name="files[%s]"; filename="%s" \r\n' % (file, file) 363 body += 'Content-Disposition: form-data; name="files[%s]"; filename="%s" \r\n' % (file, file)
322 body += 'Content-Type: application/octet-stream\r\n' 364 body += 'Content-Type: application/octet-stream\r\n'
323 body += 'Content-Transfer-Encoding: binary\r\n' 365 body += 'Content-Transfer-Encoding: binary\r\n'
324 body += '\r\n' + data + '\r\n' 366 body += '\r\n' + data + '\r\n'
325 body += '--%s--\r\n' % boundary 367 body += '--%s--\r\n' % boundary
326 368
327 body = body.encode('utf-8') 369 body = body.encode('utf-8')
Sebastian Noack 2017/09/27 04:04:52 This seems unrelated, but I noticed that the encod
tlucas 2017/09/27 10:51:30 Done.
328 request = urllib2.Request(url, StringIO(body)) 370 return (
329 request.add_header('Content-Type', 'multipart/form-data; boundary=%s' % boun dary) 371 StringIO(body),
330 request.add_header('Content-Length', len(body)) 372 {
331 result = urllib2.urlopen(request).read() 373 'Content-Type': 'multipart/form-data; boundary=%s' % boundary,
332 if result.find('<success') < 0: 374 'Content-Length': len(body)
333 raise Exception('Server indicated that the operation was not successful\ n' + result) 375 }
376 )
334 377
335 378
336 def updateTranslationMaster(localeConfig, metadata, dir, projectName, key): 379 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))) 380 result = crowdin_request(projectName, 'info', key, {'json': 1})
338 381
339 existing = set(map(lambda f: f['name'], result['files'])) 382 existing = set(map(lambda f: f['name'], result['files']))
340 add = [] 383 add = []
341 update = [] 384 update = []
342 for file in os.listdir(dir): 385 for file in os.listdir(dir):
343 path = os.path.join(dir, file) 386 path = os.path.join(dir, file)
344 if os.path.isfile(path): 387 if os.path.isfile(path):
345 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'): 388 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'):
346 data = preprocessChromeLocale(path, metadata, True) 389 data = preprocessChromeLocale(path, metadata, True)
347 newName = file 390 newName = file
348 elif localeConfig['file_format'] == 'chrome-json': 391 elif localeConfig['file_format'] == 'chrome-json':
349 fileHandle = codecs.open(path, 'rb', encoding='utf-8') 392 fileHandle = codecs.open(path, 'rb', encoding='utf-8')
350 data = json.dumps({file: {'message': fileHandle.read()}}) 393 data = json.dumps({file: {'message': fileHandle.read()}})
351 fileHandle.close() 394 fileHandle.close()
352 newName = file + '.json' 395 newName = file + '.json'
353 else: 396 else:
354 data = toJSON(path) 397 data = toJSON(path)
355 newName = file + '.json' 398 newName = file + '.json'
356 399
357 if data: 400 if data:
358 if newName in existing: 401 if newName in existing:
359 update.append((newName, data)) 402 update.append((newName, data))
360 existing.remove(newName) 403 existing.remove(newName)
361 else: 404 else:
362 add.append((newName, data)) 405 add.append((newName, data))
363 406
364 if len(add): 407 if len(add):
365 titles = urllib.urlencode([('titles[%s]' % name, re.sub(r'\.json', '', n ame)) for name, data in add]) 408 data = {'titles[{}]'.format(name): re.sub(r'\.json', '', name)
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
366 postFiles(add, 'http://api.crowdin.net/api/project/%s/add-file?key=%s&ty pe=chrome&%s' % (projectName, key, titles)) 409 for name, data in add}
410 data['type'] = 'chrome'
411 data, headers = crowdin_body_headers(add)
412 crowdin_request(projectName, 'add-file', key, post_data=data,
413 headers=headers, raises=True)
367 if len(update): 414 if len(update):
368 postFiles(update, 'http://api.crowdin.net/api/project/%s/update-file?key =%s' % (projectName, key)) 415 data, headers = crowdin_body_headers(update)
416 crowdin_request(projectName, 'update-file', key, post_data=data,
417 headers=headers, raises=True)
369 for file in existing: 418 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() 419 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 420
374 421
375 def uploadTranslations(localeConfig, metadata, dir, locale, projectName, key): 422 def uploadTranslations(localeConfig, metadata, dir, locale, projectName, key):
376 files = [] 423 files = []
377 for file in os.listdir(dir): 424 for file in os.listdir(dir):
378 path = os.path.join(dir, file) 425 path = os.path.join(dir, file)
379 if os.path.isfile(path): 426 if os.path.isfile(path):
380 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'): 427 if localeConfig['file_format'] == 'chrome-json' and file.endswith('. json'):
381 data = preprocessChromeLocale(path, metadata, False) 428 data = preprocessChromeLocale(path, metadata, False)
382 newName = file 429 newName = file
383 elif localeConfig['file_format'] == 'chrome-json': 430 elif localeConfig['file_format'] == 'chrome-json':
384 fileHandle = codecs.open(path, 'rb', encoding='utf-8') 431 fileHandle = codecs.open(path, 'rb', encoding='utf-8')
385 data = json.dumps({file: {'message': fileHandle.read()}}) 432 data = json.dumps({file: {'message': fileHandle.read()}})
386 fileHandle.close() 433 fileHandle.close()
387 newName = file + '.json' 434 newName = file + '.json'
388 else: 435 else:
389 data = toJSON(path) 436 data = toJSON(path)
390 newName = file + '.json' 437 newName = file + '.json'
391 438
392 if data: 439 if data:
393 files.append((newName, data)) 440 files.append((newName, data))
394 if len(files): 441 if len(files):
395 postFiles(files, 'http://api.crowdin.net/api/project/%s/upload-translati on?key=%s&language=%s' % ( 442 language = mapLocale(localeConfig['name_format'], locale)
396 projectName, key, mapLocale(localeConfig['name_format'], locale)) 443 data, headers = crowdin_body_headers(files)
397 ) 444 crowdin_request(projectName, 'upload-translation', key,
445 {'language': language}, post_data=data,
446 headers=headers, raises=True)
398 447
399 448
400 def getTranslations(localeConfig, projectName, key): 449 def getTranslations(localeConfig, projectName, key):
401 result = urllib2.urlopen('http://api.crowdin.net/api/project/%s/export?key=% s' % (projectName, key)).read() 450 # let crowdin build the project
402 if result.find('<success') < 0: 451 crowdin_request(projectName, 'export', key, raises=True)
403 raise Exception('Server indicated that the operation was not successful\ n' + result)
404 452
405 result = urllib2.urlopen('http://api.crowdin.net/api/project/%s/download/all .zip?key=%s' % (projectName, key)).read() 453 result = crowdin_request(projectName, 'download/all.zip', key)
tlucas 2017/09/27 10:51:31 This will be the only call expecting the response
406 zip = ZipFile(StringIO(result)) 454 zip = ZipFile(StringIO(result))
407 dirs = {} 455 dirs = {}
408 456
409 normalizedDefaultLocale = localeConfig['default_locale'] 457 normalizedDefaultLocale = localeConfig['default_locale']
410 if localeConfig['name_format'] == 'ISO-15897': 458 if localeConfig['name_format'] == 'ISO-15897':
411 normalizedDefaultLocale = normalizedDefaultLocale.replace('_', '-') 459 normalizedDefaultLocale = normalizedDefaultLocale.replace('_', '-')
412 normalizedDefaultLocale = mapLocale(localeConfig['name_format'], 460 normalizedDefaultLocale = mapLocale(localeConfig['name_format'],
413 normalizedDefaultLocale) 461 normalizedDefaultLocale)
414 462
415 for info in zip.infolist(): 463 for info in zip.infolist():
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after
463 511
464 # Remove any extra files 512 # Remove any extra files
465 for dir, files in dirs.iteritems(): 513 for dir, files in dirs.iteritems():
466 baseDir = os.path.join(localeConfig['base_path'], dir) 514 baseDir = os.path.join(localeConfig['base_path'], dir)
467 if not os.path.exists(baseDir): 515 if not os.path.exists(baseDir):
468 continue 516 continue
469 for file in os.listdir(baseDir): 517 for file in os.listdir(baseDir):
470 path = os.path.join(baseDir, file) 518 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: 519 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) 520 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