blob: e4afff0250e5b2b9c488cfeee43e59f15e4cd55b [file] [log] [blame]
Marco Brohetcb5cdb42014-07-11 22:41:53 +02001#!/usr/bin/python2
2# -*- coding: utf-8 -*-
3# cm_crowdin_sync_all.py
4#
5# Updates Crowdin source translations and pushes translations
6# directly to CyanogenMod's Gerrit.
7#
8# Copyright (C) 2014 The CyanogenMod Project
9#
10# Licensed under the Apache License, Version 2.0 (the "License");
11# you may not use this file except in compliance with the License.
12# You may obtain a copy of the License at
13#
14# http://www.apache.org/licenses/LICENSE-2.0
15#
16# Unless required by applicable law or agreed to in writing, software
17# distributed under the License is distributed on an "AS IS" BASIS,
18# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
19# See the License for the specific language governing permissions and
20# limitations under the License.
21
22import codecs
23import git
24import os
25import os.path
26import re
27import shutil
28import subprocess
29import sys
30from urllib import urlretrieve
31from xml.dom import minidom
32
33def get_caf_additions(strings_base, strings_cm):
34 # Load AOSP file and resources
35 xml_base = minidom.parse(strings_base)
36 list_base_string = xml_base.getElementsByTagName('string')
37 list_base_string_array = xml_base.getElementsByTagName('string-array')
38 list_base_plurals = xml_base.getElementsByTagName('plurals')
39 # Load CM file and resources
40 xml_cm = minidom.parse(strings_cm)
41 list_cm_string = xml_cm.getElementsByTagName('string')
42 list_cm_string_array = xml_cm.getElementsByTagName('string-array')
43 list_cm_plurals = xml_cm.getElementsByTagName('plurals')
44
45 # All names from CM
46 names_cm_string = []
47 names_cm_string_array = []
48 names_cm_plurals = []
49 # All names from AOSP
50 names_base_string = []
51 names_base_string_array = []
52 names_base_plurals = []
53
54 # Get all names from CM
55 for s in list_cm_string :
56 if not s.hasAttribute('translatable') and not s.hasAttribute('translate'):
57 names_cm_string.append(s.attributes['name'].value)
58 for s in list_cm_string_array :
59 if not s.hasAttribute('translatable') and not s.hasAttribute('translate'):
60 names_cm_string_array.append(s.attributes['name'].value)
61 for s in list_cm_plurals :
62 if not s.hasAttribute('translatable') and not s.hasAttribute('translate'):
63 names_cm_plurals.append(s.attributes['name'].value)
64 # Get all names from AOSP
65 for s in list_base_string :
66 if not s.hasAttribute('translatable') and not s.hasAttribute('translate'):
67 names_base_string.append(s.attributes['name'].value)
68 for s in list_base_string_array :
69 if not s.hasAttribute('translatable') and not s.hasAttribute('translate'):
70 names_base_string_array.append(s.attributes['name'].value)
71 for s in list_base_plurals :
72 if not s.hasAttribute('translatable') and not s.hasAttribute('translate'):
73 names_base_plurals.append(s.attributes['name'].value)
74
75 # Store all differences in this list
76 caf_additions = []
77
78 # Store all found strings/arrays/plurals.
79 # Prevent duplicates with product attribute
80 found_string = []
81 found_string_array = []
82 found_plurals = []
83
84 # Add all CAF additions to the list 'caf_additions'
85 for z in names_cm_string:
86 if z not in names_base_string and z not in found_string:
87 for string_item in list_cm_string:
88 if string_item.attributes['name'].value == z:
89 caf_additions.append(' ' + string_item.toxml())
90 found_string.append(z)
91 for y in names_cm_string_array:
92 if y not in names_base_string_array and y not in found_string_array:
93 for string_array_item in list_cm_string_array:
94 if string_array_item.attributes['name'].value == y:
95 caf_additions.append(' ' + string_array_item.toxml())
96 found_string_array.append(y)
97 for x in names_cm_plurals:
98 if x not in names_base_plurals and x not in found_plurals:
99 for plurals_item in list_cm_plurals:
100 if plurals_item.attributes['name'].value == x:
101 caf_additions.append(' ' + plurals_item.toxml())
102 found_plurals.append(x)
103
104 # Done :-)
105 return caf_additions
106
107def purge_caf_additions(strings_base, strings_cm):
108 # Load AOSP file and resources
109 xml_base = minidom.parse(strings_base)
110 list_base_string = xml_base.getElementsByTagName('string')
111 list_base_string_array = xml_base.getElementsByTagName('string-array')
112 list_base_plurals = xml_base.getElementsByTagName('plurals')
113 # Load CM file and resources
114 xml_cm = minidom.parse(strings_cm)
115 list_cm_string = xml_cm.getElementsByTagName('string')
116 list_cm_string_array = xml_cm.getElementsByTagName('string-array')
117 list_cm_plurals = xml_cm.getElementsByTagName('plurals')
118 with codecs.open(strings_cm, 'r', 'utf-8') as f:
119 content = [line.rstrip() for line in f]
120 shutil.copyfile(strings_cm, strings_cm + '.backup')
121 file_this = codecs.open(strings_cm, 'w', 'utf-8')
122
123 # All names from AOSP
124 names_base_string = []
125 names_base_string_array = []
126 names_base_plurals = []
127
128 # Get all names from AOSP
129 for s in list_base_string :
130 names_base_string.append(s.attributes['name'].value)
131 for s in list_base_string_array :
132 names_base_string_array.append(s.attributes['name'].value)
133 for s in list_base_plurals :
134 names_base_plurals.append(s.attributes['name'].value)
135
136 # Get all names from CM
137 content2 = []
138 for s in list_cm_string :
139 name = s.attributes['name'].value
140 if name not in names_base_string:
141 true = 0
142 content2 = []
143 for i in content:
144 if true == 0:
145 test = re.search('(<string name=\"' + name + ')', i)
146 if test is not None:
147 test2 = re.search('(</string>)', i)
148 if test2:
149 true = 2
150 else:
151 true = 1
152 i = ''
153 elif true == 1:
154 test2 = re.search('(</string>)', i)
155 if test2 is not None:
156 true = 2
157 i = ''
158 elif true == 2:
159 true = 3
160 content2.append(i)
161 content = content2
162 for s in list_cm_string_array :
163 name = s.attributes['name'].value
164 if name not in names_base_string_array:
165 true = 0
166 content2 = []
167 for i in content:
168 if true == 0:
169 test = re.search('(<string-array name=\"' + name + ')', i)
170 if test is not None:
171 test2 = re.search('(</string-array>)', i)
172 if test2:
173 true = 2
174 else:
175 true = 1
176 i = ''
177 elif true == 1:
178 test2 = re.search('(</string-array>)', i)
179 if test2 is not None:
180 true = 2
181 i = ''
182 elif true == 2:
183 true = 3
184 content2.append(i)
185 content = content2
186 for s in list_cm_plurals :
187 name = s.attributes['name'].value
188 if name not in names_base_plurals:
189 true = 0
190 content2 = []
191 for i in content:
192 if true == 0:
193 test = re.search('(<plurals name=\"' + name + ')', i)
194 if test is not None:
195 test2 = re.search('(</plurals>)', i)
196 if test2:
197 true = 2
198 else:
199 true = 1
200 i = ''
201 elif true == 1:
202 test2 = re.search('(</plurals>)', i)
203 if test2 is not None:
204 true = 2
205 i = ''
206 elif true == 2:
207 # The actual purging is done!
208 true = 3
209 content2.append(i)
210 content = content2
211
212 for addition in content:
213 file_this.write(addition + '\n')
214 file_this.close()
215
216def push_as_commit(path, name, branch):
217 # CM gerrit nickname
218 username = 'your_nickname'
219
220 # Get path
221 path = os.getcwd() + '/' + path
222
223 # Create git commit
224 repo = git.Repo(path)
225 removed_files = repo.git.ls_files(d=True).split('\n')
226 try:
227 repo.git.rm(removed_files)
228 except:
229 pass
230 repo.git.add('-A')
231 try:
232 repo.git.commit(m='Automatic translation import')
233 except:
234 print('Failed to create commit for ' + name + ', probably empty: skipping')
235 return
236 repo.git.push('ssh://' + username + '@review.cyanogenmod.org:29418/' + name, 'HEAD:refs/for/' + branch)
237 print('Succesfully pushed commit for ' + name)
238
239print('Welcome to the CM Crowdin sync script!')
240
241print('\nSTEP 0: Checking dependencies')
242# Check for Ruby version of crowdin-cli
243if subprocess.check_output(['rvm', 'all', 'do', 'gem', 'list', 'crowdin-cli', '-i']) == 'true':
244 sys.exit('You have not installed crowdin-cli. Terminating.')
245else:
246 print('Found: crowdin-cli')
247
248# Check for repo
249try:
250 subprocess.check_output(['which', 'repo'])
251except:
252 sys.exit('You have not installed repo. Terminating.')
253
254# Check for android/default.xml
255if not os.path.isfile('android/default.xml'):
256 sys.exit('You have no android/default.xml. Terminating.')
257else:
258 print('Found: android/default.xml')
259
260# Check for crowdin/caf.xml
261if not os.path.isfile('crowdin/caf.xml'):
262 sys.exit('You have no crowdin/caf.xml. Terminating.')
263else:
264 print('Found: crowdin/caf.xml')
265
266# Check for crowdin/crowdin_aosp.yaml
267if not os.path.isfile('crowdin/crowdin_aosp.yaml'):
268 sys.exit('You have no crowdin/crowdin_aosp.yaml. Terminating.')
269else:
270 print('Found: crowdin/crowdin_aosp.yaml')
271
272# Check for crowdin/crowdin_cm.yaml
273if not os.path.isfile('crowdin/crowdin_cm.yaml'):
274 sys.exit('You have no crowdin/crowdin_cm.yaml. Terminating.')
275else:
276 print('Found: crowdin/crowdin_cm.yaml')
277
278# Check for crowdin/extra_packages.xml
279if not os.path.isfile('crowdin/extra_packages.xml'):
280 sys.exit('You have no crowdin/extra_packages.xml. Terminating.')
281else:
282 print('Found: crowdin/extra_packages.xml')
283
284print('\nSTEP 1: Remove CAF additions (non-AOSP supported languages)')
285# Load crowdin/caf.xml
286print('Loading crowdin/caf.xml')
287xml = minidom.parse('crowdin/caf.xml')
288items = xml.getElementsByTagName('item')
289
290# Store all created cm_caf.xml files in here.
291# Easier to remove them afterwards, as they cannot be committed
292cm_caf_add = []
293
294for item in items:
295 # Create tmp dir for download of AOSP base file
296 path_to_values = item.attributes["path"].value
297 subprocess.call(['mkdir', '-p', 'tmp/' + path_to_values])
298 for aosp_item in item.getElementsByTagName('aosp'):
299 url = aosp_item.firstChild.nodeValue
300 xml_file = aosp_item.attributes["file"].value
301 path_to_base = 'tmp/' + path_to_values + '/' + xml_file
302 path_to_cm = path_to_values + '/' + xml_file
303 urlretrieve(url, path_to_base)
304 purge_caf_additions(path_to_base, path_to_cm)
305 cm_caf_add.append(path_to_cm)
306 print('Purged ' + path_to_cm + ' from CAF additions')
307
308print('\nSTEP 2: Upload Crowdin source translations (non-AOSP supported languages')
309# Execute 'crowdin-cli upload sources' and show output
310print(subprocess.check_output(['crowdin-cli', '-c', 'crowdin/crowdin_aosp.yaml', 'upload', 'sources']))
311
312print('\nSTEP 3: Revert removal of CAF additions (non-AOSP supported languages)')
313for purged_file in cm_caf_add:
314 os.remove(purged_file)
315 shutil.move(purged_file + '.backup', purged_file)
316 print('Reverted purged file ' + purged_file)
317
318print('\nSTEP 4: Create source cm_caf.xmls (AOSP supported languages)')
319# Load crowdin/caf.xml
320print('Loading crowdin/caf.xml')
321xml = minidom.parse('crowdin/caf.xml')
322items = xml.getElementsByTagName('item')
323
324# Store all created cm_caf.xml files in here.
325# Easier to remove them afterwards, as they cannot be committed
326cm_caf = []
327
328for item in items:
329 # Create tmp dir for download of AOSP base file
330 path_to_values = item.attributes["path"].value
331 subprocess.call(['mkdir', '-p', 'tmp/' + path_to_values])
332 # Create cm_caf.xml - header
333 f = codecs.open(path_to_values + '/cm_caf.xml', 'w', 'utf-8')
334 f.write('<?xml version="1.0" encoding="utf-8"?>\n')
335 f.write('<!--\n')
336 f.write(' Copyright (C) 2014 The CyanogenMod Project\n')
337 f.write('\n')
338 f.write(' Licensed under the Apache License, Version 2.0 (the "License");\n')
339 f.write(' you may not use this file except in compliance with the License.\n')
340 f.write(' You may obtain a copy of the License at\n')
341 f.write('\n')
342 f.write(' http://www.apache.org/licenses/LICENSE-2.0\n')
343 f.write('\n')
344 f.write(' Unless required by applicable law or agreed to in writing, software\n')
345 f.write(' distributed under the License is distributed on an "AS IS" BASIS,\n')
346 f.write(' WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n')
347 f.write(' See the License for the specific language governing permissions and\n')
348 f.write(' limitations under the License.\n')
349 f.write('-->\n')
350 f.write('<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">\n')
351 # Create cm_caf.xml - contents
352 # This means we also support multiple base files (e.g. checking if strings.xml and arrays.xml are changed)
353 contents = []
354 item_aosp = item.getElementsByTagName('aosp')
355 for aosp_item in item_aosp:
356 url = aosp_item.firstChild.nodeValue
357 xml_file = aosp_item.attributes["file"].value
358 path_to_base = 'tmp/' + path_to_values + '/' + xml_file
359 path_to_cm = path_to_values + '/' + xml_file
360 urlretrieve(url, path_to_base)
361 contents = contents + get_caf_additions(path_to_base, path_to_cm)
362 for addition in contents:
363 f.write(addition + '\n')
364 # Create cm_caf.xml - the end
365 f.write('</resources>')
366 f.close()
367 cm_caf.append(path_to_values + '/cm_caf.xml')
368 print('Created ' + path_to_values + '/cm_caf.xml')
369
370print('\nSTEP 5: Upload Crowdin source translations (AOSP supported languages)')
371# Execute 'crowdin-cli upload sources' and show output
372print(subprocess.check_output(['crowdin-cli', '-c', 'crowdin/crowdin_cm.yaml', 'upload', 'sources']))
373
374print('\nSTEP 6A: Download Crowdin translations (AOSP supported languages)')
375# Execute 'crowdin-cli download' and show output
376print(subprocess.check_output(['crowdin-cli', '-c', 'crowdin/crowdin_cm.yaml', 'download']))
377
378print('\nSTEP 6B: Download Crowdin translations (non-AOSP supported languages)')
379# Execute 'crowdin-cli download' and show output
380print(subprocess.check_output(['crowdin-cli', '-c', 'crowdin/crowdin_aosp.yaml', 'download']))
381
382print('\nSTEP 7: Remove temp dir')
383# We are done with cm_caf.xml files, so remove tmp/
384shutil.rmtree(os.getcwd() + '/tmp')
385
386print('\nSTEP 8: Remove useless empty translations')
387# Some line of code that I found to find all XML files
388result = [os.path.join(dp, f) for dp, dn, filenames in os.walk(os.getcwd()) for f in filenames if os.path.splitext(f)[1] == '.xml']
389for xml_file in result:
390 # We hate empty, useless files. Crowdin exports them with <resources/> (sometimes with xliff).
391 # That means: easy to find
392 if '<resources/>' in open(xml_file).read():
393 print('Removing ' + xml_file)
394 os.remove(xml_file)
395 elif '<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"/>' in open(xml_file).read():
396 print('Removing ' + xml_file)
397 os.remove(xml_file)
398 elif '<resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"/>' in open(xml_file).read():
399 print('Removing ' + xml_file)
400 os.remove(xml_file)
401
402print('\nSTEP 9: Create a list of pushable translations')
403# Get all files that Crowdin pushed
404proc = subprocess.Popen(['crowdin-cli -c crowdin/crowdin_cm.yaml list sources && crowdin-cli -c crowdin/crowdin_aosp.yaml list sources'], stdout=subprocess.PIPE, shell=True)
405proc.wait() # Wait for the above to finish
406
407print('\nSTEP 10: Remove unwanted source cm_caf.xmls (AOSP supported languages)')
408# Remove all cm_caf.xml files, which you can find in the list 'cm_caf'
409for cm_caf_file in cm_caf:
410 print('Removing ' + cm_caf_file)
411 os.remove(cm_caf_file)
412
413print('\nSTEP 11: Commit to Gerrit')
414xml = minidom.parse('android/default.xml')
415xml_extra = minidom.parse('crowdin/extra_packages.xml')
416items = xml.getElementsByTagName('project')
417items += xml_extra.getElementsByTagName('project')
418all_projects = []
419
420for path in iter(proc.stdout.readline,''):
421 # Remove the \n at the end of each line
422 path = path.rstrip()
423 # Get project root dir from Crowdin's output
424 m = re.search('/(.*Superuser)/Superuser.*|/(.*LatinIME).*|/(frameworks/base).*|/(.*CMFileManager).*|/(device/.*/.*)/.*/res/values.*|/(hardware/.*/.*)/.*/res/values.*|/(.*)/res/values.*', path)
425 for good_path in m.groups():
426 # When a project has multiple translatable files, Crowdin will give duplicates.
427 # We don't want that (useless empty commits), so we save each project in all_projects
428 # and check if it's already in there.
429 if good_path is not None and not good_path in all_projects:
430 all_projects.append(good_path)
431 for project_item in items:
432 # We need to have the Github repository for the git push url.
433 # Obtain them from android/default.xml or crowdin/extra_packages.xml.
434 if project_item.attributes['path'].value == good_path:
435 if project_item.hasAttribute('revision'):
436 branch = project_item.attributes['revision'].value
437 else:
438 branch = 'cm-11.0'
439 print('Committing ' + project_item.attributes['name'].value + ' on branch ' + branch)
440 push_as_commit(good_path, project_item.attributes['name'].value, branch)
441
442print('\nDone!')