blob: e779e64543c448261673f51901c766f45f4b1e9e [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)
153 try:
154 repo.git.commit(m='Automatic translation import')
155 repo.git.push('ssh://' + username + '@review.cyanogenmod.org:29418/' + name, 'HEAD:refs/for/' + branch)
156 print 'Succesfully pushed commit for ' + name
157 except:
158 # If git commit fails, it's probably because of no changes.
159 # Just continue.
160 print 'No commit pushed (probably empty?) for ' + name
161
162print('Welcome to the CM Crowdin sync script!')
163
164print('\nSTEP 0: Checking dependencies')
165# Check for Ruby version of crowdin-cli
166if subprocess.check_output(['rvm', 'all', 'do', 'gem', 'list', 'crowdin-cli', '-i']) == 'true':
167 sys.exit('You have not installed crowdin-cli. Terminating.')
168else:
169 print('Found: crowdin-cli')
170# Check for caf.xml
171if not os.path.isfile('caf.xml'):
172 sys.exit('You have no caf.xml. Terminating.')
173else:
174 print('Found: caf.xml')
175# Check for android/default.xml
176if not os.path.isfile('android/default.xml'):
177 sys.exit('You have no android/default.xml. Terminating.')
178else:
179 print('Found: android/default.xml')
180# Check for extra_packages.xml
181if not os.path.isfile('extra_packages.xml'):
182 sys.exit('You have no extra_packages.xml. Terminating.')
183else:
184 print('Found: extra_packages.xml')
185# Check for repo
186try:
187 subprocess.check_output(['which', 'repo'])
188except:
189 sys.exit('You have not installed repo. Terminating.')
190
191print('\nSTEP 1: Removing CAF additions')
192# Load caf.xml
193print('Loading caf.xml')
194xml = minidom.parse('caf.xml')
195items = xml.getElementsByTagName('item')
196
197# Store all created cm_caf.xml files in here.
198# Easier to remove them afterwards, as they cannot be committed
199cm_caf = []
200
201for item in items:
202 # Create tmp dir for download of AOSP base file
203 path_to_values = item.attributes["path"].value
204 subprocess.call(['mkdir', '-p', 'tmp/' + path_to_values])
205 for aosp_item in item.getElementsByTagName('aosp'):
206 url = aosp_item.firstChild.nodeValue
207 xml_file = aosp_item.attributes["file"].value
208 path_to_base = 'tmp/' + path_to_values + '/' + xml_file
209 path_to_cm = path_to_values + '/' + xml_file
210 urlretrieve(url, path_to_base)
211 purge_caf_additions(path_to_base, path_to_cm)
212 cm_caf.append(path_to_cm)
213 print('Purged ' + path_to_cm + ' from CAF additions')
214
215print('\nSTEP 2: Upload Crowdin source translations')
216# Execute 'crowdin-cli upload sources' and show output
217print(subprocess.check_output(['crowdin-cli', '-c', 'crowdin-aosp.yaml', 'upload', 'sources']))
218
219print('\nSTEP 3: Download Crowdin translations')
220# Execute 'crowdin-cli download' and show output
221print(subprocess.check_output(['crowdin-cli', '-c', 'crowdin-aosp.yaml', "download"]))
222
223print('\nSTEP 4A: Revert purges')
224for purged_file in cm_caf:
225 os.remove(purged_file)
226 shutil.move(purged_file + '.backup', purged_file)
227 print('Reverted purged file ' + purged_file)
228
229print('\nSTEP 4B: Clean up of temp dir')
230# We are done with cm_caf.xml files, so remove tmp/
231shutil.rmtree(os.getcwd() + '/tmp')
232
233print('\nSTEP 4C: Clean up of empty translations')
234# Some line of code that I found to find all XML files
235result = [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']
236for xml_file in result:
237 # We hate empty, useless files. Crowdin exports them with <resources/> (sometimes with xliff).
238 # That means: easy to find
239 if '<resources/>' in open(xml_file).read():
240 print ('Removing ' + xml_file)
241 os.remove(xml_file)
242 elif '<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"/>' in open(xml_file).read():
243 print ('Removing ' + xml_file)
244 os.remove(xml_file)
Michael Bestas4bfe4522014-05-24 01:22:05 +0300245 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():
246 print ('Removing ' + xml_file)
247 os.remove(xml_file)
Michael Bestas4de63772014-05-11 02:46:29 +0300248
249print('\nSTEP 5: Push translations to Git')
250# Get all files that Crowdin pushed
251proc = subprocess.Popen(['crowdin-cli', '-c', 'crowdin-aosp.yaml', 'list', 'sources'],stdout=subprocess.PIPE)
252xml = minidom.parse('android/default.xml')
253xml_extra = minidom.parse('extra_packages.xml')
254items = xml.getElementsByTagName('project')
255items += xml_extra.getElementsByTagName('project')
256all_projects = []
257
258for path in iter(proc.stdout.readline,''):
259 # Remove the \n at the end of each line
260 path = path.rstrip()
261 # Get project root dir from Crowdin's output
262 m = re.search('/(.*Superuser)/Superuser.*|/(.*LatinIME).*|/(frameworks/base).*|/(.*CMFileManager).*|/(device/.*/.*)/.*/res/values.*|/(hardware/.*/.*)/.*/res/values.*|/(.*)/res/values.*', path)
263 for good_path in m.groups():
264 # When a project has multiple translatable files, Crowdin will give duplicates.
265 # We don't want that (useless empty commits), so we save each project in all_projects
266 # and check if it's already in there.
267 if good_path is not None and not good_path in all_projects:
268 all_projects.append(good_path)
269 for project_item in items:
270 # We need to have the Github repository for the git push url.
271 # Obtain them from android/default.xml or extra_packages.xml.
272 if project_item.attributes["path"].value == good_path:
273 if project_item.hasAttribute('revision'):
274 branch = project_item.attributes['revision'].value
275 else:
276 branch = 'cm-11.0'
277 print 'Committing ' + project_item.attributes['name'].value + ' on branch ' + branch + ' (based on android/default.xml or extra_packages.xml)'
278 push_as_commit(good_path, project_item.attributes['name'].value, branch)
279
280print('\nSTEP 6: Done!')