Module: Fastlane::Helper::Android::LocalizeHelper

Defined in:
lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb

Constant Summary collapse

LIB_SOURCE_XML_ATTR =
'a8c-src-lib'.freeze

Downloading translations from GlotPress collapse

Class Method Summary collapse

Class Method Details

.add_xml_attributes!(string_node, library) ⇒ Object

Adds the appropriate XML attributes to an XML ‘<string>` node according to library configuration



37
38
39
40
41
42
43
44
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb', line 37

def self.add_xml_attributes!(string_node, library)
  if library[:add_ignore_attr] == true
    existing_ignores = (string_node['tools:ignore'] || '').split(',')
    existing_ignores.append('UnusedResources') unless existing_ignores.include?('UnusedResources')
    string_node['tools:ignore'] = existing_ignores.join(',')
  end
  string_node[LIB_SOURCE_XML_ATTR] = library[:source_id] unless library[:source_id].nil?
end

.create_available_languages_file(res_dir:, locale_codes:) ⇒ Object

Create the ‘available_languages.xml` file.

Parameters:

  • res_dir (String)

    The relative path to the ‘…/src/main/res` directory

  • locale_codes (Array<String>)

    The list of locale codes to include in the available_languages.xml file



210
211
212
213
214
215
216
217
218
219
220
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb', line 210

def self.create_available_languages_file(res_dir:, locale_codes:)
  doc = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
    xml.comment('Warning: Auto-generated file, do not edit.')
    xml.resources('xmlns:tools': 'http://schemas.android.com/tools') do
      xml.send(:'string-array', name: 'available_languages', translatable: 'false', 'tools:ignore': 'InconsistentArrays') do
        locale_codes.each { |code| xml.item(code.gsub('-r', '_')) }
      end
    end
  end
  File.write(File.join(res_dir, 'values', 'available_languages.xml'), doc.to_xml)
end

.download_from_glotpress(res_dir:, glotpress_project_url:, locales_map:, glotpress_filters: { status: 'current' }) ⇒ Object

Download translations from GlotPress

Parameters:

  • res_dir (String)

    The relative path to the ‘…/src/main/res` directory.

  • glotpress_project_url (String)

    The base URL to the glotpress project to download the strings from.

  • glotpress_filters (Hash{String=>String}, Array) (defaults to: { status: 'current' })

    The filters to apply when exporting strings from GlotPress. Typical examples include ‘{ status: ’current’ }‘ or `{ status: ’review’ }‘. If an array of Hashes is provided instead of a single Hash, this method will perform as many export requests as items in this array, then merge all the results – useful for OR-ing multiple filters.

  • locales_map (Array<Hash{Symbol=>String}>)

    An array of locales to download. Each item in the array must be a Hash with keys ‘:glotpress` and `:android` containing the respective locale codes.



235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb', line 235

def self.download_from_glotpress(res_dir:, glotpress_project_url:, locales_map:, glotpress_filters: { status: 'current' })
  glotpress_filters = [glotpress_filters] unless glotpress_filters.is_a?(Array)

  attributes_to_copy = %w[formatted] # Attributes that we want to replicate into translated `string.xml` files
  orig_file = File.join(res_dir, 'values', 'strings.xml')
  orig_xml = File.open(orig_file) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
  orig_attributes = orig_xml.xpath('//string').to_h { |tag| [tag['name'], tag.attributes.select { |k, _| attributes_to_copy.include?(k) }] }

  locales_map.each do |lang_codes|
    all_xml_documents = glotpress_filters.map do |filters|
      UI.message "Downloading translations for '#{lang_codes[:android]}' from GlotPress (#{lang_codes[:glotpress]}) [#{filters}]..."
      download_glotpress_export_file(project_url: glotpress_project_url, locale: lang_codes[:glotpress], filters: filters)
    end.compact
    next if all_xml_documents.empty?

    # Merge all XMLs together
    merged_xml = merge_xml_documents(all_xml_documents)

    # Process XML (text substitutions, replicate attributes, quick-lint string)
    merged_xml.xpath('//string').each do |string_tag|
      apply_substitutions(string_tag)
      orig_attributes[string_tag['name']]&.each { |k, v| string_tag[k] = v }
      quick_lint(string_tag, lang_codes[:android])
    end
    merged_xml.xpath('//string-array/item').each { |item_tag| apply_substitutions(item_tag) }

    # Save
    lang_dir = File.join(res_dir, "values-#{lang_codes[:android]}")
    FileUtils.mkdir_p(lang_dir)
    lang_file = File.join(lang_dir, 'strings.xml')
    File.open(lang_file, 'w') { |f| merged_xml.write_to(f, encoding: Encoding::UTF_8.to_s, indent: 4) }
  end
end

.merge_lib(main, library) ⇒ Boolean

Merge strings from a library into the strings.xml of the main app

Parameters:

  • main (String)

    Path to the main strings.xml file (something like ‘…/res/values/strings.xml`)

  • library (Hash)

    Hash describing the library to merge. The Hash should contain the following keys:

    • ‘:library`: The human readable name of the library, used to display in console messages

    • ‘:strings_path`: The path to the strings.xml file of the library to merge into the main one

    • ‘:exclusions`: An array of strings keys to exclude during merge. Any of those keys from the library’s ‘strings.xml` will be skipped and won’t be merged into the main one.

    • ‘:source_id`: An optional `String` which will be added as the `a8c-src-lib` XML attribute to strings coming from this library, to help identify their source in the merged file.

    • ‘:add_ignore_attr`: If set to `true`, will add `tools:ignore=“UnusedResources”` to merged strings.

Returns:

  • (Boolean)

    True if at least one string from the library has been added to (or has updated) the main strings file.



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb', line 120

def self.merge_lib(main, library)
  UI.message("Merging #{library[:library]} strings into #{main}")
  main_strings_xml = File.open(main) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
  lib_strings_xml = File.open(library[:strings_path]) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }

  updated_count = 0
  untouched_count = 0
  added_count = 0
  skipped_count = 0
  lib_strings_xml.xpath('//string').each do |string_node|
    res = merge_string_node(main_strings_xml, library, string_node)
    case res
    when :updated
      UI.verbose "#{string_node.attr('name')} updated."
      updated_count += 1
    when :found
      untouched_count += 1
    when :added
      UI.verbose "#{string_node.attr('name')} added."
      added_count += 1
    when :skipped
      skipped_count += 1
    else
      UI.user_error!("Internal Error! #{res}")
    end
  end

  File.open(main, 'w:UTF-8') do |f|
    f.write(main_strings_xml.to_xml(indent: 4))
  end

  UI.message("Done (#{added_count} added, #{updated_count} updated, #{untouched_count} untouched, #{skipped_count} skipped).")
  (added_count + updated_count) != 0
end

.merge_string_node(main_strings_xml, library, lib_string_node) ⇒ Object

Merge a single ‘lib_string_node` XML node into the `main_strings_xml“



47
48
49
50
51
52
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
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb', line 47

def self.merge_string_node(main_strings_xml, library, lib_string_node)
  string_name = lib_string_node.attr('name')
  string_content = lib_string_node.content

  # Skip strings in the exclusions list
  return :skipped if skip_string_by_exclusion_list?(library, string_name)

  # Search for the string in the main file
  result = :added
  main_strings_xml.xpath('//string').each do |main_string_node|
    next unless main_string_node.attr('name') == string_name
    # Skip if the string has the content_override tag
    return :skipped if skip_string_by_tag?(main_string_node)

    # If nodes are equivalent, skip
    return :found if lib_string_node =~ main_string_node

    # The string needs an update
    if main_string_node.attr('tools:ignore').nil?
      # No `tools:ignore` attribute; completely replace existing main string node with lib's one
      add_xml_attributes!(lib_string_node, library)
      main_string_node.replace lib_string_node
    else
      # Has the `tools:ignore` flag; update the content without touching the other existing attributes
      add_xml_attributes!(main_string_node, library)
      main_string_node.content = string_content
    end
    return :updated
  end

  # String not found, or removed because needing update and not in the exclusion list: add to the main file
  add_xml_attributes!(lib_string_node, library)
  main_strings_xml.xpath('//string').last.add_next_sibling("\n#{' ' * 4}#{lib_string_node.to_xml.strip}")
  result
end

.skip_string_by_exclusion_list?(library, string_name) ⇒ Boolean

Checks if ‘string_name` is in the exclusion list

Returns:

  • (Boolean)


26
27
28
29
30
31
32
33
34
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb', line 26

def self.skip_string_by_exclusion_list?(library, string_name)
  return false if library[:exclusions].nil?

  skip = library[:exclusions].include?(string_name)
  return false unless skip

  UI.message " - Skipping #{string_name} string"
  true
end

.skip_string_by_tag?(string_node) ⇒ Boolean

Checks if ‘string_node` has the `content_override` flag set

Returns:

  • (Boolean)


15
16
17
18
19
20
21
22
23
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb', line 15

def self.skip_string_by_tag?(string_node)
  skip = string_node.attr('content_override') == 'true' unless string_node.attr('content_override').nil?
  if skip
    UI.message " - Skipping #{string_node.attr('name')} string"
    return true
  end

  false
end

.verify_diff(diff_string, main_strings, lib_strings, library) ⇒ Object



155
156
157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb', line 155

def self.verify_diff(diff_string, main_strings, lib_strings, library)
  return unless diff_string.start_with?('name=')

  diff_string.slice!('name="')

  end_index = diff_string.index('"')
  end_index ||= diff_string.length # Use the whole string if there's no '"'

  diff_string = diff_string.slice(0..(end_index - 1))

  lib_strings.xpath('//string').each do |string_node|
    verify_string(main_strings, library, string_node) if string_node.attr('name') == diff_string
  end
end

.verify_lib(main, library, source_diff) ⇒ Object



170
171
172
173
174
175
176
177
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb', line 170

def self.verify_lib(main, library, source_diff)
  UI.message("Checking #{library[:library]} strings vs #{main}")
  main_strings = File.open(main) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
  lib_strings = File.open(library[:strings_path]) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }

  verify_local_diff(main, library, main_strings, lib_strings)
  verify_pr_diff(main, library, main_strings, lib_strings, source_diff) unless source_diff.nil?
end

.verify_local_diff(main, library, main_strings, lib_strings) ⇒ Object



179
180
181
182
183
184
185
186
187
188
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb', line 179

def self.verify_local_diff(main, library, main_strings, lib_strings)
  `git diff #{main}`.each_line do |line|
    next unless line.start_with?('+ ') || line.start_with?('- ')

    diffs = line.gsub(/\s+/m, ' ').strip.split
    diffs.each do |diff|
      verify_diff(diff, main_strings, lib_strings, library)
    end
  end
end

.verify_pr_diff(main, library, main_strings, lib_strings, source_diff) ⇒ Object



190
191
192
193
194
195
196
197
198
199
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb', line 190

def self.verify_pr_diff(main, library, main_strings, lib_strings, source_diff)
  source_diff.each_line do |line|
    next unless line.start_with?('+ ') || line.start_with?('- ')

    diffs = line.gsub(/\s+/m, ' ').strip.split
    diffs.each do |diff|
      verify_diff(diff, main_strings, lib_strings, library)
    end
  end
end

.verify_string(main_strings_xml, library, lib_string_node) ⇒ Object

Verify a string node from a library has properly been merged into the main one



84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# File 'lib/fastlane/plugin/wpmreleasetoolkit/helper/android/android_localize_helper.rb', line 84

def self.verify_string(main_strings_xml, library, lib_string_node)
  string_name = lib_string_node.attr('name')
  string_content = lib_string_node.content

  # Skip strings in the exclusions list
  return if skip_string_by_exclusion_list?(library, string_name)

  # Search for the string in the main file
  main_strings_xml.xpath('//string').each do |main_string_node|
    next unless main_string_node.attr('name') == string_name
    # Skip if the string has the content_override tag
    return if skip_string_by_tag?(main_string_node)

    # Check if up-to-date
    UI.user_error!("String #{string_name} [#{string_content}] has been updated in the main file but not in the library #{library[:library]}.") if main_string_node.content != string_content
    return
  end

  # String not found and not in the exclusion list
  UI.user_error!("String #{string_name} [#{string_content}] was found in library #{library[:library]} but not in the main file.")
end