| Left: | ||
| Right: |
| OLD | NEW |
|---|---|
| 1 # coding: utf-8 | 1 # coding: utf-8 |
| 2 | 2 |
| 3 # This Source Code is subject to the terms of the Mozilla Public License | 3 # This Source Code is subject to the terms of the Mozilla Public License |
| 4 # version 2.0 (the "License"). You can obtain a copy of the License at | 4 # version 2.0 (the "License"). You can obtain a copy of the License at |
| 5 # http://mozilla.org/MPL/2.0/. | 5 # http://mozilla.org/MPL/2.0/. |
| 6 | 6 |
| 7 import re, os, sys, codecs, json, urllib, urllib2 | 7 import re, os, sys, codecs, json, urllib, urllib2 |
| 8 from StringIO import StringIO | 8 from StringIO import StringIO |
| 9 from ConfigParser import SafeConfigParser | 9 from ConfigParser import SafeConfigParser |
| 10 from zipfile import ZipFile | 10 from zipfile import ZipFile |
| 11 from xml.parsers.expat import ParserCreate, XML_PARAM_ENTITY_PARSING_ALWAYS | 11 from xml.parsers.expat import ParserCreate, XML_PARAM_ENTITY_PARSING_ALWAYS |
| 12 | 12 |
| 13 langMapping = { | 13 langMappingGecko = { |
| 14 'dsb': 'dsb-DE', | 14 'dsb': 'dsb-DE', |
| 15 'hsb': 'hsb-DE', | 15 'hsb': 'hsb-DE', |
| 16 } | 16 } |
| 17 | 17 |
| 18 langMappingChrome = { | |
| 19 'es-419': 'es-AR', | |
| 20 'es': 'es-ES', | |
| 21 'sv': 'sv-SE', | |
| 22 'ml': 'ml-IN', | |
| 23 'nb': 'no', | |
| 24 } | |
| 25 | |
| 26 chromeLocales = [ | |
| 27 "am", | |
| 28 "ar", | |
| 29 "bg", | |
| 30 "bn", | |
| 31 "ca", | |
| 32 "cs", | |
| 33 "da", | |
| 34 "de", | |
| 35 "el", | |
| 36 "en-GB", | |
| 37 "en-US", | |
| 38 "es-419", | |
| 39 "es", | |
| 40 "et", | |
| 41 "fa", | |
| 42 "fi", | |
| 43 "fil", | |
| 44 "fr", | |
| 45 "gu", | |
| 46 "he", | |
| 47 "hi", | |
| 48 "hr", | |
| 49 "hu", | |
| 50 "id", | |
| 51 "it", | |
| 52 "ja", | |
| 53 "kn", | |
| 54 "ko", | |
| 55 "lt", | |
| 56 "lv", | |
| 57 "ml", | |
| 58 "mr", | |
| 59 "ms", | |
| 60 "nb", | |
| 61 "nl", | |
| 62 "pl", | |
| 63 "pt-BR", | |
| 64 "pt-PT", | |
| 65 "ro", | |
| 66 "ru", | |
| 67 "sk", | |
| 68 "sl", | |
| 69 "sr", | |
| 70 "sv", | |
| 71 "sw", | |
| 72 "ta", | |
| 73 "te", | |
| 74 "th", | |
| 75 "tr", | |
| 76 "uk", | |
| 77 "vi", | |
| 78 "zh-CN", | |
| 79 "zh-TW", | |
| 80 ] | |
| 81 | |
| 18 class OrderedDict(dict): | 82 class OrderedDict(dict): |
| 19 def __init__(self): | 83 def __init__(self): |
| 20 self.__order = [] | 84 self.__order = [] |
| 21 def __setitem__(self, key, value): | 85 def __setitem__(self, key, value): |
| 22 self.__order.append(key) | 86 self.__order.append(key) |
| 23 dict.__setitem__(self, key, value) | 87 dict.__setitem__(self, key, value) |
| 24 def iteritems(self): | 88 def iteritems(self): |
| 25 done = set() | 89 done = set() |
| 26 for key in self.__order: | 90 for key in self.__order: |
| 27 if not key in done and key in self: | 91 if not key in done and key in self: |
| 28 yield (key, self[key]) | 92 yield (key, self[key]) |
| 29 done.add(key) | 93 done.add(key) |
| 30 | 94 |
| 31 def escapeEntity(value): | 95 def escapeEntity(value): |
| 32 return value.replace('&', '&').replace('<', '<').replace('>', '>').r eplace('"', '"') | 96 return value.replace('&', '&').replace('<', '<').replace('>', '>').r eplace('"', '"') |
| 33 | 97 |
| 34 def unescapeEntity(value): | 98 def unescapeEntity(value): |
| 35 return value.replace('&', '&').replace('<', '<').replace('>', '>').r eplace('"', '"') | 99 return value.replace('&', '&').replace('<', '<').replace('>', '>').r eplace('"', '"') |
| 36 | 100 |
| 101 def mapLocale(type, locale): | |
| 102 mapping = langMappingChrome if type == 'chrome' else langMappingGecko | |
| 103 return mapping.get(locale, locale) | |
| 104 | |
| 37 def parseDTDString(data, path): | 105 def parseDTDString(data, path): |
| 38 result = [] | 106 result = [] |
| 39 currentComment = [None] | 107 currentComment = [None] |
| 40 | 108 |
| 41 parser = ParserCreate() | 109 parser = ParserCreate() |
| 42 parser.UseForeignDTD(True) | 110 parser.UseForeignDTD(True) |
| 43 parser.SetParamEntityParsing(XML_PARAM_ENTITY_PARSING_ALWAYS) | 111 parser.SetParamEntityParsing(XML_PARAM_ENTITY_PARSING_ALWAYS) |
| 44 | 112 |
| 45 def ExternalEntityRefHandler(context, base, systemId, publicId): | 113 def ExternalEntityRefHandler(context, base, systemId, publicId): |
| 46 subparser = parser.ExternalEntityParserCreate(context, 'utf-8') | 114 subparser = parser.ExternalEntityParserCreate(context, 'utf-8') |
| (...skipping 91 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
| 138 return None | 206 return None |
| 139 | 207 |
| 140 result = OrderedDict() | 208 result = OrderedDict() |
| 141 for name, comment, value in it: | 209 for name, comment, value in it: |
| 142 obj = {'message': value} | 210 obj = {'message': value} |
| 143 if comment == None: | 211 if comment == None: |
| 144 obj['description'] = name | 212 obj['description'] = name |
| 145 else: | 213 else: |
| 146 obj['description'] = '%s: %s' % (name, comment) | 214 obj['description'] = '%s: %s' % (name, comment) |
| 147 result[name] = obj | 215 result[name] = obj |
| 148 return json.dumps(result, indent=2) | 216 return json.dumps(result, ensure_ascii=False, indent=2) |
| 149 | 217 |
| 150 def fromJSON(path, data): | 218 def fromJSON(path, data): |
| 151 data = json.loads(data) | 219 data = json.loads(data) |
| 152 if not data: | 220 if not data: |
| 153 if os.path.exists(path): | 221 if os.path.exists(path): |
| 154 os.remove(path) | 222 os.remove(path) |
| 155 return | 223 return |
| 156 | 224 |
| 157 dir = os.path.dirname(path) | 225 dir = os.path.dirname(path) |
| 158 if not os.path.exists(dir): | 226 if not os.path.exists(dir): |
| 159 os.makedirs(dir) | 227 os.makedirs(dir) |
| 160 file = codecs.open(path, 'wb', encoding='utf-8') | 228 file = codecs.open(path, 'wb', encoding='utf-8') |
| 161 for key, value in data.iteritems(): | 229 for key, value in data.iteritems(): |
| 162 file.write(generateStringEntry(key, value['message'], path)) | 230 file.write(generateStringEntry(key, value['message'], path)) |
| 163 file.close() | 231 file.close() |
| 164 | 232 |
| 165 def setupTranslations(locales, projectName, key): | 233 def preprocessChromeLocale(path, metadata, isMaster): |
| 234 fileHandle = codecs.open(path, 'rb', encoding='utf-8') | |
| 235 data = json.load(fileHandle) | |
| 236 fileHandle.close() | |
| 237 | |
| 238 # Remove synced keys, these don't need to be translated | |
| 239 if metadata.has_section('locale_sync'): | |
| 240 for file, stringIDs in metadata.items('locale_sync'): | |
| 241 for stringID in re.split(r'\s+', stringIDs): | |
| 242 if file == 'remove': | |
| 243 key = stringID | |
| 244 else: | |
| 245 key = re.sub(r'\..*', '', file) + '_' + re.sub(r'\W', '_', stringID) | |
|
Felix Dahlke
2013/01/10 16:36:20
We have pretty much the same line in localeSyncChr
Wladimir Palant
2013/01/16 14:20:15
Yes, the idea is to make locale syncing part of th
| |
| 246 if key in data: | |
| 247 del data[key] | |
| 248 | |
| 249 for key, value in data.iteritems(): | |
| 250 if isMaster: | |
| 251 # Make sure the key name is listed in the description | |
| 252 if "description" in value: | |
| 253 value["description"] = "%s: %s" % (key, value["description"]) | |
| 254 else: | |
| 255 value["description"] = key | |
| 256 else: | |
| 257 # Delete description from translations | |
| 258 if "description" in value: | |
| 259 del value["description"] | |
| 260 | |
| 261 return json.dumps(data, ensure_ascii=False, sort_keys=True, indent=2) | |
| 262 | |
| 263 def postprocessChromeLocale(path, data): | |
| 264 parsed = json.loads(data) | |
| 265 | |
| 266 # Delete description from translations | |
| 267 for key, value in parsed.iteritems(): | |
| 268 if "description" in value: | |
| 269 del value["description"] | |
| 270 | |
| 271 file = codecs.open(path, 'wb', encoding='utf-8') | |
| 272 json.dump(parsed, file, ensure_ascii=False, sort_keys=True, indent=2, separato rs=(',', ': ')) | |
| 273 file.close() | |
| 274 | |
| 275 def setupTranslations(type, locales, projectName, key): | |
| 276 # Copy locales list, we don't want to change the parameter | |
| 166 locales = set(locales) | 277 locales = set(locales) |
| 167 firefoxLocales = urllib2.urlopen('http://www.mozilla.org/en-US/firefox/all.htm l').read() | 278 |
| 168 for match in re.finditer(r'&lang=([\w\-]+)"', firefoxLocales): | 279 # Fill up with locales that we don't have but the browser supports |
| 169 locales.add(langMapping.get(match.group(1), match.group(1))) | 280 if type == 'chrome': |
| 170 langPacks = urllib2.urlopen('https://addons.mozilla.org/en-US/firefox/language -tools/').read() | 281 for locale in chromeLocales: |
| 171 for match in re.finditer(r'<tr>.*?</tr>', langPacks, re.S): | 282 locales.add(locale) |
| 172 if match.group(0).find('Install Language Pack') >= 0: | 283 else: |
| 173 match2 = re.search(r'lang="([\w\-]+)"', match.group(0)) | 284 firefoxLocales = urllib2.urlopen('http://www.mozilla.org/en-US/firefox/all.h tml').read() |
| 174 if match2: | 285 for match in re.finditer(r'&lang=([\w\-]+)"', firefoxLocales): |
| 175 locales.add(langMapping.get(match2.group(1), match2.group(1))) | 286 locales.add(mapLocale(type, match.group(1))) |
| 287 langPacks = urllib2.urlopen('https://addons.mozilla.org/en-US/firefox/langua ge-tools/').read() | |
| 288 for match in re.finditer(r'<tr>.*?</tr>', langPacks, re.S): | |
| 289 if match.group(0).find('Install Language Pack') >= 0: | |
| 290 match2 = re.search(r'lang="([\w\-]+)"', match.group(0)) | |
| 291 if match2: | |
| 292 locales.add(mapLocale(type, match2.group(1))) | |
| 293 | |
| 294 # Convert locale codes to the ones that Crowdin will understand | |
| 295 locales = set(map(lambda locale: mapLocale(type, locale), locales)) | |
| 176 | 296 |
| 177 allowed = set() | 297 allowed = set() |
| 178 allowedLocales = urllib2.urlopen('http://crowdin.net/page/language-codes').rea d() | 298 allowedLocales = urllib2.urlopen('http://crowdin.net/page/language-codes').rea d() |
| 179 for match in re.finditer(r'<tr>\s*<td>([\w\-]+)</td>', allowedLocales, re.S): | 299 for match in re.finditer(r'<tr>\s*<td>([\w\-]+)</td>', allowedLocales, re.S): |
| 180 allowed.add(match.group(1)) | 300 allowed.add(match.group(1)) |
| 181 if not allowed.issuperset(locales): | 301 if not allowed.issuperset(locales): |
| 182 print 'Warning, following locales aren\'t allowed by server: ' + ', '.join(l ocales - allowed) | 302 print 'Warning, following locales aren\'t allowed by server: ' + ', '.join(l ocales - allowed) |
| 183 | 303 |
| 184 locales = list(locales & allowed) | 304 locales = list(locales & allowed) |
| 185 locales.sort() | 305 locales.sort() |
| 186 params = urllib.urlencode([('languages[]', locale) for locale in locales]) | 306 params = urllib.urlencode([('languages[]', locale) for locale in locales]) |
| 187 result = urllib2.urlopen('http://api.crowdin.net/api/project/%s/edit-project?k ey=%s&%s' % (projectName, key, params)).read() | 307 result = urllib2.urlopen('http://api.crowdin.net/api/project/%s/edit-project?k ey=%s&%s' % (projectName, key, params)).read() |
| 188 if result.find('<success') < 0: | 308 if result.find('<success') < 0: |
| 189 raise Exception('Server indicated that the operation was not successful\n' + result) | 309 raise Exception('Server indicated that the operation was not successful\n' + result) |
| 190 | 310 |
| 191 def updateTranslationMaster(dir, locale, projectName, key): | 311 def postFiles(files, url): |
| 312 boundary = '----------ThIs_Is_tHe_bouNdaRY_$' | |
|
Felix Dahlke
2013/01/10 16:36:20
Would it make sense to generate this randomly?
Wladimir Palant
2013/01/16 14:20:15
I'm not sure where this boundary string originates
| |
| 313 body = '' | |
| 314 for file, data in files: | |
| 315 body += '--%s\r\n' % boundary | |
| 316 body += 'Content-Disposition: form-data; name="files[%s]"; filename="%s"\r\n ' % (file, file) | |
| 317 body += 'Content-Type: application/octet-stream\r\n' | |
| 318 body += 'Content-Transfer-Encoding: binary\r\n' | |
| 319 body += '\r\n' + data + '\r\n' | |
| 320 body += '--%s--\r\n' % boundary | |
| 321 | |
| 322 body = body.encode('utf-8') | |
| 323 request = urllib2.Request(url, StringIO(body)) | |
| 324 request.add_header('Content-Type', 'multipart/form-data; boundary=%s' % bounda ry) | |
| 325 request.add_header('Content-Length', len(body)) | |
| 326 result = urllib2.urlopen(request).read() | |
| 327 if result.find('<success') < 0: | |
| 328 raise Exception('Server indicated that the operation was not successful\n' + result) | |
| 329 | |
| 330 def updateTranslationMaster(type, metadata, dir, projectName, key): | |
| 192 result = json.load(urllib2.urlopen('http://api.crowdin.net/api/project/%s/info ?key=%s&json=1' % (projectName, key))) | 331 result = json.load(urllib2.urlopen('http://api.crowdin.net/api/project/%s/info ?key=%s&json=1' % (projectName, key))) |
| 193 | 332 |
| 194 existing = set(map(lambda f: f['name'], result['files'])) | 333 existing = set(map(lambda f: f['name'], result['files'])) |
| 195 add = [] | 334 add = [] |
| 196 update = [] | 335 update = [] |
| 197 for file in os.listdir(dir): | 336 for file in os.listdir(dir): |
| 198 path = os.path.join(dir, file) | 337 path = os.path.join(dir, file) |
| 199 if os.path.isfile(path): | 338 if os.path.isfile(path): |
| 200 data = toJSON(path) | 339 if type == 'chrome': |
| 340 data = preprocessChromeLocale(path, metadata, True) | |
| 341 newName = file | |
| 342 else: | |
| 343 data = toJSON(path) | |
| 344 newName = file + '.json' | |
| 345 | |
| 201 if data: | 346 if data: |
| 202 newName = file + '.json' | |
| 203 if newName in existing: | 347 if newName in existing: |
| 204 update.append((newName, data)) | 348 update.append((newName, data)) |
| 205 existing.remove(newName) | 349 existing.remove(newName) |
| 206 else: | 350 else: |
| 207 add.append((newName, data)) | 351 add.append((newName, data)) |
| 208 | 352 |
| 209 def postFiles(files, url): | |
| 210 boundary = '----------ThIs_Is_tHe_bouNdaRY_$' | |
| 211 body = '' | |
| 212 for file, data in files: | |
| 213 body += '--%s\r\n' % boundary | |
| 214 body += 'Content-Disposition: form-data; name="files[%s]"; filename="%s"\r \n' % (file, file) | |
| 215 body += 'Content-Type: application/octet-stream\r\n' | |
| 216 body += '\r\n' + data.encode('utf-8') + '\r\n' | |
| 217 body += '--%s--\r\n' % boundary | |
| 218 | |
| 219 request = urllib2.Request(url, body) | |
| 220 request.add_header('Content-Type', 'multipart/form-data; boundary=%s' % boun dary) | |
| 221 request.add_header('Content-Length', len(body)) | |
| 222 result = urllib2.urlopen(request).read() | |
| 223 if result.find('<success') < 0: | |
| 224 raise Exception('Server indicated that the operation was not successful\n' + result) | |
| 225 | |
| 226 if len(add): | 353 if len(add): |
| 227 titles = urllib.urlencode([('titles[%s]' % name, re.sub(r'\.json', '', name) ) for name, data in add]) | 354 titles = urllib.urlencode([('titles[%s]' % name, re.sub(r'\.json', '', name) ) for name, data in add]) |
| 228 postFiles(add, 'http://api.crowdin.net/api/project/%s/add-file?key=%s&type=c hrome&%s' % (projectName, key, titles)) | 355 postFiles(add, 'http://api.crowdin.net/api/project/%s/add-file?key=%s&type=c hrome&%s' % (projectName, key, titles)) |
| 229 if len(update): | 356 if len(update): |
| 230 postFiles(update, 'http://api.crowdin.net/api/project/%s/update-file?key=%s' % (projectName, key)) | 357 postFiles(update, 'http://api.crowdin.net/api/project/%s/update-file?key=%s' % (projectName, key)) |
| 231 for file in existing: | 358 for file in existing: |
| 232 result = urllib2.urlopen('http://api.crowdin.net/api/project/%s/delete-file? key=%s&file=%s' % (projectName, key, file)).read() | 359 result = urllib2.urlopen('http://api.crowdin.net/api/project/%s/delete-file? key=%s&file=%s' % (projectName, key, file)).read() |
| 233 if result.find('<success') < 0: | 360 if result.find('<success') < 0: |
| 234 raise Exception('Server indicated that the operation was not successful\n' + result) | 361 raise Exception('Server indicated that the operation was not successful\n' + result) |
| 235 | 362 |
| 236 def getTranslations(localesDir, defaultLocale, projectName, key): | 363 def uploadTranslations(type, metadata, dir, locale, projectName, key): |
| 364 files = [] | |
| 365 for file in os.listdir(dir): | |
| 366 path = os.path.join(dir, file) | |
| 367 if os.path.isfile(path): | |
| 368 if type == 'chrome': | |
| 369 data = preprocessChromeLocale(path, metadata, False) | |
| 370 newName = file | |
| 371 else: | |
| 372 data = toJSON(path) | |
| 373 newName = file + '.json' | |
| 374 | |
| 375 if data: | |
| 376 files.append((newName, data)) | |
| 377 if len(files): | |
| 378 postFiles(files, 'http://api.crowdin.net/api/project/%s/upload-translation?k ey=%s&language=%s' % (projectName, key, mapLocale(type, locale))) | |
| 379 | |
| 380 def getTranslations(type, localesDir, defaultLocale, projectName, key): | |
| 237 result = urllib2.urlopen('http://api.crowdin.net/api/project/%s/export?key=%s' % (projectName, key)).read() | 381 result = urllib2.urlopen('http://api.crowdin.net/api/project/%s/export?key=%s' % (projectName, key)).read() |
| 238 if result.find('<success') < 0: | 382 if result.find('<success') < 0: |
| 239 raise Exception('Server indicated that the operation was not successful\n' + result) | 383 raise Exception('Server indicated that the operation was not successful\n' + result) |
| 240 | 384 |
| 241 result = urllib2.urlopen('http://api.crowdin.net/api/project/%s/download/all.z ip?key=%s' % (projectName, key)).read() | 385 result = urllib2.urlopen('http://api.crowdin.net/api/project/%s/download/all.z ip?key=%s' % (projectName, key)).read() |
| 242 zip = ZipFile(StringIO(result)) | 386 zip = ZipFile(StringIO(result)) |
| 243 dirs = {} | 387 dirs = {} |
| 244 for info in zip.infolist(): | 388 for info in zip.infolist(): |
| 245 if not info.filename.endswith('.dtd.json') and not info.filename.endswith('. properties.json'): | 389 if not info.filename.endswith('.json'): |
| 246 continue | 390 continue |
| 247 | 391 |
| 248 dir, file = os.path.split(info.filename) | 392 dir, file = os.path.split(info.filename) |
| 249 origFile = re.sub(r'\.json$', '', file) | |
| 250 if not re.match(r'^[\w\-]+$', dir) or dir == defaultLocale: | 393 if not re.match(r'^[\w\-]+$', dir) or dir == defaultLocale: |
| 251 continue | 394 continue |
| 252 | 395 if type == 'chrome': |
| 253 for key, value in langMapping.iteritems(): | 396 origFile = file |
| 397 else: | |
| 398 origFile = re.sub(r'\.json$', '', file) | |
| 399 if not origFile.endswith('.dtd') and not origFile.endswith('.properties'): | |
| 400 continue | |
| 401 | |
| 402 mapping = langMappingChrome if type == 'chrome' else langMappingGecko | |
| 403 for key, value in mapping.iteritems(): | |
| 254 if value == dir: | 404 if value == dir: |
| 255 dir = key | 405 dir = key |
| 406 if type == 'chrome': | |
| 407 dir = dir.replace('-', '_') | |
| 408 | |
| 409 data = zip.open(info.filename).read() | |
| 410 if data == '[]': | |
| 411 continue | |
| 256 | 412 |
| 257 if not dir in dirs: | 413 if not dir in dirs: |
| 258 dirs[dir] = set() | 414 dirs[dir] = set() |
| 259 dirs[dir].add(origFile) | 415 dirs[dir].add(origFile) |
| 260 | 416 |
| 261 data = zip.open(info.filename).read() | 417 path = os.path.join(localesDir, dir, origFile) |
| 262 fromJSON(os.path.join(localesDir, dir, origFile), data) | 418 if not os.path.exists(os.path.dirname(path)): |
| 419 os.makedirs(os.path.dirname(path)) | |
| 420 if type == 'chrome': | |
| 421 postprocessChromeLocale(path, data) | |
| 422 else: | |
| 423 fromJSON(path, data) | |
| 263 | 424 |
| 264 # Remove any extra files | 425 # Remove any extra files |
| 265 for dir, files in dirs.iteritems(): | 426 for dir, files in dirs.iteritems(): |
| 266 baseDir = os.path.join(localesDir, dir) | 427 baseDir = os.path.join(localesDir, dir) |
| 267 if not os.path.exists(baseDir): | 428 if not os.path.exists(baseDir): |
| 268 continue | 429 continue |
| 269 for file in os.listdir(baseDir): | 430 for file in os.listdir(baseDir): |
| 270 path = os.path.join(baseDir, file) | 431 path = os.path.join(baseDir, file) |
| 271 if os.path.isfile(path) and (file.endswith('.properties') or file.endswith ('.dtd')) and not file in files: | 432 if os.path.isfile(path) and (file.endswith('.json') or file.endswith('.pro perties') or file.endswith('.dtd')) and not file in files: |
| 272 os.remove(path) | 433 os.remove(path) |
| OLD | NEW |