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

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

Issue 29328085: Issue 3076 - [CMS] Map locale names to match Crowdin's expectations (Closed)
Patch Set: Addressed nits Created Sept. 17, 2015, 2:43 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 # 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 shutil
25 import sys 26 import sys
26 import urllib 27 import urllib
27 import zipfile 28 import zipfile
28 29
29 import urllib3 30 import urllib3
30 31
31 import cms.utils 32 import cms.utils
32 from cms.sources import FileSource 33 from cms.sources import FileSource
33 34
34 logger = logging.getLogger("cms.bin.translate") 35 logger = logging.getLogger("cms.bin.translate")
(...skipping 75 matching lines...) Expand 10 before | Expand all | Expand 10 after
110 comment += ", ".join("{%d}: %s" % i_s 111 comment += ", ".join("{%d}: %s" % i_s
111 for i_s in enumerate(fixed_strings, 1)) 112 for i_s in enumerate(fixed_strings, 1))
112 if comment: 113 if comment:
113 store[name]["description"] = comment 114 store[name]["description"] = comment
114 115
115 for page, format in source.list_pages(): 116 for page, format in source.list_pages():
116 cms.utils.process_page(source, defaultlocale, page, 117 cms.utils.process_page(source, defaultlocale, page,
117 format=format, localized_string_callback=record_strin g) 118 format=format, localized_string_callback=record_strin g)
118 return page_strings 119 return page_strings
119 120
120 def configure_locales(crowdin_api, required_locales, enabled_locales, 121 def configure_locales(crowdin_api, local_locales, enabled_locales,
121 defaultlocale): 122 defaultlocale):
122 logger.info("Checking which locales are supported by Crowdin...") 123 logger.info("Checking which locales are supported by Crowdin...")
123 response = crowdin_api.request("GET", "supported-languages") 124 response = crowdin_api.request("GET", "supported-languages")
124 125
125 supported_locales = {l["crowdin_code"] for l in response} 126 supported_locales = {l["crowdin_code"] for l in response}
126 skipped_locales = required_locales - supported_locales
127 127
128 if skipped_locales: 128 # We need to map the locale names we use to the ones that Crowdin is expecting
129 logger.warning("Ignoring locales that Crowdin doesn't support: %s", 129 # and at the same time ensure that they are supported.
130 ", ".join(skipped_locales)) 130 required_locales = {}
131 required_locales -= skipped_locales 131 for locale in local_locales:
132 if "_" in locale:
133 crowdin_locale = locale.replace("_", "-")
134 elif locale in supported_locales:
135 crowdin_locale = locale
136 else:
137 crowdin_locale = "%s-%s" % (locale, locale.upper())
132 138
133 if not required_locales.issubset(enabled_locales): 139 if crowdin_locale in supported_locales:
140 required_locales[locale] = crowdin_locale
141 else:
142 logger.warning("Ignoring locale '%s', which Crowdin doesn't support",
143 locale)
144
145 required_crowdin_locales = set(required_locales.values())
146 if not required_crowdin_locales.issubset(enabled_locales):
134 logger.info("Enabling the required locales for the Crowdin project...") 147 logger.info("Enabling the required locales for the Crowdin project...")
135 crowdin_api.request( 148 crowdin_api.request(
136 "POST", "edit-project", 149 "POST", "edit-project",
137 data={"languages": enabled_locales | required_locales} 150 data={"languages": enabled_locales | required_crowdin_locales}
138 ) 151 )
139 152
140 return required_locales 153 return required_locales
141 154
142 def list_remote_files(project_info): 155 def list_remote_files(project_info):
143 def parse_file_node(node, path=""): 156 def parse_file_node(node, path=""):
144 if node["node_type"] == "file": 157 if node["node_type"] == "file":
145 remote_files.add(path + node["name"]) 158 remote_files.add(path + node["name"])
146 elif node["node_type"] == "directory": 159 elif node["node_type"] == "directory":
147 dir_name = path + node["name"] 160 dir_name = path + node["name"]
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after
191 204
192 def upload_translations(crowdin_api, source_dir, new_files, required_locales): 205 def upload_translations(crowdin_api, source_dir, new_files, required_locales):
193 def open_locale_files(locale, files): 206 def open_locale_files(locale, files):
194 for file_name in files: 207 for file_name in files:
195 path = os.path.join(source_dir, "locales", locale, file_name) 208 path = os.path.join(source_dir, "locales", locale, file_name)
196 if os.path.isfile(path): 209 if os.path.isfile(path):
197 with open(path, "rb") as f: 210 with open(path, "rb") as f:
198 yield (file_name, f.read(), "application/json") 211 yield (file_name, f.read(), "application/json")
199 212
200 if new_files: 213 if new_files:
201 for locale in required_locales: 214 for locale, crowdin_locale in required_locales.iteritems():
202 for files in grouper(open_locale_files(locale, new_files), 215 for files in grouper(open_locale_files(locale, new_files),
203 crowdin_api.FILES_PER_REQUEST): 216 crowdin_api.FILES_PER_REQUEST):
204 logger.info("Uploading %d existing translation " 217 logger.info("Uploading %d existing translation "
205 "files for locale %s...", len(files), locale) 218 "files for locale %s...", len(files), locale)
206 crowdin_api.request("POST", "upload-translation", files=files, 219 crowdin_api.request("POST", "upload-translation", files=files,
207 data={"language": locale}) 220 data={"language": crowdin_locale})
208 221
209 def remove_old_files(crowdin_api, old_files): 222 def remove_old_files(crowdin_api, old_files):
210 for file_name in old_files: 223 for file_name in old_files:
211 logger.info("Removing old file %s", file_name) 224 logger.info("Removing old file %s", file_name)
212 crowdin_api.request("POST", "delete-file", data={"file": file_name}) 225 crowdin_api.request("POST", "delete-file", data={"file": file_name})
213 226
214 def remove_old_directories(crowdin_api, old_directories): 227 def remove_old_directories(crowdin_api, old_directories):
215 for directory in reversed(sorted(old_directories, key=len)): 228 for directory in reversed(sorted(old_directories, key=len)):
216 logger.info("Removing old directory %s", directory) 229 logger.info("Removing old directory %s", directory)
217 crowdin_api.request("POST", "delete-directory", data={"name": directory}) 230 crowdin_api.request("POST", "delete-directory", data={"name": directory})
218 231
219 def download_translations(crowdin_api, source_dir, required_locales): 232 def download_translations(crowdin_api, source_dir, required_locales):
220 logger.info("Requesting generation of fresh translations archive...") 233 logger.info("Requesting generation of fresh translations archive...")
221 result = crowdin_api.request("GET", "export") 234 result = crowdin_api.request("GET", "export")
222 if result.get("success", {}).get("status") == "skipped": 235 if result.get("success", {}).get("status") == "skipped":
223 logger.warning("Archive generation skipped, either " 236 logger.warning("Archive generation skipped, either "
224 "no changes or API usage excessive") 237 "no changes or API usage excessive")
225 238
226 logger.info("Downloading translations archive...") 239 logger.info("Downloading translations archive...")
227 response = crowdin_api.raw_request("GET", "download/all.zip") 240 response = crowdin_api.raw_request("GET", "download/all.zip")
228 241
242 inverted_required_locales = {crowdin: local for local, crowdin in
243 required_locales.iteritems()}
229 logger.info("Extracting translations archive...") 244 logger.info("Extracting translations archive...")
230 with zipfile.ZipFile(io.BytesIO(response.data), "r") as archive: 245 with zipfile.ZipFile(io.BytesIO(response.data), "r") as archive:
231 locale_path = os.path.join(source_dir, "locales") 246 locale_path = os.path.join(source_dir, "locales")
232 # First clear existing translation files 247 # First clear existing translation files
233 for root, dirs, files in os.walk(locale_path, topdown=True): 248 for root, dirs, files in os.walk(locale_path, topdown=True):
234 if root == locale_path: 249 if root == locale_path:
235 dirs[:] = [d for d in dirs if d in required_locales] 250 dirs[:] = [d for d in dirs if d in required_locales]
236 for f in files: 251 for f in files:
237 if f.lower().endswith(".json"): 252 if f.lower().endswith(".json"):
238 os.remove(os.path.join(root, f)) 253 os.remove(os.path.join(root, f))
239 # Then extract the new ones in place 254 # Then extract the new ones in place
240 for member in archive.namelist(): 255 for member in archive.namelist():
241 path, file_name = posixpath.split(member) 256 path, file_name = posixpath.split(member)
242 ext = posixpath.splitext(file_name)[1] 257 ext = posixpath.splitext(file_name)[1]
243 locale = path.split(posixpath.sep)[0] 258 path_parts = path.split(posixpath.sep)
244 if ext.lower() == ".json" and locale in required_locales: 259 locale, file_path = path_parts[0], path_parts[1:]
245 archive.extract(member, locale_path) 260 if ext.lower() == ".json" and locale in inverted_required_locales:
261 output_path = os.path.join(
262 locale_path, inverted_required_locales[locale],
263 *file_path + [file_name]
264 )
265 with archive.open(member) as source_file, \
266 open(output_path, "wb") as target_file:
267 shutil.copyfileobj(source_file, target_file)
246 268
247 def crowdin_sync(source_dir, crowdin_api_key): 269 def crowdin_sync(source_dir, crowdin_api_key):
248 with FileSource(source_dir) as source: 270 with FileSource(source_dir) as source:
249 config = source.read_config() 271 config = source.read_config()
250 defaultlocale = config.get("general", "defaultlocale") 272 defaultlocale = config.get("general", "defaultlocale")
251 crowdin_project_name = config.get("general", "crowdin-project-name") 273 crowdin_project_name = config.get("general", "crowdin-project-name")
252 274
253 crowdin_api = CrowdinAPI(crowdin_api_key, crowdin_project_name) 275 crowdin_api = CrowdinAPI(crowdin_api_key, crowdin_project_name)
254 276
255 logger.info("Requesting project information...") 277 logger.info("Requesting project information...")
256 project_info = crowdin_api.request("GET", "info") 278 project_info = crowdin_api.request("GET", "info")
257 page_strings = extract_strings(source, defaultlocale) 279 page_strings = extract_strings(source, defaultlocale)
258 280
259 required_locales = {l for l in source.list_locales() if l != defaultlocale} 281 local_locales = source.list_locales() - {defaultlocale}
260 enabled_locales = {l["code"] for l in project_info["languages"]} 282 enabled_locales = {l["code"] for l in project_info["languages"]}
261 283
262 required_locales = configure_locales(crowdin_api, required_locales, 284 required_locales = configure_locales(crowdin_api, local_locales,
263 enabled_locales, defaultlocale) 285 enabled_locales, defaultlocale)
264 286
265 remote_files, remote_directories = list_remote_files(project_info) 287 remote_files, remote_directories = list_remote_files(project_info)
266 local_files, local_directories = list_local_files(page_strings) 288 local_files, local_directories = list_local_files(page_strings)
267 289
268 # Avoid deleting all remote content if there was a problem listing local files 290 # Avoid deleting all remote content if there was a problem listing local files
269 if not local_files: 291 if not local_files:
270 logger.error("No existing strings found, maybe the project directory is " 292 logger.error("No existing strings found, maybe the project directory is "
271 "not set up correctly? Aborting!") 293 "not set up correctly? Aborting!")
272 sys.exit(1) 294 sys.exit(1)
(...skipping 18 matching lines...) Expand all
291 if __name__ == "__main__": 313 if __name__ == "__main__":
292 if len(sys.argv) < 3: 314 if len(sys.argv) < 3:
293 print >>sys.stderr, "Usage: python -m cms.bin.translate www_directory crowdi n_project_api_key [logging_level]" 315 print >>sys.stderr, "Usage: python -m cms.bin.translate www_directory crowdi n_project_api_key [logging_level]"
294 sys.exit(1) 316 sys.exit(1)
295 317
296 logging.basicConfig() 318 logging.basicConfig()
297 logger.setLevel(sys.argv[3] if len(sys.argv) > 3 else logging.INFO) 319 logger.setLevel(sys.argv[3] if len(sys.argv) > 3 else logging.INFO)
298 320
299 source_dir, crowdin_api_key = sys.argv[1:3] 321 source_dir, crowdin_api_key = sys.argv[1:3]
300 crowdin_sync(source_dir, crowdin_api_key) 322 crowdin_sync(source_dir, crowdin_api_key)
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