Class: Fastlane::Helper::Ios::L10nHelper

Inherits:
Object
  • Object
show all
Defined in:
lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb

Class Method Summary collapse

Class Method Details

.download_glotpress_export_file(project_url:, locale:, filters:, destination:) ⇒ Object

Downloads the export from GlotPress for a given locale and given filters.

Parameters:

  • project_url (String)

    The URL to the GlotPress project to export from, e.g. ‘“translate.wordpress.org/projects/apps/ios/dev”`

  • locale (String)

    The GlotPress locale code to download strings for.

  • filters (Hash{Symbol=>String})

    The hash of filters to apply when exporting from GlotPress. Typical examples include ‘{ status: ’current’ }‘ or `{ status: ’review’ }‘.

  • destination (String, IO)

    The path or ‘IO`-like instance, where to write the downloaded file on disk.



144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb', line 144

def self.download_glotpress_export_file(project_url:, locale:, filters:, destination:)
  query_params = (filters || {}).transform_keys { |k| "filters[#{k}]" }.merge(format: 'strings')
  uri = URI.parse("#{project_url.chomp('/')}/#{locale}/default/export-translations/?#{URI.encode_www_form(query_params)}")

  # Set an unambiguous User Agent so GlotPress won't rate-limit us
  options = { 'User-Agent' => Wpmreleasetoolkit::USER_AGENT }

  begin
    IO.copy_stream(uri.open(options), destination)
  rescue StandardError => e
    UI.error "Error downloading locale `#{locale}` — #{e.message} (#{uri})"
    retry if e.is_a?(OpenURI::HTTPError) && UI.confirm("Retry downloading `#{locale}`?")
    return nil
  end
end

.generate_strings_file_from_hash(translations:, output_path:) ⇒ Object

Note:

The generated file will be in XML-plist format since ASCII plist is deprecated as an output format by every Apple tool so there’s no safe way to generate ASCII format.

Generate a ‘.strings` file from a dictionary of string translations.

Especially useful to generate ‘.strings` files not from code, but from keys extracted from another source (like a JSON file export from GlotPress, or subset of keys extracted from the main `Localizable.strings` to generate an `InfoPlist.strings`)

Parameters:

  • translations (Hash<String,String>)

    The dictionary of key=>translation translations to put in the generated ‘.strings` file

  • output_path (String)

    The path to the ‘.strings` file to generate



116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb', line 116

def self.generate_strings_file_from_hash(translations:, output_path:)
  builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
    xml.doc.create_internal_subset(
      'plist',
      '-//Apple//DTD PLIST 1.0//EN',
      'http://www.apple.com/DTDs/PropertyList-1.0.dtd'
    )
    xml.comment('Warning: Auto-generated file, do not edit.')
    xml.plist(version: '1.0') do
      xml.dict do
        translations.sort.each do |k, v| # NOTE: use `sort` just in order to be deterministic over various runs
          xml.key(k.to_s)
          xml.string(v.to_s)
        end
      end
    end
  end
  File.write(output_path, builder.to_xml)
end

.merge_strings(paths:, output_path:) ⇒ Array<String>

Note:

For now, this method only supports merging ‘.strings` file in `:text` format and basically concatenates the files (+ checking for duplicates in the process)

Note:

The method is able to handle input files which are using different encodings, guessing the encoding of each input file using the BOM (and defaulting to UTF8). The generated file will always be in utf-8, by convention.

Merge the content of multiple ‘.strings` files into a new `.strings` text file.

Parameters:

  • paths (Hash<String, String>)

    The paths of the ‘.strings` files to merge together, associated with the prefix to prepend to each of their respective keys

  • output_path (String)

    The path to the merged ‘.strings` file to generate as a result.

Returns:

  • (Array<String>)

    List of duplicate keys found while validating the merge.

Raises:

  • (RuntimeError)

    If one of the paths provided is not in text format (but XML or binary instead), or if any of the files are missing.



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb', line 53

def self.merge_strings(paths:, output_path:)
  duplicates = []
  Tempfile.create('wpmrt-l10n-merge-', encoding: 'utf-8') do |tmp_file|
    all_keys_found = []

    tmp_file.write("/* Generated File. Do not edit. */\n\n")
    paths.each do |input_file, prefix|
      next if File.empty?(input_file) # Skip existing but totally empty files, to avoid adding useless `MARK:` comment for them

      fmt = strings_file_type(path: input_file)
      raise "The file `#{input_file}` does not exist or is of unknown format." if fmt.nil?
      raise "The file `#{input_file}` is in #{fmt} format but we currently only support merging `.strings` files in text format." unless fmt == :text

      string_keys = read_strings_file_as_hash(path: input_file).keys.map { |k| "#{prefix}#{k}" }
      duplicates += (string_keys & all_keys_found) # Find duplicates using Array intersection, and add those to duplicates list
      all_keys_found += string_keys

      tmp_file.write("/* MARK: - #{File.basename(input_file)} */\n\n")
      # Read line-by-line to reduce memory footprint during content copy; Be sure to guess file encoding using the Byte-Order-Mark.
      File.readlines(input_file, mode: 'rb:BOM|UTF-8').each do |line|
        unless prefix.nil? || prefix.empty?
          # We need to ensure the line and RegExp are using the same encoding, so we transcode everything to UTF-8.
          line.encode!(Encoding::UTF_8)
          # The `/u` modifier on the RegExps is to make them UTF-8
          line.gsub!(/^(\s*")/u, "\\1#{prefix}") # Lines starting with a quote are considered to be start of a key; add prefix right after the quote
          line.gsub!(/^(\s*)([A-Z0-9_]+)(\s*=\s*")/ui, "\\1\"#{prefix}\\2\"\\3") # Lines starting with an identifier followed by a '=' are considered to be an unquoted key (typical in InfoPlist.strings files for example)
        end
        tmp_file.write(line)
      end
      tmp_file.write("\n")
    end
    tmp_file.close # ensure we flush the content to disk
    FileUtils.cp(tmp_file.path, output_path)
  end
  duplicates
end

.read_strings_file_as_hash(path:) ⇒ Hash<String,String>

Return the list of translations in a ‘.strings` file.

Parameters:

  • path (String)

    The path to the ‘.strings` file to read

Returns:

  • (Hash<String,String>)

    A dictionary of key=>translation translations.

Raises:

  • (RuntimeError)

    If the file is not a valid strings file or there was an error in parsing its content.



96
97
98
99
100
101
102
103
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb', line 96

def self.read_strings_file_as_hash(path:)
  return {} if File.empty?(path) # Return empty hash if completely empty file

  output, status = Open3.capture2e('/usr/bin/plutil', '-convert', 'json', '-o', '-', path)
  raise output unless status.success?

  JSON.parse(output)
end

.strings_file_type(path:) ⇒ Symbol

Returns the type of a ‘.strings` file (XML, binary or ASCII)

Parameters:

  • path (String)

    The path to the ‘.strings` file to check

Returns:

  • (Symbol)

    The file format used by the ‘.strings` file. Can be one of:

    • ‘:text` for the ASCII-plist file format (containing typical `“key” = “value”;` lines)

    • ‘:xml` for XML plist file format (can be used if machine-generated, especially since there’s no official way/tool to generate the ASCII-plist file format as output)

    • ‘:binary` for binary plist file format (usually only true for `.strings` files converted by Xcode at compile time and included in the final `.app`/`.ipa`)

    • ‘nil` if the file does not exist or is neither of those format (e.g. not a `.strings` file at all)



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb', line 21

def self.strings_file_type(path:)
  return :text if File.empty?(path) # If completely empty file, consider it as a valid `.strings` files in textual format

  # Start by checking it seems like a valid property-list file (and not e.g. an image or plain text file)
  _, status = Open3.capture2('/usr/bin/plutil', '-lint', path)
  return nil unless status.success?

  # If it is a valid property-list file, determine the actual format used
  format_desc, status = Open3.capture2('/usr/bin/file', path)
  return nil unless status.success?

  case format_desc
  when /Apple binary property list/ then return :binary
  when /XML/ then return :xml
  when /text/ then return :text
  end
end