blob: 8f831fbfec81bb006a46d0485dd2f421359812dc [file] [log] [blame]
Michael Bestasc899b8c2015-03-03 00:53:19 +02001#!/usr/bin/python2
Marco Brohetcb5cdb42014-07-11 22:41:53 +02002# -*- 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#
Michael Bestas97677e12015-02-08 13:11:59 +02008# Copyright (C) 2014-2015 The CyanogenMod Project
Marco Brohetcb5cdb42014-07-11 22:41:53 +02009#
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
Michael Bestasc899b8c2015-03-03 00:53:19 +020022############################################# IMPORTS ##############################################
Marco Brohet6b6b4e52014-07-20 00:05:16 +020023
24import argparse
Michael Bestasc899b8c2015-03-03 00:53:19 +020025import codecs
Marco Brohetcb5cdb42014-07-11 22:41:53 +020026import git
27import os
Michael Bestasc899b8c2015-03-03 00:53:19 +020028import os.path
29import re
30import shutil
Marco Brohetcb5cdb42014-07-11 22:41:53 +020031import subprocess
32import sys
Michael Bestasc899b8c2015-03-03 00:53:19 +020033from urllib import urlretrieve
Marco Brohetcb5cdb42014-07-11 22:41:53 +020034from xml.dom import minidom
35
Michael Bestasc899b8c2015-03-03 00:53:19 +020036############################################ FUNCTIONS #############################################
Marco Brohet6b6b4e52014-07-20 00:05:16 +020037
Marco Brohet6b6b4e52014-07-20 00:05:16 +020038def push_as_commit(path, name, branch, username):
Michael Bestasc899b8c2015-03-03 00:53:19 +020039 print('Committing ' + name + ' on branch ' + branch)
Marco Brohetcb5cdb42014-07-11 22:41:53 +020040
41 # Get path
Michael Bestasc899b8c2015-03-03 00:53:19 +020042 path = os.getcwd() + '/' + path
Marco Brohetcb5cdb42014-07-11 22:41:53 +020043
Marco Brohet6b6b4e52014-07-20 00:05:16 +020044 # Create repo object
Marco Brohetcb5cdb42014-07-11 22:41:53 +020045 repo = git.Repo(path)
Marco Brohet6b6b4e52014-07-20 00:05:16 +020046
47 # Remove previously deleted files from Git
Michael Bestasc899b8c2015-03-03 00:53:19 +020048 removed_files = repo.git.ls_files(d=True).split('\n')
49 try:
50 repo.git.rm(removed_files)
51 except:
52 pass
Marco Brohet6b6b4e52014-07-20 00:05:16 +020053
54 # Add all files to commit
Marco Brohetcb5cdb42014-07-11 22:41:53 +020055 repo.git.add('-A')
Marco Brohet6b6b4e52014-07-20 00:05:16 +020056
57 # Create commit; if it fails, probably empty so skipping
Marco Brohetcb5cdb42014-07-11 22:41:53 +020058 try:
59 repo.git.commit(m='Automatic translation import')
60 except:
Michael Bestasc899b8c2015-03-03 00:53:19 +020061 print('Failed to create commit for ' + name + ', probably empty: skipping')
Marco Brohetcb5cdb42014-07-11 22:41:53 +020062 return
Marco Brohet6b6b4e52014-07-20 00:05:16 +020063
64 # Push commit
Michael Bestasf96f67b2014-10-21 00:43:37 +030065 try:
Michael Bestasc899b8c2015-03-03 00:53:19 +020066 repo.git.push('ssh://' + username + '@review.cyanogenmod.org:29418/' + name, 'HEAD:refs/for/' + branch + '%topic=translation')
67 print('Succesfully pushed commit for ' + name)
Michael Bestasf96f67b2014-10-21 00:43:37 +030068 except:
Michael Bestasc899b8c2015-03-03 00:53:19 +020069 print('Failed to push commit for ' + name)
Marco Brohetcb5cdb42014-07-11 22:41:53 +020070
Michael Bestasc899b8c2015-03-03 00:53:19 +020071def run_command(cmd):
Michael Bestas97677e12015-02-08 13:11:59 +020072 p = subprocess.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr)
73 ret = p.wait()
74 if ret != 0:
Michael Bestasc899b8c2015-03-03 00:53:19 +020075 print('Failed to run cmd: %s' % ' '.join(cmd))
Michael Bestas97677e12015-02-08 13:11:59 +020076 sys.exit(ret)
77
Michael Bestasc899b8c2015-03-03 00:53:19 +020078####################################################################################################
Marco Brohet6b6b4e52014-07-20 00:05:16 +020079
Michael Bestasc899b8c2015-03-03 00:53:19 +020080parser = argparse.ArgumentParser(description='Synchronising CyanogenMod\'s translations with Crowdin')
81sync = parser.add_mutually_exclusive_group()
82parser.add_argument('-u', '--username', help='Gerrit username', required=True)
83parser.add_argument('-b', '--branch', help='CyanogenMod branch', required=True)
84sync.add_argument('--no-upload', action='store_true', help='Only download CM translations from Crowdin')
85sync.add_argument('--no-download', action='store_true', help='Only upload CM source translations to Crowdin')
86args = parser.parse_args()
87argsdict = vars(args)
Marco Brohet6b6b4e52014-07-20 00:05:16 +020088
Michael Bestasc899b8c2015-03-03 00:53:19 +020089username = argsdict['username']
90default_branch = argsdict['branch']
Marco Brohet6b6b4e52014-07-20 00:05:16 +020091
Michael Bestasc899b8c2015-03-03 00:53:19 +020092####################################################################################################
Michael Bestas6b6db122015-02-08 13:22:22 +020093
Michael Bestasc899b8c2015-03-03 00:53:19 +020094print('Welcome to the CM Crowdin sync script!')
Michael Bestas6b6db122015-02-08 13:22:22 +020095
Michael Bestasc899b8c2015-03-03 00:53:19 +020096############################################# PREPARE ##############################################
Marco Brohet6b6b4e52014-07-20 00:05:16 +020097
Michael Bestasc899b8c2015-03-03 00:53:19 +020098print('\nSTEP 0: Checking dependencies & define shared variables')
99# Check for Ruby version of crowdin-cli
100if subprocess.check_output(['rvm', 'all', 'do', 'gem', 'list', 'crowdin-cli', '-i']) == 'true':
101 sys.exit('You have not installed crowdin-cli. Terminating.')
102else:
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200103 print('Found: crowdin-cli')
104
Michael Bestasc899b8c2015-03-03 00:53:19 +0200105# Check for repo
106try:
107 subprocess.check_output(['which', 'repo'])
108except:
109 sys.exit('You have not installed repo. Terminating.')
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200110
Michael Bestasc899b8c2015-03-03 00:53:19 +0200111# Check for android/default.xml
112if not os.path.isfile('android/default.xml'):
113 sys.exit('You have no android/default.xml. Terminating.')
114else:
115 print('Found: android/default.xml')
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200116
Michael Bestasc899b8c2015-03-03 00:53:19 +0200117# Variables regarding android/default.xml
118print('Loading: android/default.xml')
119xml_android = minidom.parse('android/default.xml')
Marco Brohet6b6b4e52014-07-20 00:05:16 +0200120
Michael Bestasc899b8c2015-03-03 00:53:19 +0200121# Check for crowdin/extra_packages_' + default_branch + '.xml
122if not os.path.isfile('crowdin/extra_packages_' + default_branch + '.xml'):
123 sys.exit('You have no crowdin/extra_packages_' + default_branch + '.xml. Terminating.')
124else:
125 print('Found: crowdin/extra_packages_' + default_branch + '.xml')
Michael Bestas4b26c4e2014-10-23 23:21:59 +0300126
Michael Bestasc899b8c2015-03-03 00:53:19 +0200127# Check for crowdin/config.yaml
128if not os.path.isfile('crowdin/config.yaml'):
129 sys.exit('You have no crowdin/config.yaml. Terminating.')
130else:
131 print('Found: crowdin/config.yaml')
Michael Bestas4b26c4e2014-10-23 23:21:59 +0300132
Michael Bestasc899b8c2015-03-03 00:53:19 +0200133# Check for crowdin/config_aosp.yaml
134if not os.path.isfile('crowdin/config_aosp.yaml'):
135 sys.exit('You have no crowdin/config_aosp.yaml. Terminating.')
136else:
137 print('Found: crowdin/config_aosp.yaml')
Michael Bestas4b26c4e2014-10-23 23:21:59 +0300138
Michael Bestasc899b8c2015-03-03 00:53:19 +0200139# Check for crowdin/crowdin_' + default_branch + '.yaml
140if not os.path.isfile('crowdin/crowdin_' + default_branch + '.yaml'):
141 sys.exit('You have no crowdin/crowdin_' + default_branch + '.yaml. Terminating.')
142else:
143 print('Found: crowdin/crowdin_' + default_branch + '.yaml')
Michael Bestas4b26c4e2014-10-23 23:21:59 +0300144
Michael Bestasc899b8c2015-03-03 00:53:19 +0200145# Check for crowdin/crowdin_' + default_branch + '_aosp.yaml
146if not os.path.isfile('crowdin/crowdin_' + default_branch + '_aosp.yaml'):
147 sys.exit('You have no crowdin/crowdin_' + default_branch + '_aosp.yaml. Terminating.')
148else:
149 print('Found: crowdin/crowdin_' + default_branch + '_aosp.yaml')
150
151############################################### MAIN ###############################################
152
153if not args.no_upload:
Michael Bestas919053f2014-10-20 23:30:54 +0300154 print('\nSTEP 1: Upload Crowdin source translations')
Michael Bestasc899b8c2015-03-03 00:53:19 +0200155 print('Uploading Crowdin source translations (AOSP supported languages)')
Michael Bestas4b26c4e2014-10-23 23:21:59 +0300156 # Execute 'crowdin-cli upload sources' and show output
Michael Bestasc899b8c2015-03-03 00:53:19 +0200157 run_command(['crowdin-cli', '--config=crowdin/crowdin_' + default_branch + '.yaml', '--identity=crowdin/config.yaml', 'upload', 'sources'])
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200158
Michael Bestasc899b8c2015-03-03 00:53:19 +0200159 print('\nUploading Crowdin source translations (non-AOSP supported languages)')
Anthony King69a95382015-02-08 18:44:10 +0000160 # Execute 'crowdin-cli upload sources' and show output
Michael Bestasc899b8c2015-03-03 00:53:19 +0200161 run_command(['crowdin-cli', '--config=crowdin/crowdin_' + default_branch + '_aosp.yaml', '--identity=crowdin/config_aosp.yaml', 'upload', 'sources'])
162else:
163 print('\nSkipping source translations upload')
Anthony King69a95382015-02-08 18:44:10 +0000164
Michael Bestasc899b8c2015-03-03 00:53:19 +0200165if not args.no_download:
Michael Bestas919053f2014-10-20 23:30:54 +0300166 print('\nSTEP 2: Download Crowdin translations')
Michael Bestasc899b8c2015-03-03 00:53:19 +0200167 print('Downloading Crowdin translations (AOSP supported languages)')
Michael Bestas919053f2014-10-20 23:30:54 +0300168 # Execute 'crowdin-cli download' and show output
Michael Bestasc899b8c2015-03-03 00:53:19 +0200169 run_command(['crowdin-cli', '--config=crowdin/crowdin_' + default_branch + '.yaml', '--identity=crowdin/config.yaml', 'download'])
Michael Bestas50579d22014-08-09 17:49:14 +0300170
Michael Bestasa02eb4b2015-02-08 15:47:01 +0200171 print('\nDownloading Crowdin translations (non-AOSP supported languages)')
Michael Bestas919053f2014-10-20 23:30:54 +0300172 # Execute 'crowdin-cli download' and show output
Michael Bestasc899b8c2015-03-03 00:53:19 +0200173 run_command(['crowdin-cli', '--config=crowdin/crowdin_' + default_branch + '_aosp.yaml', '--identity=crowdin/config_aosp.yaml', 'download'])
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200174
Michael Bestas919053f2014-10-20 23:30:54 +0300175 print('\nSTEP 3: Remove useless empty translations')
Michael Bestasc899b8c2015-03-03 00:53:19 +0200176 # Some line of code that I found to find all XML files
177 result = [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']
178 empty_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"/>'}
179 for xml_file in result:
Michael Bestas919053f2014-10-20 23:30:54 +0300180 for line in empty_contents:
Michael Bestasc899b8c2015-03-03 00:53:19 +0200181 if line in open(xml_file).read():
Michael Bestas919053f2014-10-20 23:30:54 +0300182 print('Removing ' + xml_file)
183 os.remove(xml_file)
184 break
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200185
Michael Bestas919053f2014-10-20 23:30:54 +0300186 print('\nSTEP 4: Create a list of pushable translations')
187 # Get all files that Crowdin pushed
Michael Bestasc899b8c2015-03-03 00:53:19 +0200188 proc = subprocess.Popen(['crowdin-cli --config=crowdin/crowdin_' + default_branch + '.yaml --identity=crowdin/config.yaml list sources | grep "' + default_branch + '" | sed "s#/' + default_branch + '##g" && crowdin-cli --config=crowdin/crowdin_' + default_branch + '_aosp.yaml --identity=crowdin/config_aosp.yaml list sources | grep "' + default_branch + '" | sed "s#/' + default_branch + '##g"'], stdout=subprocess.PIPE, shell=True)
189 proc.wait() # Wait for the above to finish
Michael Bestas50579d22014-08-09 17:49:14 +0300190
Michael Bestas919053f2014-10-20 23:30:54 +0300191 print('\nSTEP 5: Upload to Gerrit')
Michael Bestasc899b8c2015-03-03 00:53:19 +0200192 xml_extra = minidom.parse('crowdin/extra_packages_' + default_branch + '.xml')
193 items = xml_android.getElementsByTagName('project')
194 items += xml_extra.getElementsByTagName('project')
Michael Bestas919053f2014-10-20 23:30:54 +0300195 all_projects = []
196
Michael Bestasc899b8c2015-03-03 00:53:19 +0200197 for path in iter(proc.stdout.readline,''):
198 # Remove the \n at the end of each line
199 path = path.rstrip()
200
Michael Bestas919053f2014-10-20 23:30:54 +0300201 if not path:
202 continue
203
Michael Bestasc899b8c2015-03-03 00:53:19 +0200204 # Get project root dir from Crowdin's output by regex
205 m = re.search('/(.*Superuser)/Superuser.*|/(.*LatinIME).*|/(frameworks/base).*|/(.*CMFileManager).*|/(.*CMHome).*|/(device/.*/.*)/.*/res/values.*|/(hardware/.*/.*)/.*/res/values.*|/(.*)/res/values.*', path)
206
207 if not m.groups():
208 # Regex result is empty, warn the user
209 print('WARNING: Cannot determine project root dir of [' + path + '], skipping')
Anthony King69a95382015-02-08 18:44:10 +0000210 continue
Marco Brohetcb5cdb42014-07-11 22:41:53 +0200211
Michael Bestasc899b8c2015-03-03 00:53:19 +0200212 for i in m.groups():
213 if not i:
Michael Bestas919053f2014-10-20 23:30:54 +0300214 continue
Michael Bestasc899b8c2015-03-03 00:53:19 +0200215 result = i
Anthony King69a95382015-02-08 18:44:10 +0000216 break
Marco Brohet6b6b4e52014-07-20 00:05:16 +0200217
Michael Bestasc899b8c2015-03-03 00:53:19 +0200218 if result in all_projects:
219 # Already committed for this project, go to next project
220 continue
Michael Bestas50579d22014-08-09 17:49:14 +0300221
Michael Bestasc899b8c2015-03-03 00:53:19 +0200222 # When a project has multiple translatable files, Crowdin will give duplicates.
223 # We don't want that (useless empty commits), so we save each project in all_projects
224 # and check if it's already in there.
225 all_projects.append(result)
Anthony King69a95382015-02-08 18:44:10 +0000226
Michael Bestasc899b8c2015-03-03 00:53:19 +0200227 # Search in android/default.xml or crowdin/extra_packages_' + default_branch + '.xml for the project's name
228 for project_item in items:
229 if project_item.attributes['path'].value != result:
230 # No match found, go to next item
231 continue
Anthony King69a95382015-02-08 18:44:10 +0000232
Michael Bestasc899b8c2015-03-03 00:53:19 +0200233 # Define branch (custom branch if defined in xml file, otherwise the default one)
234 if project_item.hasAttribute('revision'):
235 branch = project_item.attributes['revision'].value
236 else:
237 branch = default_branch
Anthony King69a95382015-02-08 18:44:10 +0000238
Michael Bestasc899b8c2015-03-03 00:53:19 +0200239 push_as_commit(result, project_item.attributes['name'].value, branch, username)
240else:
241 print('\nSkipping translations download')
Anthony King69a95382015-02-08 18:44:10 +0000242
Michael Bestasc899b8c2015-03-03 00:53:19 +0200243############################################### DONE ###############################################
Anthony King69a95382015-02-08 18:44:10 +0000244
Michael Bestasc899b8c2015-03-03 00:53:19 +0200245print('\nDone!')