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

Delta Between Two Patch Sets: cms/bin/translate.py

Issue 29317015: Issue 2625 - [cms] Crowdin synchronisation script (Closed)
Left Patch Set: Addressed further feedback Created July 11, 2015, 7:17 p.m.
Right Patch Set: Give query_params a default value Created July 16, 2015, 12:47 p.m.
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments.
Jump to:
Left: Side by side diff | Download
Right: Side by side diff | Download
« no previous file with change/comment | « README.md ('k') | cms/converters.py » ('j') | no next file with change/comment »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
LEFTRIGHT
1 # coding: utf-8 1 # coding: utf-8
2 2
3 # This file is part of the Adblock Plus web scripts, 3 # This file is part of the Adblock Plus web scripts,
4 # Copyright (C) 2006-2015 Eyeo GmbH 4 # Copyright (C) 2006-2015 Eyeo GmbH
5 # 5 #
6 # Adblock Plus is free software: you can redistribute it and/or modify 6 # Adblock Plus is free software: you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License version 3 as 7 # it under the terms of the GNU General Public License version 3 as
8 # published by the Free Software Foundation. 8 # published by the Free Software Foundation.
9 # 9 #
10 # Adblock Plus is distributed in the hope that it will be useful, 10 # Adblock Plus is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details. 13 # GNU General Public License for more details.
14 # 14 #
15 # You should have received a copy of the GNU General Public License 15 # You should have received a copy of the GNU General Public License
16 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. 16 # along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>.
17 17
18 import collections 18 import collections
19 import io 19 import io
20 import itertools 20 import itertools
21 import json 21 import json
22 import logging 22 import logging
23 import os 23 import os
24 import posixpath 24 import posixpath
25 import sys 25 import sys
26 import urllib
26 import zipfile 27 import zipfile
27 28
28 import urllib3 29 import urllib3
29 30
30 import cms.utils 31 import cms.utils
31 from cms.sources import FileSource 32 from cms.sources import FileSource
32 33
33 logger = logging.getLogger("cms.bin.translate") 34 logger = logging.getLogger("cms.bin.translate")
34 35
35 class CrowdinAPI: 36 class CrowdinAPI:
36 FILES_PER_REQUEST = 20 37 FILES_PER_REQUEST = 20
37 38
38 def __init__(self, api_key, project_name): 39 def __init__(self, api_key, project_name):
39 self.api_key = api_key 40 self.api_key = api_key
40 self.project_name = project_name 41 self.project_name = project_name
41 self.connection = urllib3.connection_from_url("https://api.crowdin.com/") 42 self.connection = urllib3.connection_from_url("https://api.crowdin.com/")
42 43
44 def raw_request(self, request_method, api_endpoint, query_params=(), **kwargs) :
45 url = "/api/project/%s/%s?%s" % (
46 urllib.quote(self.project_name),
47 urllib.quote(api_endpoint),
48 urllib.urlencode((("key", self.api_key),) + query_params)
49 )
50 try:
51 response = self.connection.request(
52 request_method, str(url), **kwargs
53 )
54 except urllib3.exceptions.HTTPError:
55 logger.error("Connection to API endpoint %s failed", url)
56 raise
57 if response.status < 200 or response.status >= 300:
58 logger.error("API call to %s failed:\n%s", url, response.data)
59 raise urllib3.exceptions.HTTPError(response.status)
60 return response
61
43 def request(self, request_method, api_endpoint, data=None, files=None): 62 def request(self, request_method, api_endpoint, data=None, files=None):
44 url = "/api/project/%s/%s?key=%s&json=1" % (
45 self.project_name, api_endpoint, self.api_key
46 )
47
48 fields = [] 63 fields = []
49 if data: 64 if data:
50 for name, value in data.iteritems(): 65 for name, value in data.iteritems():
51 if isinstance(value, basestring): 66 if isinstance(value, basestring):
52 fields.append((name, value)) 67 fields.append((name, value))
53 else: 68 else:
54 fields += [(name + "[]", v) for v in value] 69 fields.extend((name + "[]", v) for v in value)
55 if files: 70 if files:
56 fields += [("files[%s]" % f[0], f) for f in files] 71 fields.extend(("files[%s]" % f[0], f) for f in files)
72
73 response = self.raw_request(
74 request_method, api_endpoint, (("json", "1"),),
75 fields=fields, preload_content=False
76 )
57 77
58 try: 78 try:
59 response = self.connection.request( 79 return json.load(response)
60 request_method, str(url), fields=fields,
61 timeout=urllib3.Timeout(connect=5)
62 )
63 except urllib3.exceptions.HTTPError as e:
64 logger.error("API call to %s failed:\n%s" % (url, response.data))
65 raise
66
67 if response.status < 200 or response.status >= 300:
68 logger.error("Connection to API failed for endpoint %s" % url)
69 raise urllib3.exceptions.HTTPError(response.status, response.data)
70
71 try:
72 return json.loads(response.data)
73 except ValueError: 80 except ValueError:
74 logger.error("Invalid response returned by API endpoint %s" % url) 81 logger.error("Invalid response returned by API endpoint %s", url)
75 raise 82 raise
76 83
77 84
78 def grouper(iterable, n): 85 def grouper(iterable, n):
79 iterator = iter(iterable) 86 iterator = iter(iterable)
80 while True: 87 while True:
81 chunk = tuple(itertools.islice(iterator, n)) 88 chunk = tuple(itertools.islice(iterator, n))
82 if not chunk: 89 if not chunk:
83 break 90 break
84 yield chunk 91 yield chunk
(...skipping 27 matching lines...) Expand all
112 119
113 def configure_locales(crowdin_api, required_locales, enabled_locales, 120 def configure_locales(crowdin_api, required_locales, enabled_locales,
114 defaultlocale): 121 defaultlocale):
115 logger.info("Checking which locales are supported by Crowdin...") 122 logger.info("Checking which locales are supported by Crowdin...")
116 response = crowdin_api.request("GET", "supported-languages") 123 response = crowdin_api.request("GET", "supported-languages")
117 124
118 supported_locales = {l["crowdin_code"] for l in response} 125 supported_locales = {l["crowdin_code"] for l in response}
119 skipped_locales = required_locales - supported_locales 126 skipped_locales = required_locales - supported_locales
120 127
121 if skipped_locales: 128 if skipped_locales:
122 logger.warning("Ignoring locales that Crowdin doesn't support: %s" % ( 129 logger.warning("Ignoring locales that Crowdin doesn't support: %s",
123 ", ".join(skipped_locales) 130 ", ".join(skipped_locales))
124 ))
125 required_locales -= skipped_locales 131 required_locales -= skipped_locales
126 132
127 if not required_locales.issubset(enabled_locales): 133 if not required_locales.issubset(enabled_locales):
128 logger.info("Enabling the required locales for the Crowdin project...") 134 logger.info("Enabling the required locales for the Crowdin project...")
129 crowdin_api.request( 135 crowdin_api.request(
130 "POST", "edit-project", 136 "POST", "edit-project",
131 data={"languages": list(enabled_locales | required_locales)} 137 data={"languages": enabled_locales | required_locales}
132 ) 138 )
133 139
134 return required_locales 140 return required_locales
135 141
136 def list_remote_files(project_info): 142 def list_remote_files(project_info):
137 def parse_file_node(node, path=""): 143 def parse_file_node(node, path=""):
138 if node["node_type"] == "file": 144 if node["node_type"] == "file":
139 remote_files.add(path + node["name"]) 145 remote_files.add(path + node["name"])
140 elif node["node_type"] == "directory": 146 elif node["node_type"] == "directory":
141 dir_name = path + node["name"] 147 dir_name = path + node["name"]
(...skipping 13 matching lines...) Expand all
155 for page, strings in page_strings.iteritems(): 161 for page, strings in page_strings.iteritems():
156 if strings: 162 if strings:
157 local_files.add(page + ".json") 163 local_files.add(page + ".json")
158 while "/" in page: 164 while "/" in page:
159 page = page.rsplit("/", 1)[0] 165 page = page.rsplit("/", 1)[0]
160 local_directories.add(page) 166 local_directories.add(page)
161 return local_files, local_directories 167 return local_files, local_directories
162 168
163 def create_directories(crowdin_api, directories): 169 def create_directories(crowdin_api, directories):
164 for directory in directories: 170 for directory in directories:
165 logger.info("Creating directory %s" % directory) 171 logger.info("Creating directory %s", directory)
166 crowdin_api.request("POST", "add-directory", data={"name": directory}) 172 crowdin_api.request("POST", "add-directory", data={"name": directory})
167 173
168 def add_update_files(crowdin_api, api_endpoint, message, files, page_strings): 174 def add_update_files(crowdin_api, api_endpoint, message, files, page_strings):
169 for group in grouper(files, crowdin_api.FILES_PER_REQUEST): 175 for group in grouper(files, crowdin_api.FILES_PER_REQUEST):
170 files = [] 176 files = []
171 for file_name in group: 177 for file_name in group:
172 page = os.path.splitext(file_name)[0] 178 page = os.path.splitext(file_name)[0]
173 files.append((file_name, json.dumps(page_strings[page]), "application/json ")) 179 files.append((file_name, json.dumps(page_strings[page]), "application/json "))
174 del page_strings[page] 180 del page_strings[page]
175 logger.info(message % len(files)) 181 logger.info(message, len(files))
176 crowdin_api.request("POST", api_endpoint, files=files) 182 crowdin_api.request("POST", api_endpoint, files=files)
177 183
178 def upload_new_files(crowdin_api, new_files, page_strings): 184 def upload_new_files(crowdin_api, new_files, page_strings):
179 add_update_files(crowdin_api, "add-file", "Uploading %d new pages...", 185 add_update_files(crowdin_api, "add-file", "Uploading %d new pages...",
180 new_files, page_strings) 186 new_files, page_strings)
181 187
182 def update_existing_files(crowdin_api, existing_files, page_strings): 188 def update_existing_files(crowdin_api, existing_files, page_strings):
183 add_update_files(crowdin_api, "update-file", "Updating %d existing pages...", 189 add_update_files(crowdin_api, "update-file", "Updating %d existing pages...",
184 existing_files, page_strings) 190 existing_files, page_strings)
185 191
186 def upload_translations(crowdin_api, source_dir, new_files, required_locales): 192 def upload_translations(crowdin_api, source_dir, new_files, required_locales):
187 def open_locale_files(locale, files): 193 def open_locale_files(locale, files):
188 for file_name in files: 194 for file_name in files:
189 path = os.path.join(source_dir, "locales", locale, file_name) 195 path = os.path.join(source_dir, "locales", locale, file_name)
190 if os.path.isfile(path): 196 if os.path.isfile(path):
191 with open(path, "r") as f: 197 with open(path, "rb") as f:
192 yield (file_name, f.read(), "application/json") 198 yield (file_name, f.read(), "application/json")
193 199
194 if new_files: 200 if new_files:
195 for locale in required_locales: 201 for locale in required_locales:
196 for files in grouper(open_locale_files(locale, new_files), 202 for files in grouper(open_locale_files(locale, new_files),
197 crowdin_api.FILES_PER_REQUEST): 203 crowdin_api.FILES_PER_REQUEST):
198 logger.info("Uploading %d existing translation " 204 logger.info("Uploading %d existing translation "
199 "files for locale %s..." % (len(files), locale)) 205 "files for locale %s...", len(files), locale)
200 crowdin_api.request("POST", "upload-translation", files=files, 206 crowdin_api.request("POST", "upload-translation", files=files,
201 data={"language": locale}) 207 data={"language": locale})
202 208
203 def remove_old_files(crowdin_api, old_files): 209 def remove_old_files(crowdin_api, old_files):
204 for file_name in old_files: 210 for file_name in old_files:
205 logger.info("Removing old file %s" % file_name) 211 logger.info("Removing old file %s", file_name)
206 crowdin_api.request("POST", "delete-file", data={"file": file_name}) 212 crowdin_api.request("POST", "delete-file", data={"file": file_name})
207 213
208 def remove_old_directories(crowdin_api, old_directories): 214 def remove_old_directories(crowdin_api, old_directories):
209 for directory in reversed(sorted(old_directories, key=len)): 215 for directory in reversed(sorted(old_directories, key=len)):
210 logger.info("Removing old directory %s" % directory) 216 logger.info("Removing old directory %s", directory)
211 crowdin_api.request("POST", "delete-directory", data={"name": directory}) 217 crowdin_api.request("POST", "delete-directory", data={"name": directory})
212 218
213 def download_translations(crowdin_api, source_dir, required_locales): 219 def download_translations(crowdin_api, source_dir, required_locales):
214 logger.info("Requesting generation of fresh translations archive...") 220 logger.info("Requesting generation of fresh translations archive...")
215 result = crowdin_api.request("GET", "export") 221 result = crowdin_api.request("GET", "export")
216 if result.get("success", {}).get("status") == "skipped": 222 if result.get("success", {}).get("status") == "skipped":
217 logger.warning("Archive generation skipped, either " 223 logger.warning("Archive generation skipped, either "
218 "no changes or API usage excessive") 224 "no changes or API usage excessive")
219 225
220 logger.info("Downloading translations archive...") 226 logger.info("Downloading translations archive...")
221 response = crowdin_api.connection.request( 227 response = crowdin_api.raw_request("GET", "download/all.zip")
222 "GET",
223 "/api/project/%s/download/all.zip?key=%s" % (
224 crowdin_api.project_name, crowdin_api.api_key
225 ), preload_content = False
226 )
227 if response.status < 200 or response.status >= 300:
228 raise urllib3.exceptions.HTTPError(response.status, response.data)
229 228
230 logger.info("Extracting translations archive...") 229 logger.info("Extracting translations archive...")
231 with zipfile.ZipFile(io.BytesIO(response.data), "r") as archive: 230 with zipfile.ZipFile(io.BytesIO(response.data), "r") as archive:
232 locale_path = os.path.join(source_dir, "locales") 231 locale_path = os.path.join(source_dir, "locales")
233 # First clear existing translation files 232 # First clear existing translation files
234 for root, dirs, files in os.walk(locale_path, topdown=True): 233 for root, dirs, files in os.walk(locale_path, topdown=True):
235 if root == locale_path: 234 if root == locale_path:
236 dirs[:] = [d for d in dirs if d in required_locales] 235 dirs[:] = [d for d in dirs if d in required_locales]
237 for f in files: 236 for f in files:
238 if f.endswith(".json"): 237 if f.lower().endswith(".json"):
239 os.remove(os.path.join(root, f)) 238 os.remove(os.path.join(root, f))
240 # Then extract the new ones in place 239 # Then extract the new ones in place
241 for member in archive.namelist(): 240 for member in archive.namelist():
242 path, file_name = posixpath.split(member) 241 path, file_name = posixpath.split(member)
243 ext = posixpath.splitext(file_name)[1] 242 ext = posixpath.splitext(file_name)[1]
244 locale = path.split(posixpath.sep)[0] 243 locale = path.split(posixpath.sep)[0]
245 if ext == ".json" and locale in required_locales: 244 if ext.lower() == ".json" and locale in required_locales:
246 archive.extract(member, locale_path) 245 archive.extract(member, locale_path)
247 246
248 def crowdin_sync(source_dir, crowdin_api_key): 247 def crowdin_sync(source_dir, crowdin_api_key):
249 with FileSource(source_dir) as source: 248 with FileSource(source_dir) as source:
250 config = source.read_config() 249 config = source.read_config()
251 defaultlocale = config.get("general", "defaultlocale") 250 defaultlocale = config.get("general", "defaultlocale")
252 crowdin_project_name = config.get("general", "crowdin-project-name") 251 crowdin_project_name = config.get("general", "crowdin-project-name")
253 252
254 crowdin_api = CrowdinAPI(crowdin_api_key, crowdin_project_name) 253 crowdin_api = CrowdinAPI(crowdin_api_key, crowdin_project_name)
255 254
(...skipping 36 matching lines...) Expand 10 before | Expand all | Expand 10 after
292 if __name__ == "__main__": 291 if __name__ == "__main__":
293 if len(sys.argv) < 3: 292 if len(sys.argv) < 3:
294 print >>sys.stderr, "Usage: python -m cms.bin.translate www_directory crowdi n_project_api_key [logging_level]" 293 print >>sys.stderr, "Usage: python -m cms.bin.translate www_directory crowdi n_project_api_key [logging_level]"
295 sys.exit(1) 294 sys.exit(1)
296 295
297 logging.basicConfig() 296 logging.basicConfig()
298 logger.setLevel(sys.argv[3] if len(sys.argv) > 3 else logging.INFO) 297 logger.setLevel(sys.argv[3] if len(sys.argv) > 3 else logging.INFO)
299 298
300 source_dir, crowdin_api_key = sys.argv[1:3] 299 source_dir, crowdin_api_key = sys.argv[1:3]
301 crowdin_sync(source_dir, crowdin_api_key) 300 crowdin_sync(source_dir, crowdin_api_key)
LEFTRIGHT

Powered by Google App Engine
This is Rietveld