blob: d5addc1993eaa47815ca2170e7506bd4564d75d9 [file] [log] [blame]
Marco Brohetcb5cdb42014-07-11 22:41:53 +02001#!/usr/bin/python2
2# -*- coding: utf-8 -*-
Michael Bestas1ab959b2014-07-26 16:01:01 +03003# crowdin_sync.py
Marco Brohetcb5cdb42014-07-11 22:41:53 +02004#
5# Updates Crowdin source translations and pushes translations
6# directly to CyanogenMod's Gerrit.
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
Marco Brohet6b6b4e52014-07-20 00:05:16 +020022############################################# IMPORTS ##############################################
23
24import argparse
Marco Brohetcb5cdb42014-07-11 22:41:53 +020025import codecs
26import git
27import os
28import os.path
29import re
30import shutil
31import subprocess
32import sys
33from urllib import urlretrieve
34from xml.dom import minidom
35
Marco Brohet6b6b4e52014-07-20 00:05:16 +020036############################################ FUNCTIONS #############################################
37
Marco Brohet6b6b4e52014-07-20 00:05:16 +020038def get_default_branch(xml):
39 xml_default = xml.getElementsByTagName('default')[0]
40 xml_default_revision = xml_default.attributes['revision'].value
41 return re.search('refs/heads/(.*)', xml_default_revision).groups()[0]
42
Marco Brohet6b6b4e52014-07-20 00:05:16 +020043def push_as_commit(path, name, branch, username):
44 print('Committing ' + name + ' on branch ' + branch)
Marco Brohetcb5cdb42014-07-11 22:41:53 +020045
46 # Get path
47 path = os.getcwd() + '/' + path
48
Marco Brohet6b6b4e52014-07-20 00:05:16 +020049 # Create repo object
Marco Brohetcb5cdb42014-07-11 22:41:53 +020050 repo = git.Repo(path)
Marco Brohet6b6b4e52014-07-20 00:05:16 +020051
52 # Remove previously deleted files from Git
Marco Brohetcb5cdb42014-07-11 22:41:53 +020053 removed_files = repo.git.ls_files(d=True).split('\n')
54 try:
55 repo.git.rm(removed_files)
56 except:
57 pass
Marco Brohet6b6b4e52014-07-20 00:05:16 +020058
59 # Add all files to commit
Marco Brohetcb5cdb42014-07-11 22:41:53 +020060 repo.git.add('-A')
Marco Brohet6b6b4e52014-07-20 00:05:16 +020061
62 # Create commit; if it fails, probably empty so skipping
Marco Brohetcb5cdb42014-07-11 22:41:53 +020063 try:
64 repo.git.commit(m='Automatic translation import')
65 except:
66 print('Failed to create commit for ' + name + ', probably empty: skipping')
67 return
Marco Brohet6b6b4e52014-07-20 00:05:16 +020068
69 # Push commit
Chirayu Desaid49a47f2014-08-01 18:36:39 +053070 repo.git.push('ssh://' + username + '@review.cyanogenmod.org:29418/' + name, 'HEAD:refs/for/' + branch + '%topic=translation')
Marco Brohet6b6b4e52014-07-20 00:05:16 +020071
Marco Brohetcb5cdb42014-07-11 22:41:53 +020072 print('Succesfully pushed commit for ' + name)
73
Marco Brohet6b6b4e52014-07-20 00:05:16 +020074def sync_js_translations(sync_type, path, lang=''):
75 # lang is necessary in download mode
76 if sync_type == 'download' and lang == '':
77 sys.exit('Invalid syntax. Language code is required in download mode.')
78
79 # Read source en.js file. This is necessary for both upload and download modes
80 with codecs.open(path + 'en.js', 'r', 'utf-8') as f:
81 content = f.readlines()
82
83 if sync_type == 'upload':
84 # Prepare XML file structure
85 doc = minidom.Document()
86 header = doc.createElement('resources')
87 file_write = codecs.open(path + 'en.xml', 'w', 'utf-8')
88 else:
89 # Open translation files
90 file_write = codecs.open(path + lang + '.js', 'w', 'utf-8')
91 xml_base = minidom.parse(path + lang + '.xml')
92 tags = xml_base.getElementsByTagName('string')
93
94 # Read each line of en.js
95 for a_line in content:
96 # Regex to determine string id
97 m = re.search(' (.*): [\'|\"]', a_line)
98 if m is not None:
99 for string_id in m.groups():
100 if string_id is not None:
101 # Find string id
102 string_id = string_id.replace(' ', '')
103 m2 = re.search('\'(.*)\'|"(.*)"', a_line)
104 # Find string contents
105 for string_content in m2.groups():
106 if string_content is not None:
107 break
108 if sync_type == 'upload':
109 # In upload mode, create the appropriate string element.
110 contents = doc.createElement('string')
111 contents.attributes['name'] = string_id
112 contents.appendChild(doc.createTextNode(string_content))
113 header.appendChild(contents)
114 else:
115 # In download mode, check if string_id matches a name attribute in the translation XML file.
116 # If it does, replace English text with the translation.
117 # If it does not, English text will remain and will be added to the file to retain the file structure.
118 for string in tags:
119 if string.attributes['name'].value == string_id:
120 a_line = a_line.replace(string_content.rstrip(), string.firstChild.nodeValue)
121 break
122 break
123 # In download mode do not write comments
124 if sync_type == 'download' and not '//' in a_line:
125 # Add language identifier (1)
126 if 'cmaccount.l10n.en' in a_line:
127 a_line = a_line.replace('l10n.en', 'l10n.' + lang)
128 # Add language identifier (2)
129 if 'l10n.add(\'en\'' in a_line:
130 a_line = a_line.replace('l10n.add(\'en\'', 'l10n.add(\'' + lang + '\'')
131 # Now write the line
132 file_write.write(a_line)
133
134 # Create XML file structure
135 if sync_type == 'upload':
136 header.appendChild(contents)
137 contents = header.toxml().replace('<string', '\n <string').replace('</resources>', '\n</resources>')
138 file_write.write('<?xml version="1.0" encoding="utf-8"?>\n')
139 file_write.write('<!-- .JS CONVERTED TO .XML - DO NOT MERGE THIS FILE -->\n')
140 file_write.write(contents)
141
142 # Close file
143 file_write.close()
144
145###################################################################################################
146
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200147print('Welcome to the CM Crowdin sync script!')
148
Marco Brohet6b6b4e52014-07-20 00:05:16 +0200149###################################################################################################
150
151parser = argparse.ArgumentParser(description='Synchronising CyanogenMod\'s translations with Crowdin')
152parser.add_argument('--username', help='Gerrit username', required=True)
153#parser.add_argument('--upload-only', help='Only upload CM source translations to Crowdin', required=False)
154args = vars(parser.parse_args())
155
156username = args['username']
157
158############################################## STEP 0 ##############################################
159
160print('\nSTEP 0A: Checking dependencies')
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200161# Check for Ruby version of crowdin-cli
162if subprocess.check_output(['rvm', 'all', 'do', 'gem', 'list', 'crowdin-cli', '-i']) == 'true':
163 sys.exit('You have not installed crowdin-cli. Terminating.')
164else:
165 print('Found: crowdin-cli')
166
167# Check for repo
168try:
169 subprocess.check_output(['which', 'repo'])
170except:
171 sys.exit('You have not installed repo. Terminating.')
172
173# Check for android/default.xml
174if not os.path.isfile('android/default.xml'):
175 sys.exit('You have no android/default.xml. Terminating.')
176else:
177 print('Found: android/default.xml')
178
Michael Bestas55ae81a2014-07-26 19:22:19 +0300179# Check for crowdin/config_aosp.yaml
180if not os.path.isfile('crowdin/config_aosp.yaml'):
181 sys.exit('You have no crowdin/config_aosp.yaml. Terminating.')
182else:
183 print('Found: crowdin/config_aosp.yaml')
184
185# Check for crowdin/config_cm.yaml
186if not os.path.isfile('crowdin/config_cm.yaml'):
187 sys.exit('You have no crowdin/config_cm.yaml. Terminating.')
188else:
189 print('Found: crowdin/config_cm.yaml')
190
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200191# Check for crowdin/crowdin_aosp.yaml
192if not os.path.isfile('crowdin/crowdin_aosp.yaml'):
193 sys.exit('You have no crowdin/crowdin_aosp.yaml. Terminating.')
194else:
195 print('Found: crowdin/crowdin_aosp.yaml')
196
197# Check for crowdin/crowdin_cm.yaml
198if not os.path.isfile('crowdin/crowdin_cm.yaml'):
199 sys.exit('You have no crowdin/crowdin_cm.yaml. Terminating.')
200else:
201 print('Found: crowdin/crowdin_cm.yaml')
202
203# Check for crowdin/extra_packages.xml
204if not os.path.isfile('crowdin/extra_packages.xml'):
205 sys.exit('You have no crowdin/extra_packages.xml. Terminating.')
206else:
207 print('Found: crowdin/extra_packages.xml')
208
Marco Brohet6b6b4e52014-07-20 00:05:16 +0200209# Check for crowdin/js.xml
210if not os.path.isfile('crowdin/js.xml'):
211 sys.exit('You have no crowdin/js.xml. Terminating.')
212else:
213 print('Found: crowdin/js.xml')
214
215print('\nSTEP 0B: Define shared variables')
216
217# Variables regarding android/default.xml
218print('Loading: android/default.xml')
219xml_android = minidom.parse('android/default.xml')
220
Marco Brohet6b6b4e52014-07-20 00:05:16 +0200221# Variables regarding crowdin/js.xml
222print('Loading: crowdin/js.xml')
223xml_js = minidom.parse('crowdin/js.xml')
224items_js = xml_js.getElementsByTagName('project')
225
226# Default branch
227default_branch = get_default_branch(xml_android)
228print('Default branch: ' + default_branch)
229
Marco Brohet6b6b4e52014-07-20 00:05:16 +0200230############################################## STEP 1 ##############################################
231
Michael Bestas8bbcf612014-09-10 17:26:45 +0300232print('\nSTEP 1: Upload Crowdin source translations (non-AOSP supported languages)')
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200233# Execute 'crowdin-cli upload sources' and show output
Michael Bestas3cf378d2014-07-26 15:47:29 +0300234print(subprocess.check_output(['crowdin-cli', '--config=crowdin/crowdin_aosp.yaml', '--identity=crowdin/config_aosp.yaml', 'upload', 'sources']))
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200235
Michael Bestas8bbcf612014-09-10 17:26:45 +0300236############################################## STEP 2 ##############################################
Michael Bestas50579d22014-08-09 17:49:14 +0300237
Marco Brohet6b6b4e52014-07-20 00:05:16 +0200238# JS files cannot be translated easily on Crowdin. That's why they are uploaded as Android XML
239# files. At this time, the (English) JS source file is converted to an XML file, so Crowdin knows it
240# needs to download for it.
Michael Bestas8bbcf612014-09-10 17:26:45 +0300241#print('\nSTEP 2: Convert .js source translations to .xml')
Michael Bestas50579d22014-08-09 17:49:14 +0300242#
243#js_files = []
244#
245#for item in items_js:
246# path = item.attributes['path'].value + '/'
247# sync_js_translations('upload', path)
248# print('Converted: ' + path + 'en.js to en.xml')
249# js_files.append(path + 'en.js')
Marco Brohet6b6b4e52014-07-20 00:05:16 +0200250
Michael Bestas8bbcf612014-09-10 17:26:45 +0300251############################################## STEP 3 ##############################################
Michael Bestas50579d22014-08-09 17:49:14 +0300252
Michael Bestas8bbcf612014-09-10 17:26:45 +0300253print('\nSTEP 3: Upload Crowdin source translations (AOSP supported languages)')
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200254# Execute 'crowdin-cli upload sources' and show output
Michael Bestas3cf378d2014-07-26 15:47:29 +0300255print(subprocess.check_output(['crowdin-cli', '--config=crowdin/crowdin_cm.yaml', '--identity=crowdin/config_cm.yaml', 'upload', 'sources']))
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200256
Michael Bestas8bbcf612014-09-10 17:26:45 +0300257############################################## STEP 4 ##############################################
Michael Bestas50579d22014-08-09 17:49:14 +0300258
Michael Bestas8bbcf612014-09-10 17:26:45 +0300259print('\nSTEP 4A: Download Crowdin translations (AOSP supported languages)')
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200260# Execute 'crowdin-cli download' and show output
Michael Bestas3cf378d2014-07-26 15:47:29 +0300261print(subprocess.check_output(['crowdin-cli', '--config=crowdin/crowdin_cm.yaml', '--identity=crowdin/config_cm.yaml', 'download']))
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200262
Michael Bestas8bbcf612014-09-10 17:26:45 +0300263print('\nSTEP 4B: Download Crowdin translations (non-AOSP supported languages)')
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200264# Execute 'crowdin-cli download' and show output
Michael Bestas3cf378d2014-07-26 15:47:29 +0300265print(subprocess.check_output(['crowdin-cli', '--config=crowdin/crowdin_aosp.yaml', '--identity=crowdin/config_aosp.yaml', 'download']))
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200266
Michael Bestas8bbcf612014-09-10 17:26:45 +0300267############################################## STEP 5 ##############################################
Michael Bestas50579d22014-08-09 17:49:14 +0300268
Michael Bestas8bbcf612014-09-10 17:26:45 +0300269print('\nSTEP 5: Remove useless empty translations')
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200270# Some line of code that I found to find all XML files
271result = [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']
Marco Brohet6b6b4e52014-07-20 00:05:16 +0200272empty_contents = {'<resources/>', '<resources xmlns:android="http://schemas.android.com/apk/res/android"/>', '<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"/>', '<resources xmlns:android="http://schemas.android.com/apk/res/android" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2"/>'}
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200273for xml_file in result:
Marco Brohet6b6b4e52014-07-20 00:05:16 +0200274 for line in empty_contents:
275 if line in open(xml_file).read():
276 print('Removing ' + xml_file)
277 os.remove(xml_file)
278 break
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200279
Michael Bestas50579d22014-08-09 17:49:14 +0300280#for js_file in js_files:
281# print('Removing ' + js_file)
282# os.remove(js_file)
283
Michael Bestas8bbcf612014-09-10 17:26:45 +0300284############################################## STEP 6 ##############################################
Michael Bestas50579d22014-08-09 17:49:14 +0300285
Michael Bestas8bbcf612014-09-10 17:26:45 +0300286print('\nSTEP 6: Create a list of pushable translations')
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200287# Get all files that Crowdin pushed
Michael Bestas3cf378d2014-07-26 15:47:29 +0300288proc = subprocess.Popen(['crowdin-cli --config=crowdin/crowdin_cm.yaml --identity=crowdin/config_cm.yaml list sources && crowdin-cli --config=crowdin/crowdin_aosp.yaml --identity=crowdin/config_aosp.yaml list sources'], stdout=subprocess.PIPE, shell=True)
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200289proc.wait() # Wait for the above to finish
290
Michael Bestas8bbcf612014-09-10 17:26:45 +0300291############################################## STEP 7 ##############################################
Michael Bestas50579d22014-08-09 17:49:14 +0300292
Michael Bestas8bbcf612014-09-10 17:26:45 +0300293#print('\nSTEP 7: Convert JS-XML translations to their JS format')
Marco Brohet6b6b4e52014-07-20 00:05:16 +0200294#
295#for item in items_js:
296# path = item.attributes['path'].value
297# all_xml_files = [os.path.join(dp, f) for dp, dn, filenames in os.walk(os.getcwd() + '/' + path) for f in filenames if os.path.splitext(f)[1] == '.xml']
298# for xml_file in all_xml_files:
299# lang_code = os.path.splitext(xml_file)[0]
300# sync_js_translations('download', path, lang_code)
301# os.remove(xml_file)
302# os.remove(path + '/' + item.attributes['source'].value)
303#
Michael Bestas50579d22014-08-09 17:49:14 +0300304
Michael Bestas8bbcf612014-09-10 17:26:45 +0300305############################################## STEP 8 ##############################################
Michael Bestas50579d22014-08-09 17:49:14 +0300306
Michael Bestas8bbcf612014-09-10 17:26:45 +0300307print('\nSTEP 8: Commit to Gerrit')
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200308xml_extra = minidom.parse('crowdin/extra_packages.xml')
Marco Brohet6b6b4e52014-07-20 00:05:16 +0200309items = xml_android.getElementsByTagName('project')
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200310items += xml_extra.getElementsByTagName('project')
311all_projects = []
312
313for path in iter(proc.stdout.readline,''):
314 # Remove the \n at the end of each line
315 path = path.rstrip()
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200316
Marco Brohet6b6b4e52014-07-20 00:05:16 +0200317 if not path:
318 continue
319
320 # Get project root dir from Crowdin's output by regex
321 m = re.search('/(.*Superuser)/Superuser.*|/(.*LatinIME).*|/(frameworks/base).*|/(.*CMFileManager).*|/(.*CMHome).*|/(device/.*/.*)/.*/res/values.*|/(hardware/.*/.*)/.*/res/values.*|/(.*)/res/values.*', path)
322
323 if not m.groups():
324 # Regex result is empty, warn the user
325 print('WARNING: Cannot determine project root dir of [' + path + '], skipping')
326 continue
327
328 for i in m.groups():
329 if not i:
330 continue
331 result = i
332 break
333
334 if result in all_projects:
335 # Already committed for this project, go to next project
336 continue
337
338 # When a project has multiple translatable files, Crowdin will give duplicates.
339 # We don't want that (useless empty commits), so we save each project in all_projects
340 # and check if it's already in there.
341 all_projects.append(result)
342
343 # Search in android/default.xml or crowdin/extra_packages.xml for the project's name
344 for project_item in items:
345 if project_item.attributes['path'].value != result:
346 # No match found, go to next item
347 continue
348
Michael Bestas55ae81a2014-07-26 19:22:19 +0300349 # Define branch (custom branch if defined in xml file, otherwise the default one)
Marco Brohet6b6b4e52014-07-20 00:05:16 +0200350 if project_item.hasAttribute('revision'):
351 branch = project_item.attributes['revision'].value
352 else:
353 branch = default_branch
354
355 push_as_commit(result, project_item.attributes['name'].value, branch, username)
356
357############################################### DONE ###############################################
Michael Bestas50579d22014-08-09 17:49:14 +0300358
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200359print('\nDone!')