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