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

Side by Side Diff: cms/bin/translate.py

Issue 29345291: Noissue - Adapt quotes for compliance with our coding style in the CMS (Closed)
Patch Set: Created May 29, 2016, 1:27 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 | « cms/bin/test_server.py ('k') | cms/converters.py » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 # This file is part of the Adblock Plus web scripts, 1 # This file is part of the Adblock Plus web scripts,
2 # Copyright (C) 2006-2016 Eyeo GmbH 2 # Copyright (C) 2006-2016 Eyeo GmbH
3 # 3 #
4 # Adblock Plus is free software: you can redistribute it and/or modify 4 # Adblock Plus is free software: you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License version 3 as 5 # it under the terms of the GNU General Public License version 3 as
6 # published by the Free Software Foundation. 6 # published by the Free Software Foundation.
7 # 7 #
8 # Adblock Plus is distributed in the hope that it will be useful, 8 # Adblock Plus is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of 9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
(...skipping 12 matching lines...) Expand all
23 import posixpath 23 import posixpath
24 import sys 24 import sys
25 import urllib 25 import urllib
26 import zipfile 26 import zipfile
27 27
28 import urllib3 28 import urllib3
29 29
30 import cms.utils 30 import cms.utils
31 from cms.sources import FileSource 31 from cms.sources import FileSource
32 32
33 logger = logging.getLogger("cms.bin.translate") 33 logger = logging.getLogger('cms.bin.translate')
34 34
35 35
36 class CrowdinAPI: 36 class CrowdinAPI:
37 FILES_PER_REQUEST = 20 37 FILES_PER_REQUEST = 20
38 38
39 def __init__(self, api_key, project_name): 39 def __init__(self, api_key, project_name):
40 self.api_key = api_key 40 self.api_key = api_key
41 self.project_name = project_name 41 self.project_name = project_name
42 self.connection = urllib3.connection_from_url("https://api.crowdin.com/" ) 42 self.connection = urllib3.connection_from_url('https://api.crowdin.com/' )
43 43
44 def raw_request(self, request_method, api_endpoint, query_params=(), **kwarg s): 44 def raw_request(self, request_method, api_endpoint, query_params=(), **kwarg s):
45 url = "/api/project/%s/%s?%s" % ( 45 url = '/api/project/%s/%s?%s' % (
46 urllib.quote(self.project_name), 46 urllib.quote(self.project_name),
47 urllib.quote(api_endpoint), 47 urllib.quote(api_endpoint),
48 urllib.urlencode((("key", self.api_key),) + query_params) 48 urllib.urlencode((('key', self.api_key),) + query_params)
49 ) 49 )
50 try: 50 try:
51 response = self.connection.request( 51 response = self.connection.request(
52 request_method, str(url), **kwargs 52 request_method, str(url), **kwargs
53 ) 53 )
54 except urllib3.exceptions.HTTPError: 54 except urllib3.exceptions.HTTPError:
55 logger.error("Connection to API endpoint %s failed", url) 55 logger.error('Connection to API endpoint %s failed', url)
56 raise 56 raise
57 if response.status < 200 or response.status >= 300: 57 if response.status < 200 or response.status >= 300:
58 logger.error("API call to %s failed:\n%s", url, response.data) 58 logger.error('API call to %s failed:\n%s', url, response.data)
59 raise urllib3.exceptions.HTTPError(response.status) 59 raise urllib3.exceptions.HTTPError(response.status)
60 return response 60 return response
61 61
62 def request(self, request_method, api_endpoint, data=None, files=None): 62 def request(self, request_method, api_endpoint, data=None, files=None):
63 fields = [] 63 fields = []
64 if data: 64 if data:
65 for name, value in data.iteritems(): 65 for name, value in data.iteritems():
66 if isinstance(value, basestring): 66 if isinstance(value, basestring):
67 fields.append((name, value)) 67 fields.append((name, value))
68 else: 68 else:
69 fields.extend((name + "[]", v) for v in value) 69 fields.extend((name + '[]', v) for v in value)
70 if files: 70 if files:
71 fields.extend(("files[%s]" % f[0], f) for f in files) 71 fields.extend(('files[%s]' % f[0], f) for f in files)
72 72
73 response = self.raw_request( 73 response = self.raw_request(
74 request_method, api_endpoint, (("json", "1"),), 74 request_method, api_endpoint, (('json', '1'),),
75 fields=fields, preload_content=False 75 fields=fields, preload_content=False
76 ) 76 )
77 77
78 try: 78 try:
79 return json.load(response) 79 return json.load(response)
80 except ValueError: 80 except ValueError:
81 logger.error("Invalid response returned by API endpoint %s", url) 81 logger.error('Invalid response returned by API endpoint %s', url)
82 raise 82 raise
83 83
84 84
85 def grouper(iterable, n): 85 def grouper(iterable, n):
86 iterator = iter(iterable) 86 iterator = iter(iterable)
87 while True: 87 while True:
88 chunk = tuple(itertools.islice(iterator, n)) 88 chunk = tuple(itertools.islice(iterator, n))
89 if not chunk: 89 if not chunk:
90 break 90 break
91 yield chunk 91 yield chunk
92 92
93 93
94 def extract_strings(source, defaultlocale): 94 def extract_strings(source, defaultlocale):
95 logger.info("Extracting page strings (please be patient)...") 95 logger.info('Extracting page strings (please be patient)...')
96 page_strings = {} 96 page_strings = {}
97 97
98 def record_string(page, locale, name, value, comment, fixed_strings): 98 def record_string(page, locale, name, value, comment, fixed_strings):
99 if locale != defaultlocale: 99 if locale != defaultlocale:
100 return 100 return
101 101
102 try: 102 try:
103 store = page_strings[page] 103 store = page_strings[page]
104 except KeyError: 104 except KeyError:
105 store = page_strings[page] = collections.OrderedDict() 105 store = page_strings[page] = collections.OrderedDict()
106 106
107 store[name] = {"message": value} 107 store[name] = {'message': value}
108 108
109 if fixed_strings: 109 if fixed_strings:
110 comment = comment + "\n" if comment else "" 110 comment = comment + '\n' if comment else ''
111 comment += ", ".join("{%d}: %s" % i_s 111 comment += ', '.join('{%d}: %s' % i_s
112 for i_s in enumerate(fixed_strings, 1)) 112 for i_s in enumerate(fixed_strings, 1))
113 if comment: 113 if comment:
114 store[name]["description"] = comment 114 store[name]['description'] = comment
115 115
116 for page, format in source.list_pages(): 116 for page, format in source.list_pages():
117 cms.utils.process_page(source, defaultlocale, page, 117 cms.utils.process_page(source, defaultlocale, page,
118 format=format, localized_string_callback=record_s tring) 118 format=format, localized_string_callback=record_s tring)
119 return page_strings 119 return page_strings
120 120
121 121
122 def configure_locales(crowdin_api, local_locales, enabled_locales, 122 def configure_locales(crowdin_api, local_locales, enabled_locales,
123 defaultlocale): 123 defaultlocale):
124 logger.info("Checking which locales are supported by Crowdin...") 124 logger.info('Checking which locales are supported by Crowdin...')
125 response = crowdin_api.request("GET", "supported-languages") 125 response = crowdin_api.request('GET', 'supported-languages')
126 126
127 supported_locales = {l["crowdin_code"] for l in response} 127 supported_locales = {l['crowdin_code'] for l in response}
128 128
129 # We need to map the locale names we use to the ones that Crowdin is expecti ng 129 # We need to map the locale names we use to the ones that Crowdin is expecti ng
130 # and at the same time ensure that they are supported. 130 # and at the same time ensure that they are supported.
131 required_locales = {} 131 required_locales = {}
132 for locale in local_locales: 132 for locale in local_locales:
133 if "_" in locale: 133 if '_' in locale:
134 crowdin_locale = locale.replace("_", "-") 134 crowdin_locale = locale.replace('_', '-')
135 elif locale in supported_locales: 135 elif locale in supported_locales:
136 crowdin_locale = locale 136 crowdin_locale = locale
137 else: 137 else:
138 crowdin_locale = "%s-%s" % (locale, locale.upper()) 138 crowdin_locale = '%s-%s' % (locale, locale.upper())
139 139
140 if crowdin_locale in supported_locales: 140 if crowdin_locale in supported_locales:
141 required_locales[locale] = crowdin_locale 141 required_locales[locale] = crowdin_locale
142 else: 142 else:
143 logger.warning("Ignoring locale '%s', which Crowdin doesn't support" , 143 logger.warning("Ignoring locale '%s', which Crowdin doesn't support" ,
144 locale) 144 locale)
145 145
146 required_crowdin_locales = set(required_locales.values()) 146 required_crowdin_locales = set(required_locales.values())
147 if not required_crowdin_locales.issubset(enabled_locales): 147 if not required_crowdin_locales.issubset(enabled_locales):
148 logger.info("Enabling the required locales for the Crowdin project...") 148 logger.info('Enabling the required locales for the Crowdin project...')
149 crowdin_api.request( 149 crowdin_api.request(
150 "POST", "edit-project", 150 'POST', 'edit-project',
151 data={"languages": enabled_locales | required_crowdin_locales} 151 data={'languages': enabled_locales | required_crowdin_locales}
152 ) 152 )
153 153
154 return required_locales 154 return required_locales
155 155
156 156
157 def list_remote_files(project_info): 157 def list_remote_files(project_info):
158 def parse_file_node(node, path=""): 158 def parse_file_node(node, path=''):
159 if node["node_type"] == "file": 159 if node['node_type'] == 'file':
160 remote_files.add(path + node["name"]) 160 remote_files.add(path + node['name'])
161 elif node["node_type"] == "directory": 161 elif node['node_type'] == 'directory':
162 dir_name = path + node["name"] 162 dir_name = path + node['name']
163 remote_directories.add(dir_name) 163 remote_directories.add(dir_name)
164 for file in node.get("files", []): 164 for file in node.get('files', []):
165 parse_file_node(file, dir_name + "/") 165 parse_file_node(file, dir_name + '/')
166 166
167 remote_files = set() 167 remote_files = set()
168 remote_directories = set() 168 remote_directories = set()
169 for node in project_info["files"]: 169 for node in project_info['files']:
170 parse_file_node(node) 170 parse_file_node(node)
171 return remote_files, remote_directories 171 return remote_files, remote_directories
172 172
173 173
174 def list_local_files(page_strings): 174 def list_local_files(page_strings):
175 local_files = set() 175 local_files = set()
176 local_directories = set() 176 local_directories = set()
177 for page, strings in page_strings.iteritems(): 177 for page, strings in page_strings.iteritems():
178 if strings: 178 if strings:
179 local_files.add(page + ".json") 179 local_files.add(page + '.json')
180 while "/" in page: 180 while '/' in page:
181 page = page.rsplit("/", 1)[0] 181 page = page.rsplit('/', 1)[0]
182 local_directories.add(page) 182 local_directories.add(page)
183 return local_files, local_directories 183 return local_files, local_directories
184 184
185 185
186 def create_directories(crowdin_api, directories): 186 def create_directories(crowdin_api, directories):
187 for directory in directories: 187 for directory in directories:
188 logger.info("Creating directory %s", directory) 188 logger.info('Creating directory %s', directory)
189 crowdin_api.request("POST", "add-directory", data={"name": directory}) 189 crowdin_api.request('POST', 'add-directory', data={'name': directory})
190 190
191 191
192 def add_update_files(crowdin_api, api_endpoint, message, files, page_strings): 192 def add_update_files(crowdin_api, api_endpoint, message, files, page_strings):
193 for group in grouper(files, crowdin_api.FILES_PER_REQUEST): 193 for group in grouper(files, crowdin_api.FILES_PER_REQUEST):
194 files = [] 194 files = []
195 for file_name in group: 195 for file_name in group:
196 page = os.path.splitext(file_name)[0] 196 page = os.path.splitext(file_name)[0]
197 files.append((file_name, json.dumps(page_strings[page]), "applicatio n/json")) 197 files.append((file_name, json.dumps(page_strings[page]), 'applicatio n/json'))
198 del page_strings[page] 198 del page_strings[page]
199 logger.info(message, len(files)) 199 logger.info(message, len(files))
200 crowdin_api.request("POST", api_endpoint, files=files) 200 crowdin_api.request('POST', api_endpoint, files=files)
201 201
202 202
203 def upload_new_files(crowdin_api, new_files, page_strings): 203 def upload_new_files(crowdin_api, new_files, page_strings):
204 add_update_files(crowdin_api, "add-file", "Uploading %d new pages...", 204 add_update_files(crowdin_api, 'add-file', 'Uploading %d new pages...',
205 new_files, page_strings) 205 new_files, page_strings)
206 206
207 207
208 def update_existing_files(crowdin_api, existing_files, page_strings): 208 def update_existing_files(crowdin_api, existing_files, page_strings):
209 add_update_files(crowdin_api, "update-file", "Updating %d existing pages..." , 209 add_update_files(crowdin_api, 'update-file', 'Updating %d existing pages...' ,
210 existing_files, page_strings) 210 existing_files, page_strings)
211 211
212 212
213 def upload_translations(crowdin_api, source_dir, new_files, required_locales): 213 def upload_translations(crowdin_api, source_dir, new_files, required_locales):
214 def open_locale_files(locale, files): 214 def open_locale_files(locale, files):
215 for file_name in files: 215 for file_name in files:
216 path = os.path.join(source_dir, "locales", locale, file_name) 216 path = os.path.join(source_dir, 'locales', locale, file_name)
217 if os.path.isfile(path): 217 if os.path.isfile(path):
218 with open(path, "rb") as f: 218 with open(path, 'rb') as f:
219 yield (file_name, f.read(), "application/json") 219 yield (file_name, f.read(), 'application/json')
220 220
221 if new_files: 221 if new_files:
222 for locale, crowdin_locale in required_locales.iteritems(): 222 for locale, crowdin_locale in required_locales.iteritems():
223 for files in grouper(open_locale_files(locale, new_files), 223 for files in grouper(open_locale_files(locale, new_files),
224 crowdin_api.FILES_PER_REQUEST): 224 crowdin_api.FILES_PER_REQUEST):
225 logger.info("Uploading %d existing translation " 225 logger.info('Uploading %d existing translation '
226 "files for locale %s...", len(files), locale) 226 'files for locale %s...', len(files), locale)
227 crowdin_api.request("POST", "upload-translation", files=files, 227 crowdin_api.request('POST', 'upload-translation', files=files,
228 data={"language": crowdin_locale}) 228 data={'language': crowdin_locale})
229 229
230 230
231 def remove_old_files(crowdin_api, old_files): 231 def remove_old_files(crowdin_api, old_files):
232 for file_name in old_files: 232 for file_name in old_files:
233 logger.info("Removing old file %s", file_name) 233 logger.info('Removing old file %s', file_name)
234 crowdin_api.request("POST", "delete-file", data={"file": file_name}) 234 crowdin_api.request('POST', 'delete-file', data={'file': file_name})
235 235
236 236
237 def remove_old_directories(crowdin_api, old_directories): 237 def remove_old_directories(crowdin_api, old_directories):
238 for directory in reversed(sorted(old_directories, key=len)): 238 for directory in reversed(sorted(old_directories, key=len)):
239 logger.info("Removing old directory %s", directory) 239 logger.info('Removing old directory %s', directory)
240 crowdin_api.request("POST", "delete-directory", data={"name": directory} ) 240 crowdin_api.request('POST', 'delete-directory', data={'name': directory} )
241 241
242 242
243 def download_translations(crowdin_api, source_dir, required_locales): 243 def download_translations(crowdin_api, source_dir, required_locales):
244 logger.info("Requesting generation of fresh translations archive...") 244 logger.info('Requesting generation of fresh translations archive...')
245 result = crowdin_api.request("GET", "export") 245 result = crowdin_api.request('GET', 'export')
246 if result.get("success", {}).get("status") == "skipped": 246 if result.get('success', {}).get('status') == 'skipped':
247 logger.warning("Archive generation skipped, either " 247 logger.warning('Archive generation skipped, either '
248 "no changes or API usage excessive") 248 'no changes or API usage excessive')
249 249
250 logger.info("Downloading translations archive...") 250 logger.info('Downloading translations archive...')
251 response = crowdin_api.raw_request("GET", "download/all.zip") 251 response = crowdin_api.raw_request('GET', 'download/all.zip')
252 252
253 inverted_required_locales = {crowdin: local for local, crowdin in 253 inverted_required_locales = {crowdin: local for local, crowdin in
254 required_locales.iteritems()} 254 required_locales.iteritems()}
255 logger.info("Extracting translations archive...") 255 logger.info('Extracting translations archive...')
256 with zipfile.ZipFile(io.BytesIO(response.data), "r") as archive: 256 with zipfile.ZipFile(io.BytesIO(response.data), 'r') as archive:
257 locale_path = os.path.join(source_dir, "locales") 257 locale_path = os.path.join(source_dir, 'locales')
258 # First clear existing translation files 258 # First clear existing translation files
259 for root, dirs, files in os.walk(locale_path, topdown=True): 259 for root, dirs, files in os.walk(locale_path, topdown=True):
260 if root == locale_path: 260 if root == locale_path:
261 dirs[:] = [d for d in dirs if d in required_locales] 261 dirs[:] = [d for d in dirs if d in required_locales]
262 for f in files: 262 for f in files:
263 if f.lower().endswith(".json"): 263 if f.lower().endswith('.json'):
264 os.remove(os.path.join(root, f)) 264 os.remove(os.path.join(root, f))
265 # Then extract the new ones in place 265 # Then extract the new ones in place
266 for member in archive.namelist(): 266 for member in archive.namelist():
267 path, file_name = posixpath.split(member) 267 path, file_name = posixpath.split(member)
268 ext = posixpath.splitext(file_name)[1] 268 ext = posixpath.splitext(file_name)[1]
269 path_parts = path.split(posixpath.sep) 269 path_parts = path.split(posixpath.sep)
270 locale, file_path = path_parts[0], path_parts[1:] 270 locale, file_path = path_parts[0], path_parts[1:]
271 if ext.lower() == ".json" and locale in inverted_required_locales: 271 if ext.lower() == '.json' and locale in inverted_required_locales:
272 output_path = os.path.join( 272 output_path = os.path.join(
273 locale_path, inverted_required_locales[locale], 273 locale_path, inverted_required_locales[locale],
274 *file_path + [file_name] 274 *file_path + [file_name]
275 ) 275 )
276 with archive.open(member) as source_file: 276 with archive.open(member) as source_file:
277 locale_file_contents = json.load(source_file) 277 locale_file_contents = json.load(source_file)
278 if len(locale_file_contents): 278 if len(locale_file_contents):
279 with codecs.open(output_path, "wb", "utf-8") as target_f ile: 279 with codecs.open(output_path, 'wb', 'utf-8') as target_f ile:
280 json.dump(locale_file_contents, target_file, ensure_ ascii=False, 280 json.dump(locale_file_contents, target_file, ensure_ ascii=False,
281 sort_keys=True, indent=2, separators=(",", ": ")) 281 sort_keys=True, indent=2, separators=(',', ': '))
282 282
283 283
284 def crowdin_sync(source_dir, crowdin_api_key): 284 def crowdin_sync(source_dir, crowdin_api_key):
285 with FileSource(source_dir) as source: 285 with FileSource(source_dir) as source:
286 config = source.read_config() 286 config = source.read_config()
287 defaultlocale = config.get("general", "defaultlocale") 287 defaultlocale = config.get('general', 'defaultlocale')
288 crowdin_project_name = config.get("general", "crowdin-project-name") 288 crowdin_project_name = config.get('general', 'crowdin-project-name')
289 289
290 crowdin_api = CrowdinAPI(crowdin_api_key, crowdin_project_name) 290 crowdin_api = CrowdinAPI(crowdin_api_key, crowdin_project_name)
291 291
292 logger.info("Requesting project information...") 292 logger.info('Requesting project information...')
293 project_info = crowdin_api.request("GET", "info") 293 project_info = crowdin_api.request('GET', 'info')
294 page_strings = extract_strings(source, defaultlocale) 294 page_strings = extract_strings(source, defaultlocale)
295 295
296 local_locales = source.list_locales() - {defaultlocale} 296 local_locales = source.list_locales() - {defaultlocale}
297 enabled_locales = {l["code"] for l in project_info["languages"]} 297 enabled_locales = {l['code'] for l in project_info['languages']}
298 298
299 required_locales = configure_locales(crowdin_api, local_locales, 299 required_locales = configure_locales(crowdin_api, local_locales,
300 enabled_locales, defaultlocale) 300 enabled_locales, defaultlocale)
301 301
302 remote_files, remote_directories = list_remote_files(project_info) 302 remote_files, remote_directories = list_remote_files(project_info)
303 local_files, local_directories = list_local_files(page_strings) 303 local_files, local_directories = list_local_files(page_strings)
304 304
305 # Avoid deleting all remote content if there was a problem listing local fil es 305 # Avoid deleting all remote content if there was a problem listing local fil es
306 if not local_files: 306 if not local_files:
307 logger.error("No existing strings found, maybe the project directory is " 307 logger.error('No existing strings found, maybe the project directory is '
308 "not set up correctly? Aborting!") 308 'not set up correctly? Aborting!')
309 sys.exit(1) 309 sys.exit(1)
310 310
311 new_files = local_files - remote_files 311 new_files = local_files - remote_files
312 new_directories = local_directories - remote_directories 312 new_directories = local_directories - remote_directories
313 create_directories(crowdin_api, new_directories) 313 create_directories(crowdin_api, new_directories)
314 upload_new_files(crowdin_api, new_files, page_strings) 314 upload_new_files(crowdin_api, new_files, page_strings)
315 upload_translations(crowdin_api, source_dir, new_files, required_locales) 315 upload_translations(crowdin_api, source_dir, new_files, required_locales)
316 316
317 existing_files = local_files - new_files 317 existing_files = local_files - new_files
318 update_existing_files(crowdin_api, existing_files, page_strings) 318 update_existing_files(crowdin_api, existing_files, page_strings)
319 319
320 old_files = remote_files - local_files 320 old_files = remote_files - local_files
321 old_directories = remote_directories - local_directories 321 old_directories = remote_directories - local_directories
322 remove_old_files(crowdin_api, old_files) 322 remove_old_files(crowdin_api, old_files)
323 remove_old_directories(crowdin_api, old_directories) 323 remove_old_directories(crowdin_api, old_directories)
324 324
325 download_translations(crowdin_api, source_dir, required_locales) 325 download_translations(crowdin_api, source_dir, required_locales)
326 logger.info("Crowdin sync completed.") 326 logger.info('Crowdin sync completed.')
327 327
328 if __name__ == "__main__": 328 if __name__ == '__main__':
329 if len(sys.argv) < 3: 329 if len(sys.argv) < 3:
330 print >>sys.stderr, "Usage: python -m cms.bin.translate www_directory cr owdin_project_api_key [logging_level]" 330 print >>sys.stderr, 'Usage: python -m cms.bin.translate www_directory cr owdin_project_api_key [logging_level]'
331 sys.exit(1) 331 sys.exit(1)
332 332
333 logging.basicConfig() 333 logging.basicConfig()
334 logger.setLevel(sys.argv[3] if len(sys.argv) > 3 else logging.INFO) 334 logger.setLevel(sys.argv[3] if len(sys.argv) > 3 else logging.INFO)
335 335
336 source_dir, crowdin_api_key = sys.argv[1:3] 336 source_dir, crowdin_api_key = sys.argv[1:3]
337 crowdin_sync(source_dir, crowdin_api_key) 337 crowdin_sync(source_dir, crowdin_api_key)
OLDNEW
« no previous file with comments | « cms/bin/test_server.py ('k') | cms/converters.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld