blob: 9da76189253f6299a36cf8381536f1d23f03885d [file] [log] [blame]
Marco Brohet4683bee2014-02-28 01:06:03 +01001#!/usr/bin/python2
Marco Brohetf1742722014-03-04 22:41:18 +01002# -*- coding: utf-8 -*-
Marco Brohet4683bee2014-02-28 01:06:03 +01003# 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 Brohetf1742722014-03-04 22:41:18 +010022import codecs
Marco Brohet4683bee2014-02-28 01:06:03 +010023import git
24import mmap
Marco Brohetcf4069b2014-02-28 18:48:17 +010025import os
Marco Brohet4683bee2014-02-28 01:06:03 +010026import os.path
27import re
28import shutil
29import subprocess
30import sys
31from urllib import urlretrieve
32from xml.dom import minidom
33
Marco Brohet8b78a1b2014-02-28 21:01:26 +010034def get_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')
Marco Brohet4683bee2014-02-28 01:06:03 +010045
Marco Brohet8b78a1b2014-02-28 21:01:26 +010046 # All names from CM
47 names_cm_string = []
48 names_cm_string_array = []
49 names_cm_plurals = []
50 # All names from AOSP
51 names_base_string = []
52 names_base_string_array = []
53 names_base_plurals = []
54
55 # Get all names from CM
56 for s in list_cm_string :
57 if not s.hasAttribute('translatable') and not s.hasAttribute('translate'):
58 names_cm_string.append(s.attributes['name'].value)
59 for s in list_cm_string_array :
60 if not s.hasAttribute('translatable') and not s.hasAttribute('translate'):
61 names_cm_string_array.append(s.attributes['name'].value)
62 for s in list_cm_plurals :
63 if not s.hasAttribute('translatable') and not s.hasAttribute('translate'):
64 names_cm_plurals.append(s.attributes['name'].value)
65 # Get all names from AOSP
66 for s in list_base_string :
67 if not s.hasAttribute('translatable') and not s.hasAttribute('translate'):
68 names_base_string.append(s.attributes['name'].value)
69 for s in list_base_string_array :
70 if not s.hasAttribute('translatable') and not s.hasAttribute('translate'):
71 names_base_string_array.append(s.attributes['name'].value)
72 for s in list_base_plurals :
73 if not s.hasAttribute('translatable') and not s.hasAttribute('translate'):
74 names_base_plurals.append(s.attributes['name'].value)
75
76 # Store all differences in this list
77 caf_additions = []
78
Marco Brohet3cd76422014-03-08 21:12:09 +010079 # Store all found strings/arrays/plurals.
80 # Prevent duplicates with product attribute
81 found_string = []
82 found_string_array = []
83 found_plurals = []
84
Marco Brohet8b78a1b2014-02-28 21:01:26 +010085 # Add all CAF additions to the list 'caf_additions'
86 for z in names_cm_string:
Marco Brohet3cd76422014-03-08 21:12:09 +010087 if z not in names_base_string and z not in found_string:
Marco Brohet25623ce2014-03-08 19:13:07 +010088 for string_item in list_cm_string:
89 if string_item.attributes['name'].value == z:
90 caf_additions.append(' ' + string_item.toxml())
Marco Brohet3cd76422014-03-08 21:12:09 +010091 found_string.append(z)
Marco Brohet25623ce2014-03-08 19:13:07 +010092 for y in names_cm_string_array:
Marco Brohet3cd76422014-03-08 21:12:09 +010093 if y not in names_base_string_array and y not in found_string_array:
Marco Brohet25623ce2014-03-08 19:13:07 +010094 for string_array_item in list_cm_string_array:
95 if string_array_item.attributes['name'].value == y:
96 caf_additions.append(' ' + string_array_item.toxml())
Marco Brohet3cd76422014-03-08 21:12:09 +010097 found_string_array.append(y)
Marco Brohet25623ce2014-03-08 19:13:07 +010098 for x in names_cm_plurals:
Marco Brohet3cd76422014-03-08 21:12:09 +010099 if x not in names_base_plurals and x not in found_plurals:
Marco Brohet25623ce2014-03-08 19:13:07 +0100100 for plurals_item in list_cm_plurals:
101 if plurals_item.attributes['name'].value == x:
102 caf_additions.append(' ' + plurals_item.toxml())
Marco Brohet3cd76422014-03-08 21:12:09 +0100103 found_plurals.append(x)
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100104
105 # Done :-)
106 return caf_additions
107
Marco Brohet7165b4e2014-03-02 17:31:17 +0100108def sync_js_translations(sync_type, path, lang=''):
109 # lang is necessary in download mode
110 if sync_type == 'download' and lang == '':
111 sys.exit('Invalid syntax. Language code is required in download mode.')
112
113 # Read source en.js file. This is necessary for both upload and download modes
Marco Brohet44657ed2014-03-04 22:49:23 +0100114 with codecs.open(path + 'en.js', 'r', 'utf-8') as f:
Marco Brohet7165b4e2014-03-02 17:31:17 +0100115 content = f.readlines()
116
117 if sync_type == 'upload':
118 # Prepare XML file structure
119 doc = xml.dom.minidom.Document()
120 header = doc.createElement('resources')
Marco Brohet44657ed2014-03-04 22:49:23 +0100121 file_write = codecs.open(path + 'en.xml', 'w', 'utf-8')
Marco Brohet7165b4e2014-03-02 17:31:17 +0100122 else:
123 # Open translation files
Marco Brohet44657ed2014-03-04 22:49:23 +0100124 file_write = codecs.open(path + lang + '.js', 'w', 'utf-8')
Marco Brohet7165b4e2014-03-02 17:31:17 +0100125 xml_base = xml.dom.minidom.parse(path + lang + '.xml')
126 tags = xml_base.getElementsByTagName('string')
127
128 # Read each line of en.js
129 for a_line in content:
130 # Regex to determine string id
131 m = re.search(' (.*): [\'|\"]', a_line)
132 if m is not None:
133 for string_id in m.groups():
134 if string_id is not None:
135 # Find string id
136 string_id = string_id.replace(' ', '')
137 m2 = re.search('\'(.*)\'|"(.*)"', a_line)
138 # Find string contents
139 for string_content in m2.groups():
140 if string_content is not None:
141 break
142 if sync_type == 'upload':
143 # In upload mode, create the appropriate string element.
144 contents = doc.createElement('string')
145 contents.attributes['name'] = string_id
146 contents.appendChild(doc.createTextNode(string_content))
147 header.appendChild(contents)
148 else:
149 # In download mode, check if string_id matches a name attribute in the translation XML file.
150 # If it does, replace English text with the translation.
151 # If it does not, English text will remain and will be added to the file to retain the file structure.
152 for string in tags:
153 if string.attributes['name'].value == string_id:
154 a_line = a_line.replace(string_content.rstrip(), string.firstChild.nodeValue)
155 break
156 break
157 # In download mode do not write comments
158 if sync_type == 'download' and not '//' in a_line:
159 # Add language identifier (1)
160 if 'cmaccount.l10n.en' in a_line:
161 a_line = a_line.replace('l10n.en', 'l10n.' + lang)
162 # Add language identifier (2)
163 if 'l10n.add(\'en\'' in a_line:
164 a_line = a_line.replace('l10n.add(\'en\'', 'l10n.add(\'' + lang + '\'')
165 # Now write the line
166 file_write.write(a_line)
167
168
169 # Create XML file structure
170 if sync_type == 'upload':
171 header.appendChild(contents)
172 contents = header.toxml().replace('<string', '\n <string').replace('</resources>', '\n</resources>')
173 file_write.write('<?xml version="1.0" encoding="utf-8"?>\n')
174 file_write.write('<!-- .JS CONVERTED TO .XML - DO NOT MERGE THIS FILE -->\n')
175 file_write.write(contents)
176
177 # Close file
178 file_write.close()
179
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100180def push_as_commit(path, name):
181 # Get path
182 path = os.getcwd() + '/' + path
183
184 # Create git commit
185 repo = git.Repo(path)
186 repo.git.add(path)
187 try:
188 repo.git.commit(m='DO NOT MERGE: Automatic translation import test commit')
189# repo.git.push('ssh://cobjeM@review.cyanogenmod.org:29418/' + name, 'HEAD:refs/for/cm-11.0')
190 print 'Succesfully pushed commit for ' + name
191 except:
192 # If git commit fails, it's probably because of no changes.
193 # Just continue.
194 print 'No commit pushed (probably empty?) for ' + name
Michael Bestas7fd0f072014-03-29 20:29:13 +0200195 print 'WARNING: If the repository name was not obtained from android/default.xml, the name might be wrong!'
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100196
197print('Welcome to the CM Crowdin sync script!')
198
199print('\nSTEP 0: Checking dependencies')
Marco Brohet7165b4e2014-03-02 17:31:17 +0100200# Check for Ruby version of crowdin-cli
Marco Brohet4683bee2014-02-28 01:06:03 +0100201if subprocess.check_output(['rvm', 'all', 'do', 'gem', 'list', 'crowdin-cli', '-i']) == 'true':
202 sys.exit('You have not installed crowdin-cli. Terminating.')
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100203else:
204 print('Found: crowdin-cli')
Marco Brohet7165b4e2014-03-02 17:31:17 +0100205# Check for caf.xml
Marco Brohet4683bee2014-02-28 01:06:03 +0100206if not os.path.isfile('caf.xml'):
207 sys.exit('You have no caf.xml. Terminating.')
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100208else:
209 print('Found: caf.xml')
Michael Bestas7fd0f072014-03-29 20:29:13 +0200210# Check for android/default.xml
211if not os.path.isfile('android/default.xml'):
212 sys.exit('You have no android/default.xml. Terminating.')
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100213else:
Michael Bestas7fd0f072014-03-29 20:29:13 +0200214 print('Found: android/default.xml')
Marco Brohet7165b4e2014-03-02 17:31:17 +0100215# Check for repo
216try:
217 subprocess.check_output(['which', 'repo'])
218except:
219 sys.exit('You have not installed repo. Terminating.')
Marco Brohet4683bee2014-02-28 01:06:03 +0100220
Marco Brohet25623ce2014-03-08 19:13:07 +0100221print('\nSTEP 1: Create cm_caf.xml')
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100222# Load caf.xml
Marco Brohet25623ce2014-03-08 19:13:07 +0100223print('Loading caf.xml')
224xml = minidom.parse('caf.xml')
225items = xml.getElementsByTagName('item')
Marco Brohet4683bee2014-02-28 01:06:03 +0100226
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100227# Store all created cm_caf.xml files in here.
228# Easier to remove them afterwards, as they cannot be committed
Marco Brohet25623ce2014-03-08 19:13:07 +0100229cm_caf = []
Marco Brohet4683bee2014-02-28 01:06:03 +0100230
Marco Brohet25623ce2014-03-08 19:13:07 +0100231for item in items:
232 # Create tmp dir for download of AOSP base file
233 path_to_values = item.attributes["path"].value
234 subprocess.call(['mkdir', '-p', 'tmp/' + path_to_values])
235 # Create cm_caf.xml - header
236 f = codecs.open(path_to_values + '/cm_caf.xml', 'w', 'utf-8')
237 f.write('<?xml version="1.0" encoding="utf-8"?>\n')
Michael Bestasdb8462d2014-03-25 15:11:50 +0200238 f.write('<!--\n')
239 f.write(' Copyright (C) 2014 The CyanogenMod Project\n')
240 f.write('\n')
241 f.write(' Licensed under the Apache License, Version 2.0 (the "License");\n')
242 f.write(' you may not use this file except in compliance with the License.\n')
243 f.write(' You may obtain a copy of the License at\n')
244 f.write('\n')
245 f.write(' http://www.apache.org/licenses/LICENSE-2.0\n')
246 f.write('\n')
247 f.write(' Unless required by applicable law or agreed to in writing, software\n')
248 f.write(' distributed under the License is distributed on an "AS IS" BASIS,\n')
249 f.write(' WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n')
250 f.write(' See the License for the specific language governing permissions and\n')
251 f.write(' limitations under the License.\n')
252 f.write('-->\n')
Marco Brohet25623ce2014-03-08 19:13:07 +0100253 f.write('<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">\n')
254 # Create cm_caf.xml - contents
255 # This means we also support multiple base files (e.g. checking if strings.xml and arrays.xml are changed)
256 contents = []
257 item_aosp = item.getElementsByTagName('aosp')
258 for aosp_item in item_aosp:
259 url = aosp_item.firstChild.nodeValue
260 xml_file = aosp_item.attributes["file"].value
261 path_to_base = 'tmp/' + path_to_values + '/' + xml_file
262 path_to_cm = path_to_values + '/' + xml_file
263 urlretrieve(url, path_to_base)
264 contents = contents + get_caf_additions(path_to_base, path_to_cm)
265 for addition in contents:
266 f.write(addition + '\n')
267 # Create cm_caf.xml - the end
268 f.write('</resources>')
269 f.close()
270 cm_caf.append(path_to_values + '/cm_caf.xml')
271 print('Created ' + path_to_values + '/cm_caf.xml')
Marco Brohet4683bee2014-02-28 01:06:03 +0100272
273print('\nSTEP 2: Upload Crowdin source translations')
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100274# Execute 'crowdin-cli upload sources' and show output
Marco Brohet4683bee2014-02-28 01:06:03 +0100275print(subprocess.check_output(['crowdin-cli', 'upload', 'sources']))
276
Marco Brohetcf4069b2014-02-28 18:48:17 +0100277print('STEP 3: Download Crowdin translations')
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100278# Execute 'crowdin-cli download' and show output
Marco Brohetcf4069b2014-02-28 18:48:17 +0100279print(subprocess.check_output(['crowdin-cli', "download"]))
Marco Brohet4683bee2014-02-28 01:06:03 +0100280
Marco Brohet25623ce2014-03-08 19:13:07 +0100281print('STEP 4A: Clean up of source cm_caf.xmls')
282# Remove all cm_caf.xml files, which you can find in the list 'cm_caf'
283for cm_caf_file in cm_caf:
284 print ('Removing ' + cm_caf_file)
285 os.remove(cm_caf_file)
Marco Brohetcf4069b2014-02-28 18:48:17 +0100286
Marco Brohet25623ce2014-03-08 19:13:07 +0100287print('\nSTEP 4B: Clean up of temp dir')
288# We are done with cm_caf.xml files, so remove tmp/
289shutil.rmtree(os.getcwd() + '/tmp')
Marco Brohetcf4069b2014-02-28 18:48:17 +0100290
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100291print('\nSTEP 4C: Clean up of empty translations')
292# Some line of code that I found to find all XML files
Marco Brohet4683bee2014-02-28 01:06:03 +0100293result = [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']
294for xml_file in result:
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100295 # We hate empty, useless files. Crowdin exports them with <resources/> (sometimes with xliff).
296 # That means: easy to find
Marco Brohet4683bee2014-02-28 01:06:03 +0100297 if '<resources/>' in open(xml_file).read():
298 print ('Removing ' + xml_file)
299 os.remove(xml_file)
Marco Brohetcf4069b2014-02-28 18:48:17 +0100300 elif '<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"/>' in open(xml_file).read():
Marco Brohet4683bee2014-02-28 01:06:03 +0100301 print ('Removing ' + xml_file)
302 os.remove(xml_file)
303
Marco Brohet4683bee2014-02-28 01:06:03 +0100304print('\nSTEP 5: Push translations to Git')
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100305# Get all files that Crowdin pushed
Marco Brohet4683bee2014-02-28 01:06:03 +0100306proc = subprocess.Popen(['crowdin-cli', 'list', 'sources'],stdout=subprocess.PIPE)
Michael Bestas7fd0f072014-03-29 20:29:13 +0200307xml = minidom.parse('android/default.xml')
Marco Brohet4683bee2014-02-28 01:06:03 +0100308items = xml.getElementsByTagName('project')
Marco Brohetcf4069b2014-02-28 18:48:17 +0100309all_projects = []
Marco Brohet4683bee2014-02-28 01:06:03 +0100310
Marco Brohetcf4069b2014-02-28 18:48:17 +0100311for path in iter(proc.stdout.readline,''):
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100312 # Remove the \n at the end of each line
Marco Brohetcf4069b2014-02-28 18:48:17 +0100313 path = path.rstrip()
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100314 # Get project root dir from Crowdin's output
Marco Brohetcf4069b2014-02-28 18:48:17 +0100315 m = re.search('/(.*Superuser)/Superuser.*|/(.*LatinIME).*|/(frameworks/base).*|/(.*CMFileManager).*|/(device/.*/.*)/.*/res/values.*|/(hardware/.*/.*)/.*/res/values.*|/(.*)/res/values.*', path)
316 for good_path in m.groups():
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100317 # When a project has multiple translatable files, Crowdin will give duplicates.
318 # We don't want that (useless empty commits), so we save each project in all_projects
319 # and check if it's already in there.
Marco Brohetcf4069b2014-02-28 18:48:17 +0100320 if good_path is not None and not good_path in all_projects:
321 all_projects.append(good_path)
322 working = 'false'
323 for project_item in items:
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100324 # We need to have the Github repository for the git push url. Obtain them from
Michael Bestas7fd0f072014-03-29 20:29:13 +0200325 # android/default.xml based on the project root dir.
Marco Brohetcf4069b2014-02-28 18:48:17 +0100326 if project_item.attributes["path"].value == good_path:
327 working = 'true'
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100328 push_as_commit(good_path, project_item.attributes['name'].value)
Michael Bestas7fd0f072014-03-29 20:29:13 +0200329 print 'Committing ' + project_item.attributes['name'].value + ' (based on android/default.xml)'
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100330 # We also translate repositories that are not downloaded by default (e.g. device parts).
331 # This is just a fallback.
332 # WARNING: If the name is wrong, this will not stop the script.
Marco Brohetcf4069b2014-02-28 18:48:17 +0100333 if working == 'false':
Marco Brohet8b78a1b2014-02-28 21:01:26 +0100334 push_as_commit(good_path, 'CyanogenMod/android_' + good_path.replace('/', '_'))
335 print 'Committing ' + project_item.attributes['name'].value + ' (workaround)'
Marco Brohet4683bee2014-02-28 01:06:03 +0100336
337print('STEP 6: Done!')