viktorasl/xlocalize

View on GitHub
lib/xlocalize/executor.rb

Summary

Maintainability
C
1 day
Test Coverage
require 'xlocalize/webtranslateit'
require 'xlocalize/xliff'
require 'xlocalize/helper'
require 'xlocalize/importer'
require 'colorize'
require 'nokogiri'
require 'yaml'
require 'apfel'

module Xlocalize
  class Executor

    def plurals_file_name(locale)
      return locale_file_name(locale) << '_plurals.yml'
    end

    def locale_file_name(locale)
      if Helper.xcode_at_least?(10)
        return "#{locale}.xcloc/Localized Contents/#{locale}.xliff"
      else
        return "#{locale}.xliff"
      end
    end

    def export_master(wti, project, targets, excl_prefix, master_lang, exclude_units=[], no_cryptic)
      master_file_name = locale_file_name(master_lang)

      File.delete(master_file_name) if File.exist?(master_file_name)

      if Helper.xcode_at_least?(9)
        Kernel.system "xcodebuild -exportLocalizations -localizationPath ./ -project #{project}"
      else
        # hacky way to finish xcodebuild -exportLocalizations script, because
        # since Xcode7.3 & OS X Sierra script hangs even though it produces
        # xliff output
        # http://www.openradar.me/25857436
        Kernel.system "xcodebuild -exportLocalizations -localizationPath ./ -project #{project} & sleep 0"
        while !File.exist?(master_file_name) do
          sleep(1)
        end        
      end

      purelyze(master_lang, targets, excl_prefix, project, filer_ui_duplicates=Helper.xcode_at_least?(9.3), exclude_units)
      if no_cryptic then
        config_fname = '.xlocalize.yml'
        config = (YAML.load_file(config_fname) if File.file?(config_fname)) || {}
        allow_cryptic = config['allow_cryptic'] || {}
        
        doc = Nokogiri::XML(File.open(locale_file_name(master_lang)))
        cryptic_trans_units = doc.cryptic_trans_units(allow_cryptic)
        if !cryptic_trans_units.empty? then
          err_msg = "Found cryptic translation units\n"
          err_msg += cryptic_trans_units.map { |fname, units| "#{fname}" + "\n " + units.join("\n ") }.join("\n")
          raise err_msg
        end
      end

      if wti then
        original_doc = Nokogiri::XML(wti.pull(master_lang)['xliff'])
        Nokogiri::XML(File.open(master_file_name)).merge_on_top_of(original_doc)
        File.write(master_file_name, original_doc.to_xml)
      end
      push_master_file(wti, master_lang, master_file_name) if !wti.nil?
    end

    def push_master_file(wti, master_lang, master_file_name)
      # Pushing master file to WebtranslateIt
      begin
        puts "Uploading master file to WebtranslateIt"
        file = File.open(master_file_name, 'r')
        plurals_path = plurals_file_name(master_lang)
        plurals_file = File.exist?(plurals_path) ? File.open(plurals_path, 'r') : nil
        wti.push_master(file, plurals_file)
        puts "Done.".green
      rescue => err
        puts err.to_s.red
      ensure
        file.close unless file.nil?
        plurals_file.close unless plurals_file.nil?
      end if !wti.nil?
    end

    def purelyze(locale, targets, excl_prefix, project, filer_ui_duplicates=false, exclude_units)
      locale_file_name = locale_file_name(locale)
      doc = Nokogiri::XML(File.open(locale_file_name))

      puts "Removing all files not matching required targets" if $VERBOSE
      doc.filter_not_target_files(targets)
      puts "Removing trans-unit's having reserverd prefix in their sources" if $VERBOSE
      doc.filter_trans_units(excl_prefix)
      puts "Filtering plurals" if $VERBOSE
      plurals = doc.filter_plurals(project)
      puts "Removing excluded translation units" if $VERBOSE
      doc.xpath("//xmlns:trans-unit").each { |unit| unit.remove if exclude_units.include?(unit['id']) }
      puts "Removing all files having no trans-unit elements after removal" if $VERBOSE
      doc.filter_empty_files
      puts "Unescaping translation units" if $VERBOSE
      doc.unescape

      if filer_ui_duplicates
        puts "Filtering duplicate xib & storyboard translation files" if $VERBOSE
        doc.filter_duplicate_storyboard_xib_files
      end
      
      puts "Writing modified XLIFF file to #{locale_file_name}" if $VERBOSE
      File.open(locale_file_name, 'w') { |f| f.write(doc.to_xml) }
      if !plurals.empty?
        puts "Writing plurals to plurals YAML file" if $VERBOSE
        File.open(plurals_file_name(locale), 'w') { |f| f.write({locale => plurals}.to_yaml) }
      end
    end

    def out_list_of_translations_of_locale(wti, locale, translations)
      puts "Downloading translations for #{locale}"
      translations = wti.pull(locale)
      plurals_content = translations['plurals']

      out_list = [{
        "path" => "#{locale}.xliff",
        "content" => translations['xliff']
      }]
      out_list << {
        "path" => "#{locale}_plurals.yaml",
        "content" => plurals_content
      } if not plurals_content.nil?

      return out_list
    end

    def download(wti, locales)
      begin
        locales.each do |locale|
          translations = wti.pull(locale)
          
          out_list_of_translations_of_locale(wti, locale, translations).each do |out|
            File.open(out["path"], "w") do |file|
              file.write(out["content"])
              puts "Done saving #{out['path']}.".green
            end
          end
        end
      rescue => err
        puts err.to_s.red
      end
    end

    def localized_filename(file_name, locale)
      parts = file_name.split('/')

      # WebTranslateIt uses _ for Language_Country separator e.g. pt_PT
      # but Xcode uses - e.g. pt-PT
      # Repace it to match Xcode file locations
      locale.sub!("_", "-")

      name = ""
      parts.each_with_index do |part, idx|
        name += "/" if idx > 0
        if part.end_with?(".lproj")
          name += "#{locale}.lproj"
        elsif idx+1 == parts.count
          extension = (part.split('.')[1] == 'stringsdict') ? 'stringsdict' : 'strings'
          # TODO: join all parts till the last '.'
          name += "#{part.split('.')[0]}.#{extension}"
        else
          name += part
        end
      end
      return name
    end

    def import_xliff(fname)
      puts "Importing translations from #{fname}" if $VERBOSE
      Nokogiri::XML(File.open(fname)).xpath("//xmlns:file").each do |node|
        tr_fname = node["original"]
        source_lang = node["source-language"]
        target_lang = node["target-language"]
        
        localized_src_fname = localized_filename(tr_fname, source_lang)
        next if !File.exist?(localized_src_fname)

        translations_hash = Apfel.parse(localized_src_fname).to_hash
        importer = Importer.new
        importer.translate_from_node(translations_hash, node)

        f_content = importer.strings_content_from_translations_hash(translations_hash)
        target_fname = localized_filename(tr_fname, target_lang)
        File.open(target_fname, 'w') { |f| f.write(f_content) }
      end
    end

    def import_plurals_if_needed(locale)
      plurals_fname = "#{locale}_plurals.yaml"
      return if !File.exist?(plurals_fname)
      puts "Importing translations from #{plurals_fname}" if $VERBOSE
      plurals_yml = YAML.load_file(plurals_fname)
      plurals_yml[locale].each do |original_fname, trans_units|
        content = ''
        content << '<?xml version="1.0" encoding="UTF-8"?>' + "\n"
        content << '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">' + "\n"
        content << '<plist version="1.0">' + "\n"

        fname = localized_filename(original_fname, locale)
        content << "<dict>\n"
        trans_units.each do |key, element|
          content << "\t<key>#{key}</key>\n"
          content << "\t<dict>\n"

          content << "\t\t<key>NSStringLocalizedFormatKey</key>\n"
          content << "\t\t<string>%\#@value@</string>\n"
          content << "\t\t<key>value</key>\n"
          content << "\t\t<dict>\n"
          element.each do |k, v|
            content << "\t\t\t<key>#{k}</key>\n"
            content << "\t\t\t<string>#{v}</string>\n"
          end
          content << "\t\t\t<key>NSStringFormatSpecTypeKey</key>\n"
          content << "\t\t\t<string>NSStringPluralRuleType</string>\n"
          content << "\t\t\t<key>NSStringFormatValueTypeKey</key>\n"
          content << "\t\t\t<string>d</string>\n"
          content << "\t\t</dict>\n"

          content << "\t</dict>\n"
        end
        content << "</dict>\n"
        content << "</plist>\n"

        File.open(fname, 'w') { |f| f.write content }
      end
    end

    def import(locales, allows_missing_files=false)
      puts 'Importing translations' if $VERBOSE
      locales.each do |locale|
        import_xliff("#{locale}.xliff")
        import_plurals_if_needed(locale)
        puts "Done #{locale}".green if $VERBOSE
      end
    end
  end
end