class Fastlane::Actions::GoogleSheetLocalizeAction

Public Class Methods

authors() click to toggle source
# File lib/fastlane/plugin/google_sheet_localize/actions/google_sheet_localize_action.rb, line 561
def self.authors
  ["Mario Hahn", "Thomas Koller"]
end
available_options() click to toggle source
# File lib/fastlane/plugin/google_sheet_localize/actions/google_sheet_localize_action.rb, line 574
def self.available_options
  [
    FastlaneCore::ConfigItem.new(key: :service_account_path,
                            env_name: "SERVICE_ACCOUNT_PATH",
                         description: "Credentials path",
                            optional: false,
                                type: String),
     FastlaneCore::ConfigItem.new(key: :sheet_id,
                             env_name: "SHEET_ID",
                          description: "Your Google-spreadsheet id",
                             optional: false,
                                 type: String),
    FastlaneCore::ConfigItem.new(key: :platform,
                            env_name: "PLATFORM",
                         description: "Platform, ios or android",
                            optional: true,
                       default_value: Actions.lane_context[Actions::SharedValues::PLATFORM_NAME].to_s,
               default_value_dynamic: true,
                                type: String),
    FastlaneCore::ConfigItem.new(key: :tabs,
                            env_name: "TABS",
                         description: "Array of all Google Sheet Tabs",
                         default_value: [],
                            optional: true,
                                type: Array),
    FastlaneCore::ConfigItem.new(key: :support_spm,
                            env_name: "SUPPORT_SPM",
                         description: "Is Swift Package",
                       default_value: false,
                                type: Boolean),
    FastlaneCore::ConfigItem.new(key: :language_titles,
                            env_name: "LANGUAGE_TITLES",
                         description: "Alle language titles",
                            optional: false,
                                type: Array),
    FastlaneCore::ConfigItem.new(key: :default_language,
                            env_name: "DEFAULT_LANGUAGE",
                         description: "Default Language",
                            optional: true,
                                type: String),
    FastlaneCore::ConfigItem.new(key: :comment_example_language,
                            env_name: "COMMENT_EXAMPLE_LANGUAGE",
                         description: "Comment Example Language",
                            optional: true,
                                type: String),
    FastlaneCore::ConfigItem.new(key: :base_language,
                            env_name: "BASE_LANGUAGE",
                         description: "Base language for Xcode projects",
                            optional: true,
                                type: String),
    FastlaneCore::ConfigItem.new(key: :localization_path,
                            env_name: "LOCALIZATION_PATH",
                         description: "Output path",
                            optional: false,
                                type: String),
    FastlaneCore::ConfigItem.new(key: :identifier_name,
                            env_name: "IDENTIFIER_NAME",
                         description: "Identifier for Platform",
                            optional: true,
                                type: String),
    FastlaneCore::ConfigItem.new(key: :code_generation_path,
                            env_name: "CODEGENERATIONPATH",
                         description: "Code generation path for the Swift file",
                            optional: true,
                                type: String),
    FastlaneCore::ConfigItem.new(key: :support_objc,
                            env_name: "OBJC_SUPPORT",
                         description: "Whether the generated code should support Obj-C. Only relevant for the ios platform",
                                type: Boolean,
                       default_value: false)
  ]
end
createComment(comment, example) click to toggle source
# File lib/fastlane/plugin/google_sheet_localize/actions/google_sheet_localize_action.rb, line 238
  def self.createComment(comment, example) 

    if comment.to_s.empty?
      return %Q(
/**
#{example}
*/
)
    end

  return %Q(
/**
#{example}

- Sheet comment:
````
#{comment}
````
*/
)
  end
createFiles(languages, platform, destinationPath, defaultLanguage, base_language, codeGenerationPath, comment_example_language, support_objc, support_spm) click to toggle source
# File lib/fastlane/plugin/google_sheet_localize/actions/google_sheet_localize_action.rb, line 136
def self.createFiles(languages, platform, destinationPath, defaultLanguage, base_language, codeGenerationPath, comment_example_language, support_objc, support_spm)
    self.createFilesForLanguages(languages, platform, destinationPath, defaultLanguage, base_language)

    if platform == "web"
      jsonFileName = "Localization.json"

      jsonFilepath = "#{destinationPath}/#{jsonFileName}"

      File.open(jsonFilepath, "w") do |f|

        jsonItem = {}

        languages.each { |language|
          filteredItems = self.filterUnusedRows(language["items"],'identifier', "true")

          allKeys = {}

          filteredItems.each { |item|
            identifier = item['identifier']

            text = item['text']

            matches = text.scan(/%[0-9][sdf]/)
            matches.each { |match|
              text = text.gsub(match, "{#{match[1]}}")
            }

            if !identifier.include?('//')
              allKeys[identifier] = text
            end
          }
          jsonItem[language["language"]] = allKeys
        }

        jsonString = JSON.pretty_generate(jsonItem)
        f.write(jsonString)
      end
    end

    if platform == "ios"

      swiftFilename = "Localization.swift"

      swiftPath = codeGenerationPath

      if codeGenerationPath.to_s.empty?
        swiftPath = destinationPath
      end

      languageItems = languages.select { |item| item["language"] == comment_example_language }.first

      filteredItems = self.filterUnusedRows(languageItems["items"],'identifier', "true")

      swiftFilepath = "#{swiftPath}/#{swiftFilename}"

      File.open(swiftFilepath, "w") do |f|
        f.write("import Foundation\n\n// swiftlint:disable all\n#{getiOSTypeDefinition(support_objc)} {\n")
        filteredItems.each { |item|

          identifier = item['identifier']

          values = identifier.dup.split(/\.|_/)

          constantName = ""

          values.each_with_index do |item, index|
            if index == 0
              item[0] = item[0].downcase
              constantName += item
            else
              item[0] = item[0].upcase
              constantName += item
            end
          end

          if constantName == "continue"
            constantName = "`continue`"
          end

          if constantName == "switch"
            constantName = "`switch`"
          end

          text = self.mapInvalidPlaceholder(item['text'])

          arguments = self.findArgumentsInText(text)

          if arguments.count == 0
            f.write(self.createComment(item['comment'], item['text']))
            f.write("#{getiOSAttributes(support_objc)}public static let #{constantName} = localized(identifier: \"#{identifier}\")\n")
          else
            f.write(self.createComment(item['comment'], item['text']))
            f.write(self.createiOSFunction(constantName, identifier, arguments, support_objc))
          end
        }
        f.write("\n}")
        f.write(self.createiOSFileEndString(destinationPath, support_spm))
      end

    end
end
createFilesForLanguages(languages, platform, destinationPath, defaultLanguage, base_language) click to toggle source
# File lib/fastlane/plugin/google_sheet_localize/actions/google_sheet_localize_action.rb, line 260
def self.createFilesForLanguages(languages, platform, destinationPath, defaultLanguage, base_language)

  languages.each { |language|

  if platform == "ios"

    filteredItems = self.filterUnusedRows(language["items"],'identifier', "false")

    stringFileName = "Localizable.strings"
    pluralsFileName = "Localizable.stringsdict"

    languageName = language['language']

    if languageName == base_language
      languageName = "Base"
    end

    stringFilepath = "#{destinationPath}/#{languageName}.lproj/#{stringFileName}"
    pluralsFilepath = "#{destinationPath}/#{languageName}.lproj/#{pluralsFileName}"
    FileUtils.mkdir_p "#{destinationPath}/#{languageName}.lproj"

    File.open(stringFilepath, "w") do |f|
      filteredItems.each_with_index { |item, index|

        text = self.mapInvalidPlaceholder(item['text'])
        comment = item['comment']
        identifier = item['identifier']

        line = ""
        if identifier.include?('//')
          line = "\n\n#{identifier}\n"
        else


            if (text == "" || text == "TBD") && !defaultLanguage.to_s.empty?
              default_language_object = languages.select { |languageItem| languageItem['language'] == defaultLanguage }.first["items"]
              default_language_object = self.filterUnusedRows(default_language_object,'identifier', "false")

              defaultLanguageText = default_language_object[index]['text']
              puts "found empty text for:\n\tidentifier: #{identifier}\n\tlanguage:#{language['language']}\n\treplacing it with: #{defaultLanguageText}"
              text = self.mapInvalidPlaceholder(defaultLanguageText)
            end

            if !text.include?("one|")

            text = text.gsub("$s","$@").gsub("%s","%@")

            line = "\"#{identifier}\" = \"#{text}\";"
            if !comment.to_s.empty?
              line = line + " //#{comment}\n"
            else
              line = line + "\n"
            end
          end
        end
        f.write(line)
      }
    end

    File.open(pluralsFilepath, "w") do |f|

      f.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
      f.write("<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">\n")
      f.write("<plist version=\"1.0\">\n")
      f.write("<dict>\n")

      filteredItems.each_with_index { |item, index|

        text = self.mapInvalidPlaceholder(item['text'])
        identifier = item['identifier']

          if (text == "" || text == "TBD") && !defaultLanguage.to_s.empty?
            default_language_object = languages.select { |languageItem| languageItem['language'] == defaultLanguage }.first["items"]
            default_language_object = self.filterUnusedRows(default_language_object,'identifier', "false")

            defaultLanguageText = default_language_object[index]['text']
            puts "found empty text for:\n\tidentifier: #{identifier}\n\tlanguage:#{language['language']}\n\treplacing it with: #{defaultLanguageText}"
            text = self.mapInvalidPlaceholder(defaultLanguageText)
          end

          if !identifier.include?('//') && text.include?("one|")

          text = text.gsub("\n", "|")

          formatIdentifier = identifier.gsub(".", "")

          f.write("\t\t<key>#{identifier}</key>\n")
          f.write("\t\t<dict>\n")
          f.write("\t\t\t<key>NSStringLocalizedFormatKey</key>\n")
          f.write("\t\t\t<string>%#@#{formatIdentifier}@</string>\n")
          f.write("\t\t\t<key>#{formatIdentifier}</key>\n")
          f.write("\t\t\t<dict>\n")
          f.write("\t\t\t\t<key>NSStringFormatSpecTypeKey</key>\n")
          f.write("\t\t\t\t<string>NSStringPluralRuleType</string>\n")
          f.write("\t\t\t\t<key>NSStringFormatValueTypeKey</key>\n")
          f.write("\t\t\t\t<string>d</string>\n")

          text.split("|").each_with_index { |word, wordIndex|
            if wordIndex % 2 == 0
              f.write("\t\t\t\t<key>#{word}</key>\n")
            else
              f.write("\t\t\t\t<string>#{word}</string>\n")
            end
          }
          f.write("\t\t\t</dict>\n")
          f.write("\t\t</dict>\n")
        end
      }
      f.write("</dict>\n")
      f.write("</plist>\n")
    end
  end

  if platform == "android"
    languageDir = language['language']

    if languageDir == base_language
      languageDir = "values"
    else
      languageDir = "values-#{languageDir}"
    end

    FileUtils.mkdir_p "#{destinationPath}/#{languageDir}"
    File.open("#{destinationPath}/#{languageDir}/strings.xml", "w") do |f|
      f.write("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
      f.write("<resources>\n")

      filteredItems = self.filterUnusedRows(language["items"],'identifier', "false")

      filteredItems.each_with_index { |item, index|

        comment = item['comment']
        identifier = item['identifier']
        text = item['text']

        line = ""

        if !comment.to_s.empty?
          line = line + "    <!--#{comment}-->\n"
        end

        if (text == "" || text == "TBD") && !defaultLanguage.to_s.empty?
          default_language_object = languages.select { |languageItem| languageItem['language'] == defaultLanguage }.first["items"]
          default_language_object = self.filterUnusedRows(default_language_object,'identifier', "false")

          defaultLanguageText = default_language_object[index]['text']
          puts "found empty text for:\n\tidentifier: #{identifier}\n\tlanguage:#{language['language']}\n\treplacing it with: #{defaultLanguageText}"
          text = defaultLanguageText
        end

        text = text.gsub(/\\?'/, "\\\\'")

        if text.include?("one|")

          text = text.gsub("\n", "|")

          line = line + "    <plurals name=\"#{identifier}\">\n"

          plural = ""

          text.split("|").each_with_index { |word, wordIndex|
            if wordIndex % 2 == 0
              plural = "        <item quantity=\"#{word}\">"
            else
              plural = plural + "<![CDATA[#{word}]]></item>\n"
              line = line + plural
            end
          }
          line = line + "    </plurals>\n"
        elsif text.start_with?("[\"") && text.end_with?("\"]")

          line = line + "    <string-array name=\"#{identifier}\">\n"

          JSON.parse(text).each { |arrayItem|

            arrayItem = arrayItem.gsub("'", "\\\\'")

            line = line + "        <item><![CDATA[#{arrayItem}]]></item>\n"
          }

          line = line + "    </string-array>\n"
        else
          line = line + "    <string name=\"#{identifier}\"><![CDATA[#{text}]]></string>\n"
        end

        f.write(line)
      }
      f.write("</resources>\n")
    end
  end
  }
end
createiOSFileEndString(destinationPath, support_spm) click to toggle source
# File lib/fastlane/plugin/google_sheet_localize/actions/google_sheet_localize_action.rb, line 453
def self.createiOSFileEndString(destinationPath, support_spm)

  bundle = support_spm ? "let bundle = Bundle.module" : "let bundle = Bundle(for: LocalizationHelper.self)"

  puts destinationPath

  if destinationPath.include?(".bundle")

    bundle = %Q(let bundleUrl = Bundle(for: LocalizationHelper.self).url(forResource: "#{destinationPath.split('/').last.gsub(".bundle", "")}", withExtension: "bundle")
  \n\t\tlet bundle = Bundle(url: bundleUrl!)!)
  end

  return %Q(
  \n\nprivate class LocalizationHelper { }
  \n\nextension Localization {
  \n\tprivate static func localized(identifier key: String, _ args: CVarArg...) -> String {
  \n\t\t#{bundle}
  \n\t\tlet format = NSLocalizedString(key, tableName: nil, bundle: bundle, comment: \"\")
  \n\t\tguard !args.isEmpty else { return format }
  \n\t\treturn String(format: format, locale: .current, arguments: args)
  \n\t}
  \n})
end
createiOSFunction(constantName, identifier, arguments, support_objc) click to toggle source
# File lib/fastlane/plugin/google_sheet_localize/actions/google_sheet_localize_action.rb, line 477
def self.createiOSFunction(constantName, identifier, arguments, support_objc)
    functionTitle = "#{getiOSAttributes(support_objc)}public static func #{constantName}("

    arguments.each_with_index do |item, index|
      functionTitle = functionTitle + "_ arg#{index}: #{item[:type]}"
      if index < arguments.count - 1
        functionTitle = functionTitle + ", "
      else
        functionTitle = functionTitle + ") -> String {\n"
      end
    end
    functionTitle = functionTitle + "\t\treturn localized(identifier: \"#{identifier}\", "
    arguments.each_with_index do |item, index|
      functionTitle = functionTitle + "arg#{index}"
      if index < arguments.count - 1
        functionTitle = functionTitle + ", "
      else
        functionTitle = functionTitle + ")\n\t}"
      end
    end

    return functionTitle
end
description() click to toggle source
# File lib/fastlane/plugin/google_sheet_localize/actions/google_sheet_localize_action.rb, line 557
def self.description
  "Creates .strings files for iOS and strings.xml files for Android"
end
details() click to toggle source
# File lib/fastlane/plugin/google_sheet_localize/actions/google_sheet_localize_action.rb, line 569
def self.details
  # Optional:
  "Creates .strings files for iOS and strings.xml files for Android. The localization is mananged on a google sheet."
end
filterUnusedRows(items, identifier, filterComment) click to toggle source
# File lib/fastlane/plugin/google_sheet_localize/actions/google_sheet_localize_action.rb, line 121
def self.filterUnusedRows(items, identifier, filterComment)
  filtered = items.select { |item|
      currentIdentifier = item[identifier]
      currentIdentifier != "NR" && currentIdentifier != "" && currentIdentifier != "TBD"
  }
  
  if filterComment == "false" 
     return filtered
  end

  return filtered.select { |item|
      !item[identifier].include?('//')
  }
end
findArgumentsInText(text) click to toggle source
# File lib/fastlane/plugin/google_sheet_localize/actions/google_sheet_localize_action.rb, line 509
def self.findArgumentsInText(text)

  if text.include?("one|")
    text = text.dup.split("|")[1]
  end

  result = Array.new
  filtered = self.mapInvalidPlaceholder(text)

  stringIndexes = self.scan_str(filtered, /%([0-9]+)?\$?[s@]/)
  intIndexes = self.scan_str(filtered, /%([0-9]+)?\$?d/)
  floatIndexes = self.scan_str(filtered, /%([0-9]+)?\$?([0-9]+)?.?([0-9]+)?f/)
  doubleIndexes = self.scan_str(filtered, /%([0-9]+)?\$?([0-9]+)?.?([0-9]+)?ld/)

  if stringIndexes.count > 0
    result = result + stringIndexes.map { |e| { "index": e[0], "offset": e[1], "type": "String" }}
  end

  if intIndexes.count > 0
    result = result + intIndexes.map { |e| { "index": e[0], "offset": e[1], "type": "Int" }}
  end

  if floatIndexes.count > 0
    result = result + floatIndexes.map { |e| { "index": e[0], "offset": e[1], "type": "Float" }}
  end

  if doubleIndexes.count > 0
    result = result + doubleIndexes.map { |e| { "index": e[0], "offset": e[1], "type": "Double" }}
  end

  result = result.sort_by { |hsh| hsh[:offset] }

  return result
end
generateJSONObject(contentRows, index, identifierIndex) click to toggle source
# File lib/fastlane/plugin/google_sheet_localize/actions/google_sheet_localize_action.rb, line 93
def self.generateJSONObject(contentRows, index, identifierIndex)
    result = Array.new
    for i in 0..contentRows.count - 1
        item = self.generateSingleObject(contentRows[i], index, identifierIndex)

        if item[:identifier] != ""
          result.push(item)
        end
    end

    return result

end
generateSingleObject(row, column, identifierIndex) click to toggle source
# File lib/fastlane/plugin/google_sheet_localize/actions/google_sheet_localize_action.rb, line 107
def self.generateSingleObject(row, column, identifierIndex)
  identifier = row[identifierIndex]

  text = row[column]
  comment = row.last

  object = { 'identifier' => identifier,
             'text' => text,
             'comment' => comment
  }
  return object

end
getiOSAttributes(support_objc) click to toggle source
# File lib/fastlane/plugin/google_sheet_localize/actions/google_sheet_localize_action.rb, line 505
def self.getiOSAttributes(support_objc)
  return support_objc ? "@objc " : ""
end
getiOSTypeDefinition(support_objc) click to toggle source
# File lib/fastlane/plugin/google_sheet_localize/actions/google_sheet_localize_action.rb, line 501
def self.getiOSTypeDefinition(support_objc)
  return support_objc ? "public class Localization: NSObject" : "public struct Localization"
end
is_supported?(platform) click to toggle source
# File lib/fastlane/plugin/google_sheet_localize/actions/google_sheet_localize_action.rb, line 647
def self.is_supported?(platform)
   [:ios, :mac, :android].include?(platform)
end
mapInvalidPlaceholder(text) click to toggle source
# File lib/fastlane/plugin/google_sheet_localize/actions/google_sheet_localize_action.rb, line 552
def self.mapInvalidPlaceholder(text)
  filtered = text.gsub('%s', '%@').gsub('"', '\"')
  return filtered
end
return_value() click to toggle source
# File lib/fastlane/plugin/google_sheet_localize/actions/google_sheet_localize_action.rb, line 565
def self.return_value
  # If your method provides a return value, you can describe here what it does
end
run(params) click to toggle source
# File lib/fastlane/plugin/google_sheet_localize/actions/google_sheet_localize_action.rb, line 10
def self.run(params)

  session = ::GoogleDrive::Session.from_service_account_key(params[:service_account_path])
  spreadsheet_id = "https://docs.google.com/spreadsheets/d/#{params[:sheet_id]}"
  tabs = params[:tabs]
  platform = params[:platform]
  path = params[:localization_path]
  language_titles = params[:language_titles]
  default_language = params[:default_language]
  base_language = params[:base_language]
  code_generation_path = params[:code_generation_path]
  identifier_name = params[:identifier_name]
  comment_example_language = params[:comment_example_language]
  support_objc = params[:support_objc]
  support_spm = params[:support_spm]

  if identifier_name.to_s.empty?
    if platform == "ios"
      identifier_name = "Identifier iOS"
    end
    if platform == "android"
      identifier_name = "Identifier Android"
    end
    if platform == "web"
      identifier_name = "Identifier Web"
    end
  end

  if comment_example_language.to_s.empty?
    comment_example_language = default_language
  end

  spreadsheet = session.spreadsheet_by_url(spreadsheet_id)

  filterdWorksheets = []

  if tabs.count == 0
    filterdWorksheets = spreadsheet.worksheets
  else
    filterdWorksheets = spreadsheet.worksheets.select { |item| tabs.include?(item.title) }
  end

  result = []
  
  filterdWorksheets.each { |worksheet|

    identifierIndex = 0

    for i in 0..worksheet.max_cols
      if worksheet.rows[0][i] == identifier_name
        identifierIndex = i
      end
    end

    for i in 0..worksheet.max_cols

      title = worksheet.rows[0][i]

      if language_titles.include?(title)

        language = result.select { |item| item['language'] == title }.first

        if language.nil?
          language = {
            'language' => title,
            'items' => []
          }
        end

        contentRows = worksheet.rows.drop(1)

        items = language['items']
        items = items + self.generateJSONObject(contentRows, i, identifierIndex)

        language['items'] = items

        result.push(language)
      end
    end
}
self.createFiles(result, platform, path, default_language, base_language, code_generation_path, comment_example_language, support_objc, support_spm)
end
scan_str(str, pattern) click to toggle source
# File lib/fastlane/plugin/google_sheet_localize/actions/google_sheet_localize_action.rb, line 544
def self.scan_str(str, pattern)
  res = []
  (0..str.length).each do |i|
    res << [Regexp.last_match.to_s, i] if str[i..-1] =~ /^#{pattern}/
  end
  res
end