Class: Fastlane::Actions::AndroidPruneOrphanedTranslationsAction

Inherits:
Action
  • Object
show all
Defined in:
lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_prune_orphaned_translations_action.rb

Constant Summary collapse

LOCALE_VALUES_DIR_REGEX =

Matches an Android values-<qualifier> directory whose qualifier is a locale: a 2- or 3-letter language code (fr, kmr), an optional region (pt-rBR, es-r419), or a BCP-47 b+ form (b+sr+Latn), so non-locale qualifier dirs (e.g. values-night, values-v21, values-land) are left untouched.

/\Avalues-(?:b\+[a-zA-Z]+(?:\+[a-zA-Z0-9]+)*|[a-z]{2,3}(?:-r(?:[A-Z]{2}|\d{3}))?)\z/
UI_MODE_QUALIFIERS =

Android UI-mode qualifiers, which are never locales: https://developer.android.com/guide/topics/resources/providing-resources#UiModeQualifier car is indistinguishable by shape from a 3-letter ISO 639 language code, so LOCALE_VALUES_DIR_REGEX would otherwise treat values-car as a locale and prune its (valid) car-mode resources. There's no purely syntactic way to tell the two apart, so we exclude the documented UI-mode qualifiers explicitly. (car is the only one short enough to currently match the regex; the rest are listed to capture the full set.)

%w[car desk television appliance watch vrheadset].freeze
DEFAULT_SOURCE_STRINGS_FILE_NAME =
'strings.xml'
DEFAULT_SOURCE_STRINGS_RELATIVE_PATH =
File.join('values', DEFAULT_SOURCE_STRINGS_FILE_NAME).freeze

Documentation collapse

Class Method Summary collapse

Class Method Details

.authorsObject



135
136
137
# File 'lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_prune_orphaned_translations_action.rb', line 135

def self.authors
  ['Automattic']
end

.available_optionsObject



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
# File 'lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_prune_orphaned_translations_action.rb', line 103

def self.available_options
  [
    FastlaneCore::ConfigItem.new(
      key: :res_dir,
      description: "Path to the Android project's `res` directory containing the `values-*` locale subdirectories to prune",
      type: String,
      optional: false,
      verify_block: proc do |value|
        source_path = File.join(value, DEFAULT_SOURCE_STRINGS_RELATIVE_PATH)
        UI.user_error!("Source strings file not found: `#{source_path}`") unless File.file?(source_path)
      end
    ),
    FastlaneCore::ConfigItem.new(
      key: :additional_source_strings_paths,
      description: 'Paths to additional default `strings.xml` files whose keys should also be treated as valid ' \
                   '(e.g. a base module that the pruned `res_dir` overlays at build time)',
      type: Array,
      optional: true,
      default_value: [],
      verify_block: proc do |value|
        value.each do |path|
          UI.user_error!("Source strings file not found: `#{path}`") unless File.file?(path)
        end
      end
    ),
  ]
end

.collect_keys(paths) ⇒ Set<String>

Collects the set of resource names (string, string-array, plurals, …) declared in the given strings files.

Parameters:

  • paths (Array<String>)

    The strings.xml files to read the valid keys from.

Returns:

  • (Set<String>)

    The set of declared resource names.



52
53
54
55
56
57
# File 'lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_prune_orphaned_translations_action.rb', line 52

def self.collect_keys(paths)
  paths.each_with_object(Set.new) do |path, keys|
    doc = File.open(path) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
    doc.xpath('/resources/*[@name]').each { |node| keys << node['name'] }
  end
end

.descriptionObject



86
87
88
# File 'lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_prune_orphaned_translations_action.rb', line 86

def self.description
  'Removes translations whose keys are not present in the source strings, to avoid Lint `ExtraTranslation` errors'
end

.detailsObject



90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_prune_orphaned_translations_action.rb', line 90

def self.details
  <<~DETAILS
    When downloading translations from GlotPress, the export may include keys that are no longer present in
    the app's source strings (e.g. removed or renamed since the GlotPress source was last synced). Android
    Lint flags these orphaned translations as `ExtraTranslation` errors.

    This action removes — from every `values-*/strings.xml` under `res_dir` — any `<string>`, `<string-array>`
    or `<plurals>` entry whose `name` is not declared in the default `values/strings.xml` of `res_dir`,
    optionally unioned with `additional_source_strings_paths` (useful when a product flavor overlays a base
    module's resources at build time, so the base module's keys are valid too).
  DETAILS
end

.is_supported?(platform) ⇒ Boolean

Returns:

  • (Boolean)


139
140
141
# File 'lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_prune_orphaned_translations_action.rb', line 139

def self.is_supported?(platform)
  platform == :android
end

.prune_file(file:, valid_keys:) ⇒ Array<String>

Removes from file any resource entry whose name is not in valid_keys, preserving the rest of the file's formatting (so the change shows up as a minimal diff).

Parameters:

  • file (String)

    The locale strings.xml file to prune.

  • valid_keys (Set<String>)

    The set of names that are allowed to remain.

Returns:

  • (Array<String>)

    The names of the entries that were pruned.



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_prune_orphaned_translations_action.rb', line 65

def self.prune_file(file:, valid_keys:)
  doc = File.open(file) { |f| Nokogiri::XML(f, nil, Encoding::UTF_8.to_s) }
  orphans = doc.xpath('/resources/*[@name]').reject { |node| valid_keys.include?(node['name']) }
  return [] if orphans.empty?

  names = orphans.map { |node| node['name'] }
  orphans.each do |node|
    # Drop the indentation/newline text node right before the element too, to avoid leaving a blank line.
    previous = node.previous_sibling
    previous.remove if previous&.text? && previous.text.strip.empty?
    node.remove
  end

  File.open(file, 'w') { |f| doc.write_to(f, encoding: Encoding::UTF_8.to_s, indent: 4) }
  names
end

.return_valueObject



131
132
133
# File 'lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_prune_orphaned_translations_action.rb', line 131

def self.return_value
  'The total number of orphaned translation entries that were pruned'
end

.run(params) ⇒ Object



25
26
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/actions/android/android_prune_orphaned_translations_action.rb', line 25

def self.run(params)
  res_dir = params[:res_dir]
  source_paths = [File.join(res_dir, DEFAULT_SOURCE_STRINGS_RELATIVE_PATH)] + params[:additional_source_strings_paths]
  valid_keys = collect_keys(source_paths)

  locale_files = Dir.glob(File.join(res_dir, 'values-*', DEFAULT_SOURCE_STRINGS_FILE_NAME)).select do |file|
    dir_name = File.basename(File.dirname(file))
    dir_name.match?(LOCALE_VALUES_DIR_REGEX) && !UI_MODE_QUALIFIERS.include?(dir_name.delete_prefix('values-'))
  end
  total_pruned = 0

  locale_files.each do |file|
    pruned = prune_file(file: file, valid_keys: valid_keys)
    next if pruned.empty?

    total_pruned += pruned.count
    UI.message("Pruned #{pruned.count} orphaned entries from `#{file}`: #{pruned.join(', ')}")
  end

  UI.success("Pruned #{total_pruned} orphaned translation entries across #{locale_files.count} locale file(s).")
  total_pruned
end