Class: Fastlane::Helper::Ios::L10nHelper
- Inherits:
-
Object
- Object
- Fastlane::Helper::Ios::L10nHelper
- Defined in:
- lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb
Class Method Summary collapse
-
.download_glotpress_export_file(project_url:, locale:, filters:, destination:) ⇒ Object
Downloads the export from GlotPress for a given locale and given filters.
-
.generate_strings_file_from_hash(translations:, output_path:) ⇒ Object
Generate a
.stringsfile from a dictionary of string translations. -
.merge_strings(paths:, output_path:) ⇒ Array<String>
Merge the content of multiple
.stringsfiles into a new.stringstext file. -
.read_strings_file_as_hash(path:) ⇒ Hash<String,String>
Return the list of translations in a
.stringsfile. -
.read_utf8_lines(file) { ... } ⇒ Object
Read a file line by line and iterate over it (just like
File.readlinesdoes), except that it also detects the encoding used by the file (using the BOM if present) when reading it, and then convert each line to UTF-8 before yielding it. -
.strings_file_type(path:, assume_valid: false) ⇒ Symbol
Returns the type of a
.stringsfile (XML, binary or ASCII).
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.
195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb', line 195 def self.download_glotpress_export_file(project_url:, locale:, filters:, destination:) query_params = (filters || {}).transform_keys { |k| "filters[#{k}]" }.merge(format: 'strings') url = "#{project_url.chomp('/')}/#{locale}/default/export-translations/?#{URI.encode_www_form(query_params)}" begin Fastlane::Helper::GlotPressDownloader.download( url: url, locale: locale, auto_retry: true ) do |response_body| if destination.is_a?(String) File.write(destination, response_body) else destination.write(response_body) end end rescue StandardError => e UI.error "Error downloading locale `#{locale}` — #{e.} (#{url})" nil end end |
.generate_strings_file_from_hash(translations:, output_path:) ⇒ Object
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)
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb', line 167 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>
For now, this method only supports merging .strings file in :text format
and basically concatenates the files (+ checking for duplicates in the process)
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.
Dictionary- and array-valued entries ("k" = { … };, "k" = ( … );, nesting allowed) are
prefixed on their outer key with the value preserved verbatim. If a file still holds some
construct the tokenizer can't rewrite, its lines are copied through unprefixed with a warning
(and its keys are then bookkept unprefixed too, so the reported duplicates stay accurate)
rather than aborting the whole merge.
Merge the content of multiple .strings files into a new .strings text file.
89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb', line 89 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 raw_keys = read_strings_file_as_hash(path: input_file).keys tmp_file.write("/* MARK: - #{File.basename(input_file)} */\n\n") # Add the prefix to every key. We tokenize via `StringsFileValidationHelper.prefix_keys` rather than # matching keys with a line-based regex, so that keys are found regardless of where `.strings` comments # sit (e.g. `CFBundleName /* note */ = WordPress;`) and `key = value`-looking text inside a comment is # left alone. It also handles dictionary/array values (`"k" = { … };`) — prefixing the outer key and # copying the value verbatim. lines = read_utf8_lines(input_file) applied_prefix = prefix begin lines = Fastlane::Helper::Ios::StringsFileValidationHelper.prefix_keys(lines: lines, prefix: prefix) rescue StandardError => e # `plutil` may still accept a construct the tokenizer can't rewrite: it parses fine (so the file # clears the `:text` gate above) yet `prefix_keys` raises on it. Fail soft: copy this file's lines # through unprefixed rather than aborting the whole merge — mirroring the scanner path, where # `scan_for_duplicate_keys` returns `:unscannable` instead of crashing the lane. `lines` is untouched # by the raise (the assignment above never completes), so it still holds the original file contents, # and `applied_prefix` records that the keys went out *unprefixed* so the bookkeeping below matches. applied_prefix = '' UI.important("Could not add prefix `#{prefix}` to the keys in `#{input_file}` (#{e.}); copying its lines through unprefixed.") end # Bookkeep the keys as they were actually written — prefixed, or unprefixed on the fail-soft path. # Doing this *after* the rewrite keeps the reported duplicates consistent with the merged file even # when prefixing fell back, so a genuine collision is still surfaced rather than silently collapsed. string_keys = raw_keys.map { |k| "#{applied_prefix}#{k}" } duplicates += (string_keys & all_keys_found) # Find duplicates using Array intersection, and add those to duplicates list all_keys_found += string_keys lines.each { |line| tmp_file.write(line) } 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.
147 148 149 150 151 152 153 154 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb', line 147 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 |
.read_utf8_lines(file) { ... } ⇒ Object
Read a file line by line and iterate over it (just like File.readlines does),
except that it also detects the encoding used by the file (using the BOM if present) when reading it,
and then convert each line to UTF-8 before yielding it
This is particularly useful if you need to then use a RegExp to match part of the lines you're iterating over,
as the RegExp (which will typically be UTF-8) and the string you're matching with it have to use the same encoding
(otherwise we would get a Encoding::CompatibilityError)
remember to use the u flag on it (/…/u) to make it UTF-8-aware too.
62 63 64 65 66 67 68 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb', line 62 def self.read_utf8_lines(file) # Be sure to guess file encoding using the Byte-Order-Mark, and fallback to UTF-8 if there's no BOM. File.readlines(file, mode: 'rb:BOM|UTF-8').map do |line| # Ensure the line is re-encoded to UTF-8 regardless of the encoding that was used in the input file line.encode(Encoding::UTF_8) end end |
.strings_file_type(path:, assume_valid: false) ⇒ Symbol
Returns the type of a .strings file (XML, binary or ASCII)
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/ios/ios_l10n_helper.rb', line 27 def self.strings_file_type(path:, assume_valid: false) 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). # A caller that has already parsed the file can skip this redundant check via `assume_valid: true`. unless assume_valid _, status = Open3.capture2('/usr/bin/plutil', '-lint', path) return nil unless status.success? end # 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 :binary when /XML/ then :xml when /text/ then :text end end |