blob: b40ccebf9b2648267fd28ede91b6351f355f3a22 [file] [log] [blame]
Marco Brohet4683bee2014-02-28 01:06:03 +01001#!/usr/bin/python2
2#
3# cm_crowdin_sync.py
4#
5# Updates Crowdin source translations and pulls translations
Marco Brohet8b78a1b2014-02-28 21:01:26 +01006# 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.
Marco Brohet4683bee2014-02-28 01:06:03 +010021
Marco Brohet4683bee2014-02-28 01:06:03 +010022import git
23import mmap
Marco Brohetcf4069b2014-02-28 18:48:17 +010024import os
Marco Brohet4683bee2014-02-28 01:06:03 +010025import os.path
26import re
27import shutil
28import subprocess
29import sys
30from urllib import urlretrieve
31from xml.dom import minidom
32
Marco Brohet8b78a1b2014-02-28 21:01:26 +010033def 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')
Marco Brohet4683bee2014-02-28 01:06:03 +010044
Marco Brohet8b78a1b2014-02-28 21:01:26 +010045 # 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 # Add all CAF additions to the list 'caf_additions'
79 for z in names_cm_string:
80 if not z in names_base_string:
81 caf_additions.append(' ' + list_cm_string[names_cm_string.index(z)].toxml())
82 for z in names_cm_string_array:
83 if not z in names_base_string_array:
84 caf_additions.append(' ' + list_cm_string_array[names_cm_string_array.index(z)].toxml())
85 for z in names_cm_plurals:
86 if not z in names_base_plurals:
87 caf_additions.append(' ' + list_cm_plurals[names_cm_plurals.index(z)].toxml())
88
89 # Done :-)
90 return caf_additions
91
92def push_as_commit(path, name):
93 # Get path
94 path = os.getcwd() + '/' + path
95
96 # Create git commit
97 repo = git.Repo(path)
98 repo.git.add(path)
99 try:
100 repo.git.commit(m='DO NOT MERGE: Automatic translation import test commit')
101# repo.git.push('ssh://cobjeM@review.cyanogenmod.org:29418/' + name, 'HEAD:refs/for/cm-11.0')
102 print 'Succesfully pushed commit for ' + name
103 except:
104 # If git commit fails, it's probably because of no changes.
105 # Just continue.
106 print 'No commit pushed (probably empty?) for ' + name
107 print 'WARNING: If the repository name was not obtained from default.xml, the name might be wrong!'
108
109print('Welcome to the CM Crowdin sync script!')
110
111print('\nSTEP 0: Checking dependencies')
Marco Brohet4683bee2014-02-28 01:06:03 +0100112if subprocess.check_output(['rvm', 'all', 'do', 'gem', 'list', 'crowdin-cli', '-i']) == 'true':
113 sys.exit('You have not installed crowdin-cli. Terminating.')
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100114else:
115 print('Found: crowdin-cli')
Marco Brohet4683bee2014-02-28 01:06:03 +0100116if not os.path.isfile('caf.xml'):
117 sys.exit('You have no caf.xml. Terminating.')
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100118else:
119 print('Found: caf.xml')
Marco Brohet4683bee2014-02-28 01:06:03 +0100120if not os.path.isfile('default.xml'):
121 sys.exit('You have no default.xml. Terminating.')
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100122else:
123 print('Found: default.xml')
Marco Brohet4683bee2014-02-28 01:06:03 +0100124
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100125print('\nSTEP 1: Create cm_caf.xml')
126# Load caf.xml
Marco Brohet4683bee2014-02-28 01:06:03 +0100127xml = minidom.parse('caf.xml')
128items = xml.getElementsByTagName('item')
129
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100130# Store all created cm_caf.xml files in here.
131# Easier to remove them afterwards, as they cannot be committed
Marco Brohet4683bee2014-02-28 01:06:03 +0100132cm_caf = []
133
134for item in items:
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100135 # Create tmp dir for download of AOSP base file
136 path_to_values = item.attributes["path"].value
137 subprocess.call(['mkdir', '-p', 'tmp/' + path_to_values])
138 # Create cm_caf.xml - header
139 f = open(path_to_values + '/cm_caf.xml','w')
140 f.write('<?xml version="1.0" encoding="utf-8"?>\n')
141 f.write('<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">\n')
142 # Create cm_caf.xml - contents
143 # This means we also support multiple base files (e.g. checking if strings.xml and arrays.xml are changed)
144 contents = []
Marco Brohet4683bee2014-02-28 01:06:03 +0100145 item_aosp = item.getElementsByTagName('aosp')
146 for aosp_item in item_aosp:
147 url = aosp_item.firstChild.nodeValue
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100148 xml_file = aosp_item.attributes["file"].value
149 path_to_base = 'tmp/' + path_to_values + '/' + xml_file
150 path_to_cm = path_to_values + '/' + xml_file
Marco Brohet4683bee2014-02-28 01:06:03 +0100151 urlretrieve(url, path_to_base)
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100152 contents = contents + get_caf_additions(path_to_base, path_to_cm)
153 for addition in contents:
154 f.write(addition + '\n')
155 # Create cm_caf.xml - the end
156 f.write('</resources>')
157 f.close()
158 cm_caf.append(path_to_values + '/cm_caf.xml')
159 print('Created ' + path_to_values + '/cm_caf.xml')
Marco Brohet4683bee2014-02-28 01:06:03 +0100160
161print('\nSTEP 2: Upload Crowdin source translations')
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100162# Execute 'crowdin-cli upload sources' and show output
Marco Brohet4683bee2014-02-28 01:06:03 +0100163print(subprocess.check_output(['crowdin-cli', 'upload', 'sources']))
164
Marco Brohetcf4069b2014-02-28 18:48:17 +0100165print('STEP 3: Download Crowdin translations')
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100166# Execute 'crowdin-cli download' and show output
Marco Brohetcf4069b2014-02-28 18:48:17 +0100167print(subprocess.check_output(['crowdin-cli', "download"]))
Marco Brohet4683bee2014-02-28 01:06:03 +0100168
Marco Brohetcf4069b2014-02-28 18:48:17 +0100169print('STEP 4A: Clean up of source cm_caf.xmls')
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100170# Remove all cm_caf.xml files, which you can find in the list 'cm_caf'
Marco Brohetcf4069b2014-02-28 18:48:17 +0100171for cm_caf_file in cm_caf:
172 print ('Removing ' + cm_caf_file)
173 os.remove(cm_caf_file)
174
175print('\nSTEP 4B: Clean up of temp dir')
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100176# We are done with cm_caf.xml files, so remove tmp/
Marco Brohetcf4069b2014-02-28 18:48:17 +0100177shutil.rmtree(os.getcwd() + '/tmp')
178
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100179print('\nSTEP 4C: Clean up of empty translations')
180# Some line of code that I found to find all XML files
Marco Brohet4683bee2014-02-28 01:06:03 +0100181result = [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']
182for xml_file in result:
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100183 # We hate empty, useless files. Crowdin exports them with <resources/> (sometimes with xliff).
184 # That means: easy to find
Marco Brohet4683bee2014-02-28 01:06:03 +0100185 if '<resources/>' in open(xml_file).read():
186 print ('Removing ' + xml_file)
187 os.remove(xml_file)
Marco Brohetcf4069b2014-02-28 18:48:17 +0100188 elif '<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"/>' in open(xml_file).read():
Marco Brohet4683bee2014-02-28 01:06:03 +0100189 print ('Removing ' + xml_file)
190 os.remove(xml_file)
191
Marco Brohet4683bee2014-02-28 01:06:03 +0100192print('\nSTEP 5: Push translations to Git')
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100193# Get all files that Crowdin pushed
Marco Brohet4683bee2014-02-28 01:06:03 +0100194proc = subprocess.Popen(['crowdin-cli', 'list', 'sources'],stdout=subprocess.PIPE)
Marco Brohet4683bee2014-02-28 01:06:03 +0100195xml = minidom.parse('default.xml')
196items = xml.getElementsByTagName('project')
Marco Brohetcf4069b2014-02-28 18:48:17 +0100197all_projects = []
Marco Brohet4683bee2014-02-28 01:06:03 +0100198
Marco Brohetcf4069b2014-02-28 18:48:17 +0100199for path in iter(proc.stdout.readline,''):
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100200 # Remove the \n at the end of each line
Marco Brohetcf4069b2014-02-28 18:48:17 +0100201 path = path.rstrip()
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100202 # Get project root dir from Crowdin's output
Marco Brohetcf4069b2014-02-28 18:48:17 +0100203 m = re.search('/(.*Superuser)/Superuser.*|/(.*LatinIME).*|/(frameworks/base).*|/(.*CMFileManager).*|/(device/.*/.*)/.*/res/values.*|/(hardware/.*/.*)/.*/res/values.*|/(.*)/res/values.*', path)
204 for good_path in m.groups():
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100205 # When a project has multiple translatable files, Crowdin will give duplicates.
206 # We don't want that (useless empty commits), so we save each project in all_projects
207 # and check if it's already in there.
Marco Brohetcf4069b2014-02-28 18:48:17 +0100208 if good_path is not None and not good_path in all_projects:
209 all_projects.append(good_path)
210 working = 'false'
211 for project_item in items:
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100212 # We need to have the Github repository for the git push url. Obtain them from
213 # default.xml based on the project root dir.
Marco Brohetcf4069b2014-02-28 18:48:17 +0100214 if project_item.attributes["path"].value == good_path:
215 working = 'true'
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100216 push_as_commit(good_path, project_item.attributes['name'].value)
217 print 'Committing ' + project_item.attributes['name'].value + ' (based on default.xml)'
218 # We also translate repositories that are not downloaded by default (e.g. device parts).
219 # This is just a fallback.
220 # WARNING: If the name is wrong, this will not stop the script.
Marco Brohetcf4069b2014-02-28 18:48:17 +0100221 if working == 'false':
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100222 push_as_commit(good_path, 'CyanogenMod/android_' + good_path.replace('/', '_'))
223 print 'Committing ' + project_item.attributes['name'].value + ' (workaround)'
Marco Brohet4683bee2014-02-28 01:06:03 +0100224
225print('STEP 6: Done!')