#!/usr/bin/env ruby 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'], '%three_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, } placeholders = pattern.inject([]){ |memo, h| memo << h.first[/%(.*)%/, 1] } unless languages_mapping.nil? pattern = Hash[pattern.map{|placeholder, str| [ placeholder, (languages_mapping[placeholder[/%(.*)%/, 1]][lang['crowdin_code']] rescue nil) || str] }] end export_pattern.gsub(/%(#{placeholders.join('|')})%/, 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 def get_invalid_placeholders(export_pattern) valid_placeholders = [ '%language%', '%two_letters_code%', '%three_letters_code%', '%locale%', '%locale_with_underscore%', '%android_code%', '%original_file_name%', '%original_path%', '%file_extension%', '%file_name%' ] all_placeholders = export_pattern.scan(/%[a-z0-9_]*?%/) invalid_placeholders = all_placeholders - valid_placeholders 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 unmatched_files = [] Zip::ZipFile.open(zip) do |zip_file| zip_file.select{ |f| f.file? }.each do |f| file = files_list[f.name] if file f_path = File.join(dest, file) FileUtils.mkdir_p(File.dirname(f_path)) puts "Download: #{file}" zip_file.extract(f, f_path) else unmatched_files << f end end end unless unmatched_files.empty? puts "Warning: Downloaded translations does not match current project configuration. Some of the resulted files will be omitted." unmatched_files.each{ |file| puts " - `#{file}'" } puts "Crowdin has internal caching mechanisms that prevents us from overload. Please try to download translations later." end end ### include GLI::App version Crowdin::CLI::VERSION program_desc 'is a command line tool that allows you to manage and synchronize your localization resources with Crowdin project' program_long_desc 'This tool requires configuration file to be created. See http://crowdin.net/page/cli-client for more details.' sort_help :manually # help commands are ordered in the order declared wrap_help_text :to_terminal desc 'Be verbose' switch [:v, :verbose] desc 'Configuration file' default_value File.join(Dir.pwd, 'crowdin.yaml') arg_name '' flag [:c, :config] desc 'Allows you to upload source files and existing translations to Crowdin project' long_desc 'This command is used to upload source files and translations to Crowdin. This command is used in combination with sub-commands `sources` and `translations`.' command :upload do |c| c.desc 'safely upload source files to Crowdin' c.long_desc < '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| zipped_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) path = file.path begin @crowdin.download_translation(language, :output => path) unzip_file(path, @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 if File.exists?(global[:config]) @config = YAML.load_file(global[:config]) ['api_key', 'project_identifier'].each do |key| unless @config[key] exit_now! <