OLD | NEW |
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 |
(...skipping 47 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
58 except urllib2.HTTPError as e: | 58 except urllib2.HTTPError as e: |
59 raise Exception('Server returned HTTP Error {}:\n{}'.format(e.code, | 59 raise Exception('Server returned HTTP Error {}:\n{}'.format(e.code, |
60 e.read())) | 60 e.read())) |
61 | 61 |
62 if not raw: | 62 if not raw: |
63 return json.loads(result) | 63 return json.loads(result) |
64 | 64 |
65 return result | 65 return result |
66 | 66 |
67 | 67 |
68 class OrderedDict(dict): | |
69 def __init__(self): | |
70 self.__order = [] | |
71 | |
72 def __setitem__(self, key, value): | |
73 self.__order.append(key) | |
74 dict.__setitem__(self, key, value) | |
75 | |
76 def iteritems(self): | |
77 done = set() | |
78 for key in self.__order: | |
79 if not key in done and key in self: | |
80 yield (key, self[key]) | |
81 done.add(key) | |
82 | |
83 | |
84 def escapeEntity(value): | |
85 return value.replace('&', '&').replace('<', '<').replace('>', '>')
.replace('"', '"') | |
86 | |
87 | |
88 def unescapeEntity(value): | |
89 return value.replace('&', '&').replace('<', '<').replace('>', '>')
.replace('"', '"') | |
90 | |
91 | |
92 def parseDTDString(data, path): | |
93 result = [] | |
94 currentComment = [None] | |
95 | |
96 parser = ParserCreate() | |
97 parser.UseForeignDTD(True) | |
98 parser.SetParamEntityParsing(XML_PARAM_ENTITY_PARSING_ALWAYS) | |
99 | |
100 def ExternalEntityRefHandler(context, base, systemId, publicId): | |
101 subparser = parser.ExternalEntityParserCreate(context, 'utf-8') | |
102 subparser.Parse(data.encode('utf-8'), True) | |
103 return 1 | |
104 | |
105 def CommentHandler(data): | |
106 currentComment[0] = data.strip() | |
107 | |
108 def EntityDeclHandler(entityName, is_parameter_entity, value, base, systemId
, publicId, notationName): | |
109 result.append((unescapeEntity(entityName), currentComment[0], unescapeEn
tity(value.strip()))) | |
110 currentComment[0] = None | |
111 | |
112 parser.ExternalEntityRefHandler = ExternalEntityRefHandler | |
113 parser.CommentHandler = CommentHandler | |
114 parser.EntityDeclHandler = EntityDeclHandler | |
115 parser.Parse('<!DOCTYPE root SYSTEM "foo"><root/>', True) | |
116 | |
117 for entry in result: | |
118 yield entry | |
119 | |
120 | |
121 def escapeProperty(value): | |
122 return value.replace('\n', '\\n') | |
123 | |
124 | |
125 def unescapeProperty(value): | |
126 return value.replace('\\n', '\n') | |
127 | |
128 | |
129 def parsePropertiesString(data, path): | |
130 currentComment = None | |
131 for line in data.splitlines(): | |
132 match = re.search(r'^\s*[#!]\s*(.*)', line) | |
133 if match: | |
134 currentComment = match.group(1) | |
135 elif '=' in line: | |
136 key, value = line.split('=', 1) | |
137 yield (unescapeProperty(key), currentComment, unescapeProperty(value
)) | |
138 currentComment = None | |
139 elif re.search(r'\S', line): | |
140 print >>sys.stderr, 'Unrecognized data in file %s: %s' % (path, line
) | |
141 | |
142 | |
143 def parseString(data, path): | |
144 result = {'_origData': data} | |
145 if path.endswith('.dtd'): | |
146 it = parseDTDString(data, path) | |
147 elif path.endswith('.properties'): | |
148 it = parsePropertiesString(data, path) | |
149 else: | |
150 return None | |
151 | |
152 for name, comment, value in it: | |
153 result[name] = value | |
154 return result | |
155 | |
156 | |
157 def readFile(path): | |
158 fileHandle = codecs.open(path, 'rb', encoding='utf-8') | |
159 data = fileHandle.read() | |
160 fileHandle.close() | |
161 return parseString(data, path) | |
162 | |
163 | |
164 def generateStringEntry(key, value, path): | |
165 if path.endswith('.dtd'): | |
166 return '<!ENTITY %s "%s">\n' % (escapeEntity(key), escapeEntity(value)) | |
167 else: | |
168 return '%s=%s\n' % (escapeProperty(key), escapeProperty(value)) | |
169 | |
170 | |
171 def toJSON(path): | |
172 fileHandle = codecs.open(path, 'rb', encoding='utf-8') | |
173 data = fileHandle.read() | |
174 fileHandle.close() | |
175 | |
176 if path.endswith('.dtd'): | |
177 it = parseDTDString(data, path) | |
178 elif path.endswith('.properties'): | |
179 it = parsePropertiesString(data, path) | |
180 else: | |
181 return None | |
182 | |
183 result = OrderedDict() | |
184 for name, comment, value in it: | |
185 obj = {'message': value} | |
186 if comment == None: | |
187 obj['description'] = name | |
188 else: | |
189 obj['description'] = '%s: %s' % (name, comment) | |
190 result[name] = obj | |
191 return json.dumps(result, ensure_ascii=False, indent=2) | |
192 | |
193 | |
194 def fromJSON(path, data): | |
195 data = json.loads(data) | |
196 if not data: | |
197 if os.path.exists(path): | |
198 os.remove(path) | |
199 return | |
200 | |
201 dir = os.path.dirname(path) | |
202 if not os.path.exists(dir): | |
203 os.makedirs(dir) | |
204 file = codecs.open(path, 'wb', encoding='utf-8') | |
205 for key, value in data.iteritems(): | |
206 file.write(generateStringEntry(key, value['message'], path)) | |
207 file.close() | |
208 | |
209 | |
210 def preprocessChromeLocale(path, metadata, isMaster): | 68 def preprocessChromeLocale(path, metadata, isMaster): |
211 fileHandle = codecs.open(path, 'rb', encoding='utf-8') | 69 fileHandle = codecs.open(path, 'rb', encoding='utf-8') |
212 data = json.load(fileHandle) | 70 data = json.load(fileHandle) |
213 fileHandle.close() | 71 fileHandle.close() |
214 | 72 |
215 for key, value in data.iteritems(): | 73 for key, value in data.iteritems(): |
216 if isMaster: | 74 if isMaster: |
217 # Make sure the key name is listed in the description | 75 # Make sure the key name is listed in the description |
218 if 'description' in value: | 76 if 'description' in value: |
219 value['description'] = '%s: %s' % (key, value['description']) | 77 value['description'] = '%s: %s' % (key, value['description']) |
(...skipping 98 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
318 | 176 |
319 def updateTranslationMaster(localeConfig, metadata, dir, projectName, key): | 177 def updateTranslationMaster(localeConfig, metadata, dir, projectName, key): |
320 result = crowdin_request(projectName, 'info', key) | 178 result = crowdin_request(projectName, 'info', key) |
321 | 179 |
322 existing = set(map(lambda f: f['name'], result['files'])) | 180 existing = set(map(lambda f: f['name'], result['files'])) |
323 add = [] | 181 add = [] |
324 update = [] | 182 update = [] |
325 for file in os.listdir(dir): | 183 for file in os.listdir(dir): |
326 path = os.path.join(dir, file) | 184 path = os.path.join(dir, file) |
327 if os.path.isfile(path): | 185 if os.path.isfile(path): |
328 if localeConfig['file_format'] == 'chrome-json' and file.endswith('.
json'): | 186 if file.endswith('.json'): |
329 data = preprocessChromeLocale(path, metadata, True) | 187 data = preprocessChromeLocale(path, metadata, True) |
330 newName = file | 188 newName = file |
331 elif localeConfig['file_format'] == 'chrome-json': | 189 else: |
332 fileHandle = codecs.open(path, 'rb', encoding='utf-8') | 190 fileHandle = codecs.open(path, 'rb', encoding='utf-8') |
333 data = json.dumps({file: {'message': fileHandle.read()}}) | 191 data = json.dumps({file: {'message': fileHandle.read()}}) |
334 fileHandle.close() | 192 fileHandle.close() |
335 newName = file + '.json' | 193 newName = file + '.json' |
336 else: | |
337 data = toJSON(path) | |
338 newName = file + '.json' | |
339 | 194 |
340 if data: | 195 if data: |
341 if newName in existing: | 196 if newName in existing: |
342 update.append((newName, data)) | 197 update.append((newName, data)) |
343 existing.remove(newName) | 198 existing.remove(newName) |
344 else: | 199 else: |
345 add.append((newName, data)) | 200 add.append((newName, data)) |
346 | 201 |
347 if len(add): | 202 if len(add): |
348 query = {'titles[{}]'.format(name): os.path.splitext(name)[0] | 203 query = {'titles[{}]'.format(name): os.path.splitext(name)[0] |
349 for name, _ in add} | 204 for name, _ in add} |
350 query['type'] = 'chrome' | 205 query['type'] = 'chrome' |
351 data, headers = crowdin_prepare_upload(add) | 206 data, headers = crowdin_prepare_upload(add) |
352 crowdin_request(projectName, 'add-file', key, query, post_data=data, | 207 crowdin_request(projectName, 'add-file', key, query, post_data=data, |
353 headers=headers) | 208 headers=headers) |
354 if len(update): | 209 if len(update): |
355 data, headers = crowdin_prepare_upload(update) | 210 data, headers = crowdin_prepare_upload(update) |
356 crowdin_request(projectName, 'update-file', key, post_data=data, | 211 crowdin_request(projectName, 'update-file', key, post_data=data, |
357 headers=headers) | 212 headers=headers) |
358 for file in existing: | 213 for file in existing: |
359 crowdin_request(projectName, 'delete-file', key, {'file': file}) | 214 crowdin_request(projectName, 'delete-file', key, {'file': file}) |
360 | 215 |
361 | 216 |
362 def uploadTranslations(localeConfig, metadata, dir, locale, projectName, key): | 217 def uploadTranslations(localeConfig, metadata, dir, locale, projectName, key): |
363 files = [] | 218 files = [] |
364 for file in os.listdir(dir): | 219 for file in os.listdir(dir): |
365 path = os.path.join(dir, file) | 220 path = os.path.join(dir, file) |
366 if os.path.isfile(path): | 221 if os.path.isfile(path): |
367 if localeConfig['file_format'] == 'chrome-json' and file.endswith('.
json'): | 222 if file.endswith('.json'): |
368 data = preprocessChromeLocale(path, metadata, False) | 223 data = preprocessChromeLocale(path, metadata, False) |
369 newName = file | 224 newName = file |
370 elif localeConfig['file_format'] == 'chrome-json': | 225 else: |
371 fileHandle = codecs.open(path, 'rb', encoding='utf-8') | 226 fileHandle = codecs.open(path, 'rb', encoding='utf-8') |
372 data = json.dumps({file: {'message': fileHandle.read()}}) | 227 data = json.dumps({file: {'message': fileHandle.read()}}) |
373 fileHandle.close() | 228 fileHandle.close() |
374 newName = file + '.json' | 229 newName = file + '.json' |
375 else: | |
376 data = toJSON(path) | |
377 newName = file + '.json' | |
378 | 230 |
379 if data: | 231 if data: |
380 files.append((newName, data)) | 232 files.append((newName, data)) |
381 if len(files): | 233 if len(files): |
382 language = CROWDIN_LANG_MAPPING.get(locale, locale) | 234 language = CROWDIN_LANG_MAPPING.get(locale, locale) |
383 data, headers = crowdin_prepare_upload(files) | 235 data, headers = crowdin_prepare_upload(files) |
384 crowdin_request(projectName, 'upload-translation', key, | 236 crowdin_request(projectName, 'upload-translation', key, |
385 {'language': language}, post_data=data, | 237 {'language': language}, post_data=data, |
386 headers=headers) | 238 headers=headers) |
387 | 239 |
388 | 240 |
389 def getTranslations(localeConfig, projectName, key): | 241 def getTranslations(localeConfig, projectName, key): |
390 """Download all available translations from crowdin. | 242 """Download all available translations from crowdin. |
391 | 243 |
392 Trigger crowdin to build the available export, wait for crowdin to | 244 Trigger crowdin to build the available export, wait for crowdin to |
393 finish the job and download the generated zip afterwards. | 245 finish the job and download the generated zip afterwards. |
394 """ | 246 """ |
395 crowdin_request(projectName, 'export', key) | 247 crowdin_request(projectName, 'export', key) |
396 | 248 |
397 result = crowdin_request(projectName, 'download/all.zip', key, raw=True) | 249 result = crowdin_request(projectName, 'download/all.zip', key, raw=True) |
398 zip = ZipFile(StringIO(result)) | 250 zip = ZipFile(StringIO(result)) |
399 dirs = {} | 251 dirs = {} |
400 | 252 |
401 normalizedDefaultLocale = localeConfig['default_locale'] | 253 normalizedDefaultLocale = localeConfig['default_locale'].replace('_', '-') |
402 if localeConfig['name_format'] == 'ISO-15897': | |
403 normalizedDefaultLocale = normalizedDefaultLocale.replace('_', '-') | |
404 normalizedDefaultLocale = CROWDIN_LANG_MAPPING.get(normalizedDefaultLocale, | 254 normalizedDefaultLocale = CROWDIN_LANG_MAPPING.get(normalizedDefaultLocale, |
405 normalizedDefaultLocale) | 255 normalizedDefaultLocale) |
406 | 256 |
407 for info in zip.infolist(): | 257 for info in zip.infolist(): |
408 if not info.filename.endswith('.json'): | 258 if not info.filename.endswith('.json'): |
409 continue | 259 continue |
410 | 260 |
411 dir, file = os.path.split(info.filename) | 261 dir, file = os.path.split(info.filename) |
412 if not re.match(r'^[\w\-]+$', dir) or dir == normalizedDefaultLocale: | 262 if not re.match(r'^[\w\-]+$', dir) or dir == normalizedDefaultLocale: |
413 continue | 263 continue |
414 if localeConfig['file_format'] == 'chrome-json' and file.count('.') == 1
: | 264 if file.count('.') == 1: |
415 origFile = file | 265 origFile = file |
416 else: | 266 else: |
417 origFile = re.sub(r'\.json$', '', file) | 267 origFile = os.path.splitext(file)[0] |
418 if (localeConfig['file_format'] == 'gecko-dtd' and | |
419 not origFile.endswith('.dtd') and | |
420 not origFile.endswith('.properties')): | |
421 continue | |
422 | 268 |
423 for key, value in CROWDIN_LANG_MAPPING.iteritems(): | 269 for key, value in CROWDIN_LANG_MAPPING.iteritems(): |
424 if value == dir: | 270 if value == dir: |
425 dir = key | 271 dir = key |
426 if localeConfig['name_format'] == 'ISO-15897': | |
427 dir = dir.replace('-', '_') | |
428 | 272 |
429 data = zip.open(info.filename).read() | 273 data = zip.open(info.filename).read() |
430 if data == '[]': | 274 if data == '[]': |
431 continue | 275 continue |
432 | 276 |
433 if not dir in dirs: | 277 if not dir in dirs: |
434 dirs[dir] = set() | 278 dirs[dir] = set() |
435 dirs[dir].add(origFile) | 279 dirs[dir].add(origFile) |
436 | 280 |
437 path = os.path.join(localeConfig['base_path'], dir, origFile) | 281 path = os.path.join(localeConfig['base_path'], dir, origFile) |
438 if not os.path.exists(os.path.dirname(path)): | 282 if not os.path.exists(os.path.dirname(path)): |
439 os.makedirs(os.path.dirname(path)) | 283 os.makedirs(os.path.dirname(path)) |
440 if localeConfig['file_format'] == 'chrome-json' and file.endswith('.json
'): | 284 if file.endswith('.json'): |
441 postprocessChromeLocale(path, data) | 285 postprocessChromeLocale(path, data) |
442 elif localeConfig['file_format'] == 'chrome-json': | 286 else: |
443 data = json.loads(data) | 287 data = json.loads(data) |
444 if origFile in data: | 288 if origFile in data: |
445 fileHandle = codecs.open(path, 'wb', encoding='utf-8') | 289 fileHandle = codecs.open(path, 'wb', encoding='utf-8') |
446 fileHandle.write(data[origFile]['message']) | 290 fileHandle.write(data[origFile]['message']) |
447 fileHandle.close() | 291 fileHandle.close() |
448 else: | |
449 fromJSON(path, data) | |
450 | 292 |
451 # Remove any extra files | 293 # Remove any extra files |
452 for dir, files in dirs.iteritems(): | 294 for dir, files in dirs.iteritems(): |
453 baseDir = os.path.join(localeConfig['base_path'], dir) | 295 baseDir = os.path.join(localeConfig['base_path'], dir) |
454 if not os.path.exists(baseDir): | 296 if not os.path.exists(baseDir): |
455 continue | 297 continue |
456 for file in os.listdir(baseDir): | 298 for file in os.listdir(baseDir): |
457 path = os.path.join(baseDir, file) | 299 path = os.path.join(baseDir, file) |
458 if os.path.isfile(path) and (file.endswith('.json') or file.endswith
('.properties') or file.endswith('.dtd')) and not file in files: | 300 valid_extension = file.endswith('.json') |
| 301 if os.path.isfile(path) and valid_extension and not file in files: |
459 os.remove(path) | 302 os.remove(path) |
OLD | NEW |