Skip to content

Add android_prune_orphaned_translations action#734

Open
oguzkocer wants to merge 13 commits into
trunkfrom
add/prune-orphaned-translations
Open

Add android_prune_orphaned_translations action#734
oguzkocer wants to merge 13 commits into
trunkfrom
add/prune-orphaned-translations

Conversation

@oguzkocer

Copy link
Copy Markdown
Contributor

What does it do?

Adds an android_prune_orphaned_translations action that removes orphaned translations from downloaded locale files, so they don't trip Android Lint's ExtraTranslation rule.

When translations are downloaded from GlotPress, the export can include keys that are no longer in the app's source strings (removed or renamed since the GlotPress source was last synced).

For every values-*/strings.xml under res_dir, the action removes any <string>, <string-array> or <plurals> whose name is not declared in the source strings — the res dir's default values/strings.xml, optionally unioned with additional_source_strings_paths (for flavors that overlay a base module's resources at build time). It re-serializes with the same options the download uses, so the change is a minimal diff.

android_prune_orphaned_translations(
  res_dir: 'WordPress/src/jetpack/res',
  additional_source_strings_paths: ['WordPress/src/main/res/values/strings.xml']
)

Checklist before requesting a review

  • Run bundle exec rubocop to test for code style violations and recommendations.
  • Add Unit Tests (aka specs/*_spec.rb) if applicable.
  • Run bundle exec rspec to run the whole test suite and ensure all your tests pass.
  • Make sure you added an entry in the CHANGELOG.md file to describe your changes under the appropriate existing ### subsection of the existing ## Trunk section.
  • If applicable, add an entry in the MIGRATION.md file to describe how the changes will affect the migration from the previous major version and what the clients will need to change and consider.

Removes <string>/<string-array>/<plurals> entries from values-*/strings.xml
whose key is not declared in the source strings (the res dir's default
values/strings.xml, optionally unioned with additional_source_strings_paths).
Useful after downloading translations from GlotPress to avoid Lint
ExtraTranslation errors from keys removed/renamed since the source was synced.
@oguzkocer oguzkocer added the enhancement New feature or request label Jun 17, 2026
@oguzkocer oguzkocer marked this pull request as ready for review June 17, 2026 22:44
@oguzkocer oguzkocer requested a review from a team as a code owner June 17, 2026 22:44
@mokagio mokagio requested a review from Copilot June 18, 2026 01:28

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Fastlane Android action to remove orphaned resource entries from downloaded values-*/strings.xml locale files, preventing Android Lint ExtraTranslation errors after GlotPress exports include stale keys.

Changes:

  • Introduces android_prune_orphaned_translations action that computes valid keys from source values/strings.xml (+ optional additional sources) and prunes invalid entries from locale files.
  • Adds RSpec coverage for pruning behavior, including the “additional source strings” overlay case.
  • Documents the new action in CHANGELOG.md.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
lib/fastlane/plugin/wpmreleasetoolkit/actions/android/android_prune_orphaned_translations_action.rb Implements the new pruning action using Nokogiri XML parsing/serialization.
spec/android_prune_orphaned_translations_spec.rb Adds unit tests validating pruning behavior and reporting counts.
CHANGELOG.md Adds a Trunk entry describing the new action and its intent.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread spec/android_prune_orphaned_translations_spec.rb
Add explicit `require 'fileutils'` to the spec (matches existing specs and is
robust if spec_helper's transitive load changes). Declined the suggested
`require 'set'` in the action: Set is autoloaded on Ruby >= 3.2 (the gem's
floor) and rubocop's Lint/RedundantRequireStatement would reject it.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

Comment on lines +86 to +91
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
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like a reasonable suggestion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was originally addressed in e7789a4, but after further investigation I realized that approach wasn't following the repo's conventions, so it's now addressed in 2a7813e.

Comment on lines +92 to +99
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: []
),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also seems like a good addition.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was originally addressed in e7789a4, but after further investigation I realized that approach wasn't following the repo's conventions, so it's now addressed in 2a7813e.

source_paths = [File.join(res_dir, 'values', 'strings.xml')] + params[:additional_source_strings_paths]
valid_keys = collect_keys(source_paths)

locale_files = Dir.glob(File.join(res_dir, 'values-*', 'strings.xml'))
Restrict the glob to Android locale-qualifier directories (language, optional
region, or BCP-47 b+ form) so non-locale qualifier dirs like values-night or
values-v21 are left untouched. Addresses Copilot review on #734.
Fail with a user-facing message when the res dir's default values/strings.xml
or any additional_source_strings_paths entry is missing, instead of a
low-level Errno::ENOENT. Addresses Copilot review on #734.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread CHANGELOG.md Outdated
oguzkocer and others added 2 commits June 18, 2026 10:21
Android's `car` qualifier represents UI mode, not a locale, so the pruning action should leave `values-car` resources untouched.
This is intentionally test-only coverage; the production fix is not included.

---

Generated with the help of Codex, https://openai.com/codex

Co-Authored-By: Codex GPT-5 <noreply@openai.com>
Comment on lines +12 to +16
LOCALE_VALUES_DIR_REGEX = /\Avalues-(?:b\+[a-zA-Z]+(?:\+[a-zA-Z0-9]+)*|[a-z]{2,3}(?:-r(?:[A-Z]{2}|\d{3}))?)\z/

def self.run(params)
res_dir = params[:res_dir]
source_paths = [File.join(res_dir, 'values', 'strings.xml')] + params[:additional_source_strings_paths]

@mokagio mokagio Jun 26, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nitpick. The relative path values/strings.xml appear a number of times. Have you considered DRYing it? Example:

Suggested change
LOCALE_VALUES_DIR_REGEX = /\Avalues-(?:b\+[a-zA-Z]+(?:\+[a-zA-Z0-9]+)*|[a-z]{2,3}(?:-r(?:[A-Z]{2}|\d{3}))?)\z/
def self.run(params)
res_dir = params[:res_dir]
source_paths = [File.join(res_dir, 'values', 'strings.xml')] + params[:additional_source_strings_paths]
LOCALE_VALUES_DIR_REGEX = /\Avalues-(?:b\+[a-zA-Z]+(?:\+[a-zA-Z0-9]+)*|[a-z]{2,3}(?:-r(?:[A-Z]{2}|\d{3}))?)\z/
DEFAULT_SOURCE_STRINGS_FILE_NAME = 'strings.xml'
DEFAULT_SOURCE_STRINGS_RELATIVE_PATH = File.join('values', DEFAULT_SOURCE_STRINGS_FILE_NAME)
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]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 904e2de.

end
valid_keys = collect_keys(source_paths)

locale_files = Dir.glob(File.join(res_dir, 'values-*', 'strings.xml')).select do |file|

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Following up on my nitpick above

Suggested change
locale_files = Dir.glob(File.join(res_dir, 'values-*', 'strings.xml')).select do |file|
locale_files = Dir.glob(File.join(res_dir, 'values-*', DEFAULT_SOURCE_STRINGS_FILE_NAME)).select do |file|

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 904e2de.

Comment thread CHANGELOG.md Outdated

@mokagio mokagio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good. Thanks you for implementing this and your other recent work in the tool. Brought me back to the old days...

I left a few of suggestions and nitpicks. I'm happy for this to land as is and to follow up on those myself if it speeds along adoption in WordPress Android.

Comment on lines +9 to +12
# Matches an Android `values-<qualifier>` directory whose qualifier is a locale (a language code, optional
# region, or a BCP-47 `b+` form), so non-locale qualifier dirs (e.g. `values-night`, `values-v21`,
# `values-land`) are left untouched.
LOCALE_VALUES_DIR_REGEX = /\Avalues-(?:b\+[a-zA-Z]+(?:\+[a-zA-Z0-9]+)*|[a-z]{2,3}(?:-r(?:[A-Z]{2}|\d{3}))?)\z/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My AI noticed that values-car would pass this RegEx despite not being a valid locale folder.

I'm not familiar with how values- work in Android, but I asked for documentation and this came up: https://developer.android.com/guide/topics/resources/providing-resources#UiModeQualifier where car is listed as a qualifier for UI mode.

There's a test showing this limitation at https://github.com/wordpress-mobile/release-toolkit/pull/746/changes#diff-fc5d87ba15967a529eaa6080f6bc0f2313e7133e68f21204d26a76aee6540154R92-R110

I don't consider this a blocker to merging because we know that the action will be used on WordPress Android first, which does not have a car version.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#746 regression test has been merged and I've opened #747 to address the issue.

oguzkocer and others added 4 commits June 29, 2026 13:23
Move the existence checks for the res dir's default `values/strings.xml`
and each `additional_source_strings_paths` entry into `verify_block` procs
on their config items, matching the repo convention for input path
validation (e.g. `ios_extract_keys_from_strings_files`).
…ggestion

Co-authored-by: Gio Lodi <gio@mokacoding.com>
…uggestion

Co-authored-by: Gio Lodi <gio@mokacoding.com>
Extract the repeated `values/strings.xml` path into
DEFAULT_SOURCE_STRINGS_FILE_NAME and DEFAULT_SOURCE_STRINGS_RELATIVE_PATH,
and use them in run, the locale glob, and the res_dir verify_block.
@oguzkocer

Copy link
Copy Markdown
Contributor Author

@mokagio Thank you for the review! All comments should be addressed now. I know you've already approved the PR, but it might be worth another look since there are a few new commits. I've also pinged you for a review in #747 which is needed to land this PR since the failing regression test is addressed there.

oguzkocer and others added 2 commits July 2, 2026 18:25
The locale-detection regex's 2-3 letter language branch also matches
Android UI-mode qualifiers like `values-car`, which would prune valid
car-mode resources. `car` is also a valid 3-letter ISO 639 language
code and cannot be distinguished from a locale by shape alone, so
exclude the documented UI-mode qualifiers explicitly.

Changes:
- Add UI_MODE_QUALIFIERS exclusion list and apply it in locale detection
- Add a values-kmr test covering 3-letter legacy locales
- Note locale-only scope in the CHANGELOG entry
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants