#!/usr/bin/env ruby require 'pp' require 'crowdin-cli' # Return +hierarchy+ of directories and files in Crowdin project # # +files+ - basically, it's project files details from API method `project_info` # def get_remote_files_hierarchy(files, root = '/', hierarchy = { dirs: [], files: [] }) files.each do |node| case node['node_type'] when 'directory' hierarchy[:dirs] << "#{root}#{node['name']}" get_remote_files_hierarchy(node['files'], root + node['name'] + '/', hierarchy) when 'file' hierarchy[:files] << "#{root}#{node['name']}" end end return hierarchy end # Return +hierarchy+ of local directories and files # # @params [Array] files a list of files in a local directory. # def get_local_files_hierarchy(files, hierarchy = { dirs: [], files: [] }) hierarchy[:files] = files dirs = files.inject([]) do |res, a| res << a.split('/').drop(1).inject([]) do |res, s| res << res.last.to_s + '/' + s end end dirs.map(&:pop) # delete last element from each array hierarchy[:dirs] = dirs.flatten.uniq return hierarchy end # @param [String] path relative path to file in Crowdin project # @param [String] export_pattern basically, is a file['translation'] from crowdin.yaml # @param [Hash] lang language information # @option lang [String] :name # @option lang [String] :crowdin_code # @option lang [String] :iso_639_1 # @option lang [String] :iso_639_3 # @option lang [String] :locale # def export_pattern_to_path(path, export_pattern, lang, languages_mapping = nil) original_path = File.dirname(path) original_file_name = File.basename(path) file_extension = File.extname(path)[1..-1] file_name = File.basename(path, File.extname(path)) pattern = { '%language%' => lang['name'], '%two_letters_code%' => lang['iso_639_1'], '%tree_letters_code%' => lang['iso_639_3'], '%locale%' => lang['locale'], '%locale_with_underscore%' => lang['locale'].gsub('-', '_'), '%android_code%' => android_locale_code(lang['locale']), '%original_file_name%' => original_file_name, '%original_path%' => original_path, '%file_extension%' => file_extension, '%file_name%' => file_name, } unless languages_mapping.nil? pattern = Hash[pattern.map{|k, str| [ k, (languages_mapping[k[/%(.*)%/, 1]][str] rescue nil) || str] }] end export_pattern.gsub(/%.+?%/, pattern) end def android_locale_code(locale_code) locale_code = case locale_code when 'he-IL' then 'iw-IL' when 'yi-DE' then 'ji-DE' when 'id-ID' then 'in-ID' else locale_code end return locale_code.sub('-', '-r') end # Return a string representing that part of the directory tree that is common to all the files # # @params [Array] paths set of strings representing directory paths # def find_common_directory_path(paths) return paths.first.split('/').slice(0...-1).join('/') if paths.length <= 1 arr = paths.sort first = arr.first.split('/') last = arr.last.split('/') i = 0 i += 1 while first[i] == last[i] && i <= first.length first.slice(0, i).join('/') end def unzip_file(zip, dest, files_list) # overwrite files if they already exist inside of the extracted path Zip.options[:on_exists_proc] = true Zip::ZipFile.open(zip) do |zip_file| zip_file.select{|f| f.file?}.each do |f| file = files_list[f.name] f_path = File.join(dest, file) FileUtils.mkdir_p(File.dirname(f_path)) puts "Download: #{file}" zip_file.extract(f, f_path) end end end ### include GLI::App program_desc 'A CLI to sync locale files with crowdin.net' version Crowdin::CLI::VERSION desc 'Be verbose' switch [:v, :verbose] desc 'Path to config file' default_value File.join(Dir.pwd, 'crowdin.yaml') arg_name '' flag [:c, :config] desc 'Upload existing translations to Crowdin project' #arg_name 'Describe arguments to upload here' command :upload do |c| # Command 'upload' requires a subcommand # #c.action do |global_options, options, args| # puts "upload command ran" #end #c.default_command :all #c.desc 'Upload source and translation files' #c.command :all do |c| # c.action do |global_options, options, args| # puts options # puts "`upload all` command ran" # end #end c.desc 'Upload source files' c.command :sources do |c| c.desc 'defines whether to add sources if there is the same file previously added' c.switch [:ignore_duplicates] c.action do |global_options, options, args| project_info = @crowdin.project_info source_language = project_info['details']['source_language']['code'] # Crowdin supported languages list supported_languages = @crowdin.supported_languages source_language = supported_languages.find{ |lang| lang['crowdin_code'] == source_language } remote_project_tree = get_remote_files_hierarchy(project_info['files']) local_files = [] dest_files = [] @config['files'].each do |file| if File.exist?("#{@base_path}#{file['source']}") dest_files << file['source'] local_files << { dest: file['source'], source: "#{@base_path}#{file['source']}", export_pattern: file['translation'] } else Dir.glob("#{@base_path}#{file['source']}").each do |source| dest = source.sub("#{@base_path}", '') # relative path in Crowdin dest_files << dest file_pattern = export_pattern_to_path(dest, file['translation'], source_language) diff = (dest.split('/') - file_pattern.split('/')).join('/') export_pattern = file['translation'].sub('**', diff) local_files << { dest: dest, source: source, export_pattern: export_pattern } end end end exit_now!("Nothing to upload") if local_files.empty? common_dir = find_common_directory_path(dest_files) local_project_tree = get_local_files_hierarchy(local_files.collect{ |h| h[:dest].sub(common_dir, '') }) local_files.each{ |file| file[:dest].sub!(common_dir, '') } # Create directory tree # create_dirs = local_project_tree[:dirs] - remote_project_tree[:dirs] create_dirs.each do |dir| puts "Create directory `#{dir}`" @crowdin.add_directory(dir) end unless options[:ignore_duplicates] # Update existing files in Crowdin project # # array containing elements common to the two arrays update_files = local_project_tree[:files] & remote_project_tree[:files] files_for_upload = local_files.select{ |file| update_files.include?(file[:dest]) } files_for_upload.each do |file| puts "Update file `#{file[:dest]}`" @crowdin.update_file([] << file) end end # Add new files to Crowdin project # add_files = local_project_tree[:files] - remote_project_tree[:files] files_for_add = local_files.select{ |file| add_files.include?(file[:dest]) } files_for_add.each do |file| puts "Add new file `#{file[:dest]}`" @crowdin.add_file([] << file) end end # action end # command c.desc 'Upload translation files' c.command :translations do |c| c.desc 'the language of translation you need' c.default_value 'all' c.arg_name 'language_code' c.flag [:l, :language] c.desc 'defines whether to add translation if there is the same translation previously added' c.switch [:import_duplicates] c.desc 'defines whether to add translation if it is equal to source string at Crowdin' c.switch [:import_eq_suggestions] c.desc 'mark uploaded translations as approved' c.switch [:auto_approve_imported] c.action do |global_options, options, args| language = options[:language] project_info = @crowdin.project_info remote_project_tree = get_remote_files_hierarchy(project_info['files']) if language == 'all' project_languages = project_info['languages'].collect{ |h| h['code'] } else project_languages = [] << language end supported_languages = @crowdin.supported_languages translation_languages = supported_languages.select{ |lang| project_languages.include?(lang['crowdin_code']) } source_language = project_info['details']['source_language']['code'] source_language = supported_languages.find{ |lang| lang['crowdin_code'] == source_language } translated_files = Hash.new{ |hash, key| hash[key] = Array.new } dest_files = [] @config['files'].each do |file| languages_mapping = file['languages_mapping'] if File.exists?("#{@base_path}#{file['source']}") dest = file['source'].sub("#{@base_path}", '') dest_files << dest translation_languages.each do |lang| source = export_pattern_to_path(dest, file['translation'], lang) translated_files[lang['crowdin_code']] << { source: "#{@base_path}#{source}", dest: dest } end else Dir.glob("#{@base_path}#{file['source']}").each do |source| dest = source.sub("#{@base_path}", '') # relative path in Crowdin dest_files << dest file_pattern = export_pattern_to_path(dest, file['translation'], source_language) diff = (dest.split('/') - file_pattern.split('/')).join('/') export_pattern = file['translation'].sub('**', diff) # !!! translation_languages.each do |lang| source = export_pattern_to_path(dest, export_pattern, lang, languages_mapping) translated_files[lang['crowdin_code']] << { source: "#{@base_path}#{source}", dest: dest } end end end # if end # @config['files'] exit_now!("Nothing to upload") if dest_files.empty? params = {} params[:import_duplicates] = options[:import_dublicates] ? 1 : 0 params[:import_eq_suggestions] = options[:import_eq_suggestions] ? 1 : 0 params[:auto_approve_imported] = options[:auto_approve_imported] ? 1 : 0 common_dir = find_common_directory_path(dest_files) translated_files.each do |language, files| files.each do |file| file[:dest].sub!(common_dir, '') if remote_project_tree[:files].include?(file[:dest]) if File.exist?(file[:source]) puts "Uploading #{file[:source].sub(@base_path, '')}" @crowdin.upload_translation([] << file, language, params) else puts "Local file #{file[:source]} not exists" end else # if source file not exist, don't upload translation puts "Skip #{file[:source].sub(@base_path, '')}" end end end end # action end # command end desc 'Download existing translations' #arg_name 'Describe arguments to download here' command :download do |c| c.desc 'the language of translation you need' c.arg_name 'language_code' c.flag :l, :language, :default_value => 'all' c.action do |global_options ,options, args| # use export API method before to download the most recent translations #@crowdin.export_translations language = options[:language] project_info = @crowdin.project_info remote_project_tree = get_remote_files_hierarchy(project_info['files']) if language == 'all' project_languages = project_info['languages'].collect{ |h| h['code'] } else project_languages = [] << language end supported_languages = @crowdin.supported_languages translation_languages = supported_languages.select{ |lang| project_languages.include?(lang['crowdin_code']) } source_language = project_info['details']['source_language']['code'] source_language = supported_languages.find{ |lang| lang['crowdin_code'] == source_language } # keys is all possible files in zip archive # values is resulted local files # usually they are equal downloadable_files = {} @config['files'].each do |file| languages_mapping = file['languages_mapping'] #Hash or NilClass if File.exists?("#{@base_path}#{file['source']}") dest = file['source'].sub("#{@base_path}", '') translation_languages.each do |lang| zipper_file = export_pattern_to_path(dest, file['translation'], lang) local_file = export_pattern_to_path(dest, file['translation'], lang, languages_mapping) downloadable_files[zipped_file] = local_file end else Dir.glob("#{@base_path}#{file['source']}").each do |source| dest = source.sub("#{@base_path}", '') # relative path in Crowdin file_pattern = export_pattern_to_path(dest, file['translation'], source_language) diff = (dest.split('/') - file_pattern.split('/')).join('/') export_pattern = file['translation'].sub('**', diff) translation_languages.each do |lang| zipped_file = export_pattern_to_path(dest, export_pattern, lang) local_file = export_pattern_to_path(dest, export_pattern, lang, languages_mapping) downloadable_files[zipped_file] = local_file end end end # if end # @config['files'] ## file = Tempfile.new(language) begin @crowdin.download_translation(language, :output => file) unzip_file(file, @base_path, downloadable_files) ensure file.close file.unlink # delete the temp file end end end pre do |global ,command, options, args| # Pre logic here # Return true to proceed; false to abourt and not call the # chosen command # Use skips_pre before a command to skip this block # on that command only @config = YAML.load_file(global[:config]) #@base_path = @config['base_path'] || Dir.pwd if @config['base_path'] if @config['base_path'].start_with?('/') @base_path = @config['base_path'] else @base_path = Dir.pwd + '/' + @config['base_path'] end else @base_path = Dir.pwd end Crowdin::API.log = Logger.new($stderr) if global[:v] @crowdin = Crowdin::API.new(api_key: @config['api_key'], project_id: @config['project_id'], base_url: @config['base_url'] || 'http://api.crowdin.net') puts "Executing #{command.name}" if global[:v] true end post do |global, command, options, args| # Post logic here # Use skips_post before a command to skip this # block on that command only puts "Executed #{command.name}" if global[:v] end on_error do |exception| # Error logic here # return false to skip default error handling true end exit run(ARGV)