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: Simplify locale name conversion logic Created Sept. 17, 2015, 10:15 a.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
128 # We need to map the locale names we use to the ones that Crowdin is expecting
129 # and at the same time ensure that they are supported.
130 required_locales = {}
131 skipped_locales = []
132 for locale in local_locales:
133 if "_" in locale:
134 crowdin_locale = locale.replace("_", "-")
135 elif locale in supported_locales:
136 crowdin_locale = locale
137 else:
138 crowdin_locale = "%s-%s" % (locale, locale.upper())
139
140 if crowdin_locale in supported_locales:
141 required_locales[locale] = crowdin_locale
142 else:
143 skipped_locales.append(locale)
Sebastian Noack 2015/09/17 11:03:24 This code past hasn't been tested, obviously. ;)
Sebastian Noack 2015/09/17 12:42:15 Never mind. I just realized you turned it into a l
kzar 2015/09/17 14:43:55 Done.
127 144
128 if skipped_locales: 145 if skipped_locales:
129 logger.warning("Ignoring locales that Crowdin doesn't support: %s", 146 logger.warning("Ignoring locales that Crowdin doesn't support: %s",
130 ", ".join(skipped_locales)) 147 ", ".join(skipped_locales))
131 required_locales -= skipped_locales
132 148
133 if not required_locales.issubset(enabled_locales): 149 required_crowdin_locales = set(required_locales.values())
150 if not required_crowdin_locales.issubset(enabled_locales):
134 logger.info("Enabling the required locales for the Crowdin project...") 151 logger.info("Enabling the required locales for the Crowdin project...")
135 crowdin_api.request( 152 crowdin_api.request(
136 "POST", "edit-project", 153 "POST", "edit-project",
137 data={"languages": enabled_locales | required_locales} 154 data={"languages": enabled_locales | required_crowdin_locales}
138 ) 155 )
139 156
140 return required_locales 157 return required_locales
141 158
142 def list_remote_files(project_info): 159 def list_remote_files(project_info):
143 def parse_file_node(node, path=""): 160 def parse_file_node(node, path=""):
144 if node["node_type"] == "file": 161 if node["node_type"] == "file":
145 remote_files.add(path + node["name"]) 162 remote_files.add(path + node["name"])
146 elif node["node_type"] == "directory": 163 elif node["node_type"] == "directory":
147 dir_name = path + node["name"] 164 dir_name = path + node["name"]
(...skipping 43 matching lines...) Expand 10 before | Expand all | Expand 10 after
191 208
192 def upload_translations(crowdin_api, source_dir, new_files, required_locales): 209 def upload_translations(crowdin_api, source_dir, new_files, required_locales):
193 def open_locale_files(locale, files): 210 def open_locale_files(locale, files):
194 for file_name in files: 211 for file_name in files:
195 path = os.path.join(source_dir, "locales", locale, file_name) 212 path = os.path.join(source_dir, "locales", locale, file_name)
196 if os.path.isfile(path): 213 if os.path.isfile(path):
197 with open(path, "rb") as f: 214 with open(path, "rb") as f:
198 yield (file_name, f.read(), "application/json") 215 yield (file_name, f.read(), "application/json")
199 216
200 if new_files: 217 if new_files:
201 for locale in required_locales: 218 for locale, crowdin_locale in required_locales.iteritems():
202 for files in grouper(open_locale_files(locale, new_files), 219 for files in grouper(open_locale_files(locale, new_files),
203 crowdin_api.FILES_PER_REQUEST): 220 crowdin_api.FILES_PER_REQUEST):
204 logger.info("Uploading %d existing translation " 221 logger.info("Uploading %d existing translation "
205 "files for locale %s...", len(files), locale) 222 "files for locale %s...", len(files), locale)
206 crowdin_api.request("POST", "upload-translation", files=files, 223 crowdin_api.request("POST", "upload-translation", files=files,
207 data={"language": locale}) 224 data={"language": crowdin_locale})
208 225
209 def remove_old_files(crowdin_api, old_files): 226 def remove_old_files(crowdin_api, old_files):
210 for file_name in old_files: 227 for file_name in old_files:
211 logger.info("Removing old file %s", file_name) 228 logger.info("Removing old file %s", file_name)
212 crowdin_api.request("POST", "delete-file", data={"file": file_name}) 229 crowdin_api.request("POST", "delete-file", data={"file": file_name})
213 230
214 def remove_old_directories(crowdin_api, old_directories): 231 def remove_old_directories(crowdin_api, old_directories):
215 for directory in reversed(sorted(old_directories, key=len)): 232 for directory in reversed(sorted(old_directories, key=len)):
216 logger.info("Removing old directory %s", directory) 233 logger.info("Removing old directory %s", directory)
217 crowdin_api.request("POST", "delete-directory", data={"name": directory}) 234 crowdin_api.request("POST", "delete-directory", data={"name": directory})
218 235
219 def download_translations(crowdin_api, source_dir, required_locales): 236 def download_translations(crowdin_api, source_dir, required_locales):
220 logger.info("Requesting generation of fresh translations archive...") 237 logger.info("Requesting generation of fresh translations archive...")
221 result = crowdin_api.request("GET", "export") 238 result = crowdin_api.request("GET", "export")
222 if result.get("success", {}).get("status") == "skipped": 239 if result.get("success", {}).get("status") == "skipped":
223 logger.warning("Archive generation skipped, either " 240 logger.warning("Archive generation skipped, either "
224 "no changes or API usage excessive") 241 "no changes or API usage excessive")
225 242
226 logger.info("Downloading translations archive...") 243 logger.info("Downloading translations archive...")
227 response = crowdin_api.raw_request("GET", "download/all.zip") 244 response = crowdin_api.raw_request("GET", "download/all.zip")
228 245
246 inverted_required_locales = {crowdin: local for local, crowdin in
247 required_locales.iteritems()}
229 logger.info("Extracting translations archive...") 248 logger.info("Extracting translations archive...")
230 with zipfile.ZipFile(io.BytesIO(response.data), "r") as archive: 249 with zipfile.ZipFile(io.BytesIO(response.data), "r") as archive:
231 locale_path = os.path.join(source_dir, "locales") 250 locale_path = os.path.join(source_dir, "locales")
232 # First clear existing translation files 251 # First clear existing translation files
233 for root, dirs, files in os.walk(locale_path, topdown=True): 252 for root, dirs, files in os.walk(locale_path, topdown=True):
234 if root == locale_path: 253 if root == locale_path:
235 dirs[:] = [d for d in dirs if d in required_locales] 254 dirs[:] = [d for d in dirs if d in required_locales]
236 for f in files: 255 for f in files:
237 if f.lower().endswith(".json"): 256 if f.lower().endswith(".json"):
238 os.remove(os.path.join(root, f)) 257 os.remove(os.path.join(root, f))
239 # Then extract the new ones in place 258 # Then extract the new ones in place
240 for member in archive.namelist(): 259 for member in archive.namelist():
241 path, file_name = posixpath.split(member) 260 path, file_name = posixpath.split(member)
242 ext = posixpath.splitext(file_name)[1] 261 ext = posixpath.splitext(file_name)[1]
243 locale = path.split(posixpath.sep)[0] 262 path_parts = path.split(posixpath.sep)
244 if ext.lower() == ".json" and locale in required_locales: 263 locale, file_path = path_parts[0], path_parts[1:]
245 archive.extract(member, locale_path) 264 if ext.lower() == ".json" and locale in inverted_required_locales:
265 output_path = os.path.join(
266 locale_path, inverted_required_locales[locale],
267 *file_path + [file_name]
268 )
269 with archive.open(member) as f, open(output_path, "wb") as f_target:
Sebastian Noack 2015/09/17 11:03:24 Nit: Please use consistent and more descriptive va
kzar 2015/09/17 14:43:55 Done.
270 shutil.copyfileobj(f, f_target)
246 271
247 def crowdin_sync(source_dir, crowdin_api_key): 272 def crowdin_sync(source_dir, crowdin_api_key):
248 with FileSource(source_dir) as source: 273 with FileSource(source_dir) as source:
249 config = source.read_config() 274 config = source.read_config()
250 defaultlocale = config.get("general", "defaultlocale") 275 defaultlocale = config.get("general", "defaultlocale")
251 crowdin_project_name = config.get("general", "crowdin-project-name") 276 crowdin_project_name = config.get("general", "crowdin-project-name")
252 277
253 crowdin_api = CrowdinAPI(crowdin_api_key, crowdin_project_name) 278 crowdin_api = CrowdinAPI(crowdin_api_key, crowdin_project_name)
254 279
255 logger.info("Requesting project information...") 280 logger.info("Requesting project information...")
256 project_info = crowdin_api.request("GET", "info") 281 project_info = crowdin_api.request("GET", "info")
257 page_strings = extract_strings(source, defaultlocale) 282 page_strings = extract_strings(source, defaultlocale)
258 283
259 required_locales = {l for l in source.list_locales() if l != defaultlocale} 284 local_locales = source.list_locales() - {defaultlocale}
260 enabled_locales = {l["code"] for l in project_info["languages"]} 285 enabled_locales = {l["code"] for l in project_info["languages"]}
261 286
262 required_locales = configure_locales(crowdin_api, required_locales, 287 required_locales = configure_locales(crowdin_api, local_locales,
263 enabled_locales, defaultlocale) 288 enabled_locales, defaultlocale)
264 289
265 remote_files, remote_directories = list_remote_files(project_info) 290 remote_files, remote_directories = list_remote_files(project_info)
266 local_files, local_directories = list_local_files(page_strings) 291 local_files, local_directories = list_local_files(page_strings)
267 292
268 # Avoid deleting all remote content if there was a problem listing local files 293 # Avoid deleting all remote content if there was a problem listing local files
269 if not local_files: 294 if not local_files:
270 logger.error("No existing strings found, maybe the project directory is " 295 logger.error("No existing strings found, maybe the project directory is "
271 "not set up correctly? Aborting!") 296 "not set up correctly? Aborting!")
272 sys.exit(1) 297 sys.exit(1)
(...skipping 18 matching lines...) Expand all
291 if __name__ == "__main__": 316 if __name__ == "__main__":
292 if len(sys.argv) < 3: 317 if len(sys.argv) < 3:
293 print >>sys.stderr, "Usage: python -m cms.bin.translate www_directory crowdi n_project_api_key [logging_level]" 318 print >>sys.stderr, "Usage: python -m cms.bin.translate www_directory crowdi n_project_api_key [logging_level]"
294 sys.exit(1) 319 sys.exit(1)
295 320
296 logging.basicConfig() 321 logging.basicConfig()
297 logger.setLevel(sys.argv[3] if len(sys.argv) > 3 else logging.INFO) 322 logger.setLevel(sys.argv[3] if len(sys.argv) > 3 else logging.INFO)
298 323
299 source_dir, crowdin_api_key = sys.argv[1:3] 324 source_dir, crowdin_api_key = sys.argv[1:3]
300 crowdin_sync(source_dir, crowdin_api_key) 325 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