blob: 4456515916fad81d50c7c062900d33c0323ea508 [file] [log] [blame]
Michael Bestas4de63772014-05-11 02:46:29 +03001#!/usr/bin/python2
2# -*- coding: utf-8 -*-
3# cm_crowdin_sync.py
4#
5# Updates Crowdin source translations and pulls translations
6# directly to CyanogenMod's Git.
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 mmap
25import os
26import os.path
27import re
28import shutil
29import subprocess
30import sys
31from urllib import urlretrieve
32from xml.dom import minidom
33
34def purge_caf_additions(strings_base, strings_cm):
35 # Load AOSP file and resources
36 xml_base = minidom.parse(strings_base)
37 list_base_string = xml_base.getElementsByTagName('string')
38 list_base_string_array = xml_base.getElementsByTagName('string-array')
39 list_base_plurals = xml_base.getElementsByTagName('plurals')
40 # Load CM file and resources
41 xml_cm = minidom.parse(strings_cm)
42 list_cm_string = xml_cm.getElementsByTagName('string')
43 list_cm_string_array = xml_cm.getElementsByTagName('string-array')
44 list_cm_plurals = xml_cm.getElementsByTagName('plurals')
45 with codecs.open(strings_cm, 'r', 'utf-8') as f:
46 content = [line.rstrip() for line in f]
47 shutil.copyfile(strings_cm, strings_cm + '.backup')
48 file_this = codecs.open(strings_cm, 'w', 'utf-8')
49
50 # All names from AOSP
51 names_base_string = []
52 names_base_string_array = []
53 names_base_plurals = []
54
55 # Get all names from AOSP
56 for s in list_base_string :
57 names_base_string.append(s.attributes['name'].value)
58 for s in list_base_string_array :
59 names_base_string_array.append(s.attributes['name'].value)
60 for s in list_base_plurals :
61 names_base_plurals.append(s.attributes['name'].value)
62
63 # Get all names from CM
64 content2 = []
65 for s in list_cm_string :
66 name = s.attributes['name'].value
67 if name not in names_base_string:
68 true = 0
69 content2 = []
70 for i in content:
71 if true == 0:
72 test = re.search('(<string name=\"' + name + ')', i)
73 if test is not None:
74 test2 = re.search('(</string>)', i)
75 if test2:
76 true = 2
77 else:
78 true = 1
79 i = ''
80 elif true == 1:
81 test2 = re.search('(</string>)', i)
82 if test2 is not None:
83 true = 2
84 i = ''
85 elif true == 2:
86 true = 3
87 content2.append(i)
88 content = content2
89 for s in list_cm_string_array :
90 name = s.attributes['name'].value
91 if name not in names_base_string_array:
92 true = 0
93 content2 = []
94 for i in content:
95 if true == 0:
96 test = re.search('(<string-array name=\"' + name + ')', i)
97 if test is not None:
98 test2 = re.search('(</string-array>)', i)
99 if test2:
100 true = 2
101 else:
102 true = 1
103 i = ''
104 elif true == 1:
105 test2 = re.search('(</string-array>)', i)
106 if test2 is not None:
107 true = 2
108 i = ''
109 elif true == 2:
110 true = 3
111 content2.append(i)
112 content = content2
113 for s in list_cm_plurals :
114 name = s.attributes['name'].value
115 if name not in names_base_plurals:
116 true = 0
117 content2 = []
118 for i in content:
119 if true == 0:
120 test = re.search('(<plurals name=\"' + name + ')', i)
121 if test is not None:
122 test2 = re.search('(</plurals>)', i)
123 if test2:
124 true = 2
125 else:
126 true = 1
127 i = ''
128 elif true == 1:
129 test2 = re.search('(</plurals>)', i)
130 if test2 is not None:
131 true = 2
132 i = ''
133 elif true == 2:
134 # The actual purging is done!
135 true = 3
136 content2.append(i)
137 content = content2
138
139 for addition in content:
140 file_this.write(addition + '\n')
141 file_this.close()
142
143def push_as_commit(path, name, branch):
144 # CM gerrit nickname
145 username = 'your_nickname'
146
147 # Get path
148 path = os.getcwd() + '/' + path
149
150 # Create git commit
151 repo = git.Repo(path)
152 repo.git.add(path)
Michael Bestasf2b10902014-06-21 15:15:34 +0300153 removed_files = repo.git.ls_files(d=True).split('\n')
154 try:
155 repo.git.rm(removed_files)
156 except:
157 pass
Michael Bestas4de63772014-05-11 02:46:29 +0300158 try:
159 repo.git.commit(m='Automatic translation import')
160 repo.git.push('ssh://' + username + '@review.cyanogenmod.org:29418/' + name, 'HEAD:refs/for/' + branch)
161 print 'Succesfully pushed commit for ' + name
162 except:
163 # If git commit fails, it's probably because of no changes.
164 # Just continue.
165 print 'No commit pushed (probably empty?) for ' + name
166
167print('Welcome to the CM Crowdin sync script!')
168
169print('\nSTEP 0: Checking dependencies')
170# Check for Ruby version of crowdin-cli
171if subprocess.check_output(['rvm', 'all', 'do', 'gem', 'list', 'crowdin-cli', '-i']) == 'true':
172 sys.exit('You have not installed crowdin-cli. Terminating.')
173else:
174 print('Found: crowdin-cli')
175# Check for caf.xml
176if not os.path.isfile('caf.xml'):
177 sys.exit('You have no caf.xml. Terminating.')
178else:
179 print('Found: caf.xml')
180# Check for android/default.xml
181if not os.path.isfile('android/default.xml'):
182 sys.exit('You have no android/default.xml. Terminating.')
183else:
184 print('Found: android/default.xml')
185# Check for extra_packages.xml
186if not os.path.isfile('extra_packages.xml'):
187 sys.exit('You have no extra_packages.xml. Terminating.')
188else:
189 print('Found: extra_packages.xml')
190# Check for repo
191try:
192 subprocess.check_output(['which', 'repo'])
193except:
194 sys.exit('You have not installed repo. Terminating.')
195
196print('\nSTEP 1: Removing CAF additions')
197# Load caf.xml
198print('Loading caf.xml')
199xml = minidom.parse('caf.xml')
200items = xml.getElementsByTagName('item')
201
202# Store all created cm_caf.xml files in here.
203# Easier to remove them afterwards, as they cannot be committed
204cm_caf = []
205
206for item in items:
207 # Create tmp dir for download of AOSP base file
208 path_to_values = item.attributes["path"].value
209 subprocess.call(['mkdir', '-p', 'tmp/' + path_to_values])
210 for aosp_item in item.getElementsByTagName('aosp'):
211 url = aosp_item.firstChild.nodeValue
212 xml_file = aosp_item.attributes["file"].value
213 path_to_base = 'tmp/' + path_to_values + '/' + xml_file
214 path_to_cm = path_to_values + '/' + xml_file
215 urlretrieve(url, path_to_base)
216 purge_caf_additions(path_to_base, path_to_cm)
217 cm_caf.append(path_to_cm)
218 print('Purged ' + path_to_cm + ' from CAF additions')
219
220print('\nSTEP 2: Upload Crowdin source translations')
221# Execute 'crowdin-cli upload sources' and show output
222print(subprocess.check_output(['crowdin-cli', '-c', 'crowdin-aosp.yaml', 'upload', 'sources']))
223
224print('\nSTEP 3: Download Crowdin translations')
225# Execute 'crowdin-cli download' and show output
226print(subprocess.check_output(['crowdin-cli', '-c', 'crowdin-aosp.yaml', "download"]))
227
228print('\nSTEP 4A: Revert purges')
229for purged_file in cm_caf:
230 os.remove(purged_file)
231 shutil.move(purged_file + '.backup', purged_file)
232 print('Reverted purged file ' + purged_file)
233
234print('\nSTEP 4B: Clean up of temp dir')
235# We are done with cm_caf.xml files, so remove tmp/
236shutil.rmtree(os.getcwd() + '/tmp')
237
238print('\nSTEP 4C: Clean up of empty translations')
239# Some line of code that I found to find all XML files
240result = [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']
241for xml_file in result:
242 # We hate empty, useless files. Crowdin exports them with <resources/> (sometimes with xliff).
243 # That means: easy to find
244 if '<resources/>' in open(xml_file).read():
245 print ('Removing ' + xml_file)
246 os.remove(xml_file)
247 elif '<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"/>' in open(xml_file).read():
248 print ('Removing ' + xml_file)
249 os.remove(xml_file)
Michael Bestas4bfe4522014-05-24 01:22:05 +0300250 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():
251 print ('Removing ' + xml_file)
252 os.remove(xml_file)
Michael Bestas4de63772014-05-11 02:46:29 +0300253
254print('\nSTEP 5: Push translations to Git')
255# Get all files that Crowdin pushed
256proc = subprocess.Popen(['crowdin-cli', '-c', 'crowdin-aosp.yaml', 'list', 'sources'],stdout=subprocess.PIPE)
257xml = minidom.parse('android/default.xml')
258xml_extra = minidom.parse('extra_packages.xml')
259items = xml.getElementsByTagName('project')
260items += xml_extra.getElementsByTagName('project')
261all_projects = []
262
263for path in iter(proc.stdout.readline,''):
264 # Remove the \n at the end of each line
265 path = path.rstrip()
266 # Get project root dir from Crowdin's output
267 m = re.search('/(.*Superuser)/Superuser.*|/(.*LatinIME).*|/(frameworks/base).*|/(.*CMFileManager).*|/(device/.*/.*)/.*/res/values.*|/(hardware/.*/.*)/.*/res/values.*|/(.*)/res/values.*', path)
268 for good_path in m.groups():
269 # When a project has multiple translatable files, Crowdin will give duplicates.
270 # We don't want that (useless empty commits), so we save each project in all_projects
271 # and check if it's already in there.
272 if good_path is not None and not good_path in all_projects:
273 all_projects.append(good_path)
274 for project_item in items:
275 # We need to have the Github repository for the git push url.
276 # Obtain them from android/default.xml or extra_packages.xml.
277 if project_item.attributes["path"].value == good_path:
278 if project_item.hasAttribute('revision'):
279 branch = project_item.attributes['revision'].value
280 else:
281 branch = 'cm-11.0'
282 print 'Committing ' + project_item.attributes['name'].value + ' on branch ' + branch + ' (based on android/default.xml or extra_packages.xml)'
283 push_as_commit(good_path, project_item.attributes['name'].value, branch)
284
285print('\nSTEP 6: Done!')