bin/crowdin-cli in crowdin-cli-0.4.1 vs bin/crowdin-cli in crowdin-cli-0.4.2

- old
+ new

@@ -74,10 +74,11 @@ '%three_letters_code%' => lang['iso_639_3'], '%locale%' => lang['locale'], '%locale_with_underscore%' => lang['locale'].gsub('-', '_'), '%android_code%' => android_locale_code(lang['locale']), '%osx_code%' => osx_language_code(lang['crowdin_code']) + '.lproj', + '%osx_xliff%' => osx_language_code(lang['crowdin_code']) + '.xliff', } placeholders = pattern.inject([]){ |memo, h| memo << h.first[/%(.*)%/, 1] } unless languages_mapping.nil? @@ -201,10 +202,11 @@ '%three_letters_code%', '%locale%', '%locale_with_underscore%', '%android_code%', '%osx_code%', + '%osx_liff%', '%original_file_name%', '%original_path%', '%file_extension%', '%file_name%', ] @@ -233,22 +235,22 @@ end end # Extract compressed files +files_list+ in a ZIP archive +zipfile_name+ to +dest_path+ # -# +files_list+ is a Hash of key-value pairs. Where key is a posible archive filename based on current project configuration +# +files_list+ is a Hash of key-value pairs. Where key is a possible archive filename based on current project configuration # and value is the expanded filename # def unzip_file_with_translations(zipfile_name, dest_path, files_list) # overwrite files if they already exist inside of the extracted path Zip.on_exists_proc = true # files that exists in archive and doesn't match current project configuration unmatched_files = [] Zip::File.open(zipfile_name) do |zipfile| - zipfile.select{ |zip_entry| zip_entry.file? }.each do |f| + zipfile.select { |zip_entry| zip_entry.file? }.each do |f| # `f' - relative path in archive file = files_list[f.name] if file fpath = File.join(dest_path, file) FileUtils.mkdir_p(File.dirname(fpath)) @@ -314,10 +316,17 @@ display_tree(val, level, branches) end end end +class String + def strip_heredoc + indent = scan(/^[ \t]*(?=\S)/).min.size || 0 + gsub(/^[ \t]{#{indent}}/, '') + end +end + ### include GLI::App version Crowdin::CLI::VERSION @@ -327,11 +336,11 @@ program_long_desc I18n.t('app.long_desc') sort_help :manually # help commands are ordered in the order declared wrap_help_text :to_terminal desc I18n.t('app.switches.verbose.desc') -switch [:v, :verbose], :negatable => false +switch [:v, :verbose], negatable: false desc I18n.t('app.flags.config.desc') default_value File.join(Dir.pwd, 'crowdin.yaml') arg_name '<s>' flag [:c, :config] @@ -387,11 +396,11 @@ end local_files << local_file else Find.find(@base_path) do |source_path| - dest = source_path.sub(/\A#{@base_path}/, '') # relative path in Crowdin + dest = source_path.sub(/\A#{Regexp.escape(@base_path)}/, '') # relative path in Crowdin if File.directory?(source_path) if ignores.any? { |pattern| File.fnmatch?(pattern, dest, File::FNM_PATHNAME) } Find.prune # Don't look any further into this directory else @@ -416,15 +425,15 @@ end # if File.exists? end # @config['files'] if dest_files.empty? - exit_now! <<EOS -No source files to upload. -Check your configuration file to ensure that they contain valid directives. -See http://crowdin.com/page/cli-tool#configuration-file for more details. -EOS + exit_now! <<-EOS.strip_heredoc + No source files to upload. + Check your configuration file to ensure that they contain valid directives. + See http://crowdin.com/page/cli-tool#configuration-file for more details. + EOS end common_dir = @preserve_hierarchy ? '' : find_common_directory_path(dest_files) local_project_tree = get_local_files_hierarchy(local_files.collect { |h| h[:dest].sub(common_dir, '') }) @@ -433,11 +442,11 @@ # Create directory tree # create_dirs = local_project_tree[:dirs] - remote_project_tree[:dirs] create_dirs.each do |dir| - puts "Create directory `#{dir}`" + puts "Creating directory `#{dir}`" @crowdin.add_directory(dir) end if options['auto-update'] # Update existing files in Crowdin project @@ -522,11 +531,11 @@ # do nothing else if project_languages.include?(language) project_languages = [] << language else - exit_now!("language '#{language}' doesn't exist in a project") + exit_now!("language '#{language}' doesn't exist in the project") end end supported_languages = @crowdin.supported_languages @@ -555,20 +564,21 @@ else file_translation_languages = translation_languages end if File.exists?(File.join(@base_path, file['source'])) - dest = file['source'].sub(/\A#{@base_path}/, '') + dest = file['dest'] || file['source'] + dest.sub!(/\A#{Regexp.escape(@base_path)}/, '') dest_files << dest file_translation_languages.each do |lang| source = export_pattern_to_path(dest, file['translation'], lang, languages_mapping) translated_files[lang['crowdin_code']] << { source: File.join(@base_path, source), dest: dest } end else Find.find(@base_path) do |source_path| - dest = source_path.sub(/\A#{@base_path}/, '') # relative path in Crowdin + dest = source_path.sub(/\A#{Regexp.escape(@base_path)}/, '') # relative path in Crowdin if File.directory?(source_path) if ignores.any? { |pattern| File.fnmatch?(pattern, dest, File::FNM_PATHNAME) } Find.prune # Don't look any further into this directory else @@ -591,42 +601,42 @@ end # if end # @config['files'] if dest_files.empty? - exit_now! <<EOS -No translation files to upload. -Check your configuration file to ensure that they contain valid directives. -See http://crowdin.com/page/cli-tool#configuration-file for more details. -EOS + exit_now! <<-EOS.strip_heredoc + No translation files to upload. + Check your configuration file to ensure that they contain valid directives. + See https://crowdin.com/page/cli-tool#configuration-file for more details. + EOS end common_dir = @preserve_hierarchy ? '' : find_common_directory_path(dest_files) translated_files.each_pair do |language, files| files.each do |file| file[:dest] = file[:dest].sub(common_dir, '') if remote_project_tree[:files].include?(file[:dest]) if File.exist?(file[:source]) - print "Uploading translation file `#{file[:source].sub(/\A#{@base_path}/, '')}'" + print "Uploading translation file `#{file[:source].sub(/\A#{Regexp.escape(@base_path)}/, '')}'" resp = @crowdin.upload_translation([] << file, language, params) case resp['files'].first[1] when 'skipped' - puts "\rUploading translation file `#{file[:source].sub(/\A#{@base_path}/, '')}' - Skipped" + puts "\rUploading translation file `#{file[:source].sub(/\A#{Regexp.escape(@base_path)}/, '')}' - Skipped" when 'uploaded' - puts "\rUploading translation file `#{file[:source].sub(/\A#{@base_path}/, '')}' - OK" + puts "\rUploading translation file `#{file[:source].sub(/\A#{Regexp.escape(@base_path)}/, '')}' - OK" when 'not_allowed' - puts "\rUploading translation file `#{file[:source].sub(/\A#{@base_path}/, '')}' - is not possible" + puts "\rUploading translation file `#{file[:source].sub(/\A#{Regexp.escape(@base_path)}/, '')}' - is not possible" end else puts "Warning: Local file `#{file[:source]}' does not exist" end else # if source file does not exist, don't upload translations - puts "Warning: Skip `#{file[:source].sub(/\A#{@base_path}/, '')}'" + puts "Warning: Skip `#{file[:source].sub(/\A#{Regexp.escape(@base_path)}/, '')}'. Translation can not be uploaded for a non-existent source file `#{file[:dest]}'. Please upload sources first." end end end end # action @@ -639,11 +649,11 @@ command :download do |c| c.desc I18n.t('app.commands.download.flags.language.desc') c.long_desc I18n.t('app.commands.download.flags.language.long_desc') c.arg_name 'language_code' - c.flag [:l, :language], :default_value => 'all' + c.flag [:l, :language], default_value: 'all' c.action do |global_options ,options, args| language = options[:language] supported_languages = @crowdin.supported_languages @@ -667,11 +677,11 @@ exit_now!("language '#{language}' doesn't exist in a project") end end # use export API method before to download the most recent translations - print 'Build ZIP archive with the latest translations ' + print 'Building ZIP archive with the latest translations ' export_translations = @crowdin.export_translations if export_translations['success'] if export_translations['success']['status'] == 'built' puts "- OK" elsif export_translations['success']['status'] == 'skipped' @@ -702,22 +712,22 @@ else file_translation_languages = translation_languages end if File.exists?(File.join(@base_path, file['source'])) - dest = file['source'].sub(/\A#{@base_path}/, '') + dest = file['source'].sub(/\A#{Regexp.escape(@base_path)}/, '') file_translation_languages.each do |lang| zipped_file = export_pattern_to_path(dest, file['translation'], lang) zipped_file.sub!(/^\//, '') local_file = export_pattern_to_path(dest, file['translation'], lang, languages_mapping) downloadable_files_hash[zipped_file] = local_file end else Find.find(@base_path) do |source_path| - dest = source_path.sub(/\A#{@base_path}/, '') # relative path in Crowdin + dest = source_path.sub(/\A#{Regexp.escape(@base_path)}/, '') # relative path in Crowdin if File.directory?(source_path) if ignores.any? { |pattern| File.fnmatch?(pattern, dest, File::FNM_PATHNAME) } Find.prune # Don't look any further into this directory else @@ -760,11 +770,11 @@ command :list do |ls_cmd| ls_cmd.desc I18n.t('app.commands.list.commands.project.desc') ls_cmd.command :project do |proj_cmd| proj_cmd.desc I18n.t('app.commands.list.switches.tree.desc') - proj_cmd.switch ['tree'], :negatable => false + proj_cmd.switch ['tree'], negatable: false proj_cmd.action do |global_options, options, args| project_info = @crowdin.project_info remote_project_tree = get_remote_files_hierarchy(project_info['files']) @@ -778,11 +788,11 @@ end ls_cmd.desc I18n.t('app.commands.list.commands.sources.desc') ls_cmd.command :sources do |src_cmd| src_cmd.desc I18n.t('app.commands.list.switches.tree.desc') - src_cmd.switch ['tree'], :negatable => false + src_cmd.switch ['tree'], negatable: false src_cmd.action do |global_options, options, args| local_files = [] dest_files = [] @@ -804,11 +814,11 @@ end local_files << local_file else Find.find(@base_path) do |source_path| - dest = source_path.sub(/\A#{@base_path}/, '') # relative path in Crowdin + dest = source_path.sub(/\A#{Regexp.escape(@base_path)}/, '') # relative path in Crowdin if File.directory?(source_path) if ignores.any? { |pattern| File.fnmatch?(pattern, dest, File::FNM_PATHNAME) } Find.prune # Don't look any further into this directory else @@ -848,11 +858,11 @@ end # list sources ls_cmd.desc I18n.t('app.commands.list.commands.translations.desc') ls_cmd.command :translations do |trans_cmd| trans_cmd.desc I18n.t('app.commands.list.switches.tree.desc') - trans_cmd.switch ['tree'], :negatable => false + trans_cmd.switch ['tree'], negatable: false trans_cmd.action do |global_options, options, args| project_info = @crowdin.project_info project_languages = project_info['languages'].collect{ |h| h['code'] } @@ -865,20 +875,20 @@ languages_mapping = file['languages_mapping'] # Hash or NilClass ignores = file['ignore'] || [] if File.exists?(File.join(@base_path, file['source'])) - dest = file['source'].sub(/\A#{@base_path}/, '') + dest = file['source'].sub(/\A#{Regexp.escape(@base_path)}/, '') translation_languages.each do |lang| local_file = export_pattern_to_path(dest, file['translation'], lang, languages_mapping) translation_files << local_file end else Find.find(@base_path) do |source_path| - dest = source_path.sub(/\A#{@base_path}/, '') # relative path in Crowdin + dest = source_path.sub(/\A#{Regexp.escape(@base_path)}/, '') # relative path in Crowdin if File.directory?(source_path) if ignores.any? { |pattern| File.fnmatch?(pattern, dest, File::FNM_PATHNAME) } Find.prune # Don't look any further into this directory else @@ -933,99 +943,136 @@ 'content_segmentation', 'translatable_elements', ] unless File.exists?(globals[:config]) - exit_now! <<EOS -Can't find configuration file (default `crowdin.yaml'). -Type `crowdin-cli help` to know how to specify custom configuration file + exit_now! <<-EOS.strip_heredoc + Can't find configuration file (default `crowdin.yaml'). + Type `crowdin-cli help` to know how to specify custom configuration file -See http://crowdin.com/page/cli-tool#configuration-file for more details -EOS - else - begin - @config = YAML.load_file(globals[:config]) || {} - rescue Psych::SyntaxError => err - exit_now! <<EOS -Could not parse YAML: #{err.message} + See http://crowdin.com/page/cli-tool#configuration-file for more details + EOS + end -We were unable to successfully parse the crowdin.yaml file that you provided - it most likely is not well-formed YAML. -Please check whether your crowdin.yaml is valid YAML - you can use the http://yamllint.com/ validator to do this - and make any necessary changes to fix it. -EOS - end + # load project-specific configuration + # + begin + # undocumented feature + # you can use ERB in your config file, e.g. + # api_key: <%= # ruby code ... %> + # + @config = YAML.load(ERB.new(File.read(globals[:config])).result) || {} + rescue Psych::SyntaxError => err + exit_now! <<-EOS.strip_heredoc + Could not parse YAML: #{err.message} - if File.exists?(globals[:identity]) - identity = YAML.load_file(globals[:identity]) || {} - ['api_key', 'project_identifier', 'base_path'].each do |key| - @config[key] = identity[key] if identity[key] + We were unable to successfully parse the crowdin.yaml file that you provided - most likely it is not well-formatted YAML. + Please check whether your crowdin.yaml is valid YAML - you can use the http://yamllint.com/ validator to do this - and make any necessary changes to fix it. + EOS + end + + # try to load the API credentials from an environment variable, e.g. + # + # project_identifier_env: 'CROWDIN_PROJECT_ID' + # api_key_env: 'CROWDIN_API_KEY' + # + ['api_key', 'project_identifier'].each do |key| + if @config["#{key}_env"] + unless @config[key] # project credentials have a higher priority if they are specified in the config + @config[key] = ENV[@config["#{key}_env"]] end end + end - ['api_key', 'project_identifier'].each do |key| - unless @config[key] - exit_now! <<EOS -Configuration file misses required option `#{key}` - -See http://crowdin.com/page/cli-tool#configuration-file for more details -EOS - end + # user credentials in the user-specific config file have a higher priority than project-specific + # + if File.exists?(globals[:identity]) + identity = YAML.load(ERB.new(File.read(globals[:identity])).result) || {} + ['api_key', 'project_identifier', 'base_path'].each do |key| + @config[key] = identity[key] if identity[key] end + end - unless @config['files'] - exit_now! <<EOS -Configuration file misses required section `files` + ['api_key', 'project_identifier'].each do |key| + unless @config[key] + exit_now! <<-EOS.strip_heredoc + Configuration file misses required option `#{key}` -See http://crowdin.com/page/cli-tool#configuration-file for more details -EOS + See http://crowdin.com/page/cli-tool#configuration-file for more details + EOS end end + unless @config['files'] + exit_now! <<-EOS.strip_heredoc + Configuration file misses required section `files` + + See https://crowdin.com/page/cli-tool#configuration-file for more details + EOS + end + @config['files'].each do |file| + unless file['source'] + exit_now! <<-EOS.strip_heredoc + Files section misses required parameter `source` + + See https://crowdin.com/page/cli-tool#configuration-file for more details + EOS + end + + unless file['translation'] + exit_now! <<-EOS.strip_heredoc + Files section misses required parameter `translation` + + See https://crowdin.com/page/cli-tool#configuration-file for more details + EOS + end + file['source'] = '/' + file['source'] unless file['source'].start_with?('/') #file['translation'] = '/' + file['translation'] unless file['translation'].start_with?('/') if file['source'].include?('**') if file['source'].scan('**').size > 1 - exit_now! <<EOS -Source pattern `#{file['source']}` is not valid. The mask `**` can be used only once in the source pattern. -EOS + exit_now! <<-EOS.strip_heredoc + Source pattern `#{file['source']}` is not valid. The mask `**` can be used only once in the source pattern. + EOS elsif file['source'].scan('**').size == 1 and !file['source'].match(/\/\*\*\//) - exit_now! <<EOS -Source pattern `#{file['source']}` is not valid. The mask `**` must be surrounded by backslashes `/` in the source pattern. -EOS + exit_now! <<-EOS.strip_heredoc + Source pattern `#{file['source']}` is not valid. The mask `**` must be surrounded by slashes `/` in the source pattern. + EOS end else if file['translation'].include?('**') - exit_now! <<EOS -Translation pattern `#{file['translation']}` is not valid. The mask `**` can't be used. -When using `**` in 'translation' pattern it will always contain sub-path from 'source' for certain file. -EOS + exit_now! <<-EOS.strip_heredoc + Translation pattern `#{file['translation']}` is not valid. The mask `**` can't be used. + When using `**` in 'translation' pattern it will always contain sub-path from 'source' for certain file. + EOS end end end #@config['files'] if @config['base_path'] @base_path = @config['base_path'] else @base_path = Dir.pwd - puts <<EOS -Warning: Configuration file misses parameter `base_path` that defines your project root directory. Using `#{@base_path}` as a root directory. -EOS + puts <<-EOS.strip_heredoc + Warning: Configuration file misses parameter `base_path` that defines your project root directory. Using `#{@base_path}` as a root directory. + EOS end @preserve_hierarchy = false if @config['preserve_hierarchy'] @preserve_hierarchy = case @config['preserve_hierarchy'] when true true when false false else - exit_now! <<EOS -Parameter `preserve_hierarchy` allows values of true or false. -EOS + exit_now! <<-EOS.strip_heredoc + Parameter `preserve_hierarchy` allows values of true or false. + EOS end end @jipt_language = nil if @config['jipt_language'] @@ -1051,10 +1098,10 @@ post do |globals, 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 globals[:verbose] + # puts "Executed #{command.name}" if globals[:verbose] end on_error do |exception| # Error logic here # return false to skip default error handling