Surface issue in apps intents strings localization tooling#25805
Surface issue in apps intents strings localization tooling#25805mokagio wants to merge 9 commits into
Conversation
iOS 18 and later resolve the widget configuration UI's App Intents strings against the app bundle, whose GlotPress-managed Localizable.strings already carries the ios-widget. keys in every locale, so the static copies in the extension bundle only served iOS 17 (verified on 17.5/18.6/26.5 simulators, including that iOS 17 has no fallback to the app bundle). The iOS 17 user base no longer justifies maintaining 34 hand-refreshed translation snapshots, so its widget configuration UI now shows the English defaults.
genstrings cannot parse LocalizedStringResource, so the App Intents strings reached GlotPress through a hand-maintained mirror of the defaultValues in code, which could silently drift and ship a new string English-only. The code freeze now extracts the call sites with xcstringstool (which understands LocalizedStringResource, including comments) and merges them into the upload like the plural originals, making code the single source of truth. Translator comments move into the code via the comment: parameter, and the mirror file is deleted.
app_intents_entries duplicated current_english_values' fresh-catalog sync inline, which also pushed its ABC size past the RuboCop threshold flagged on the PR. Both now delegate to a shared synced_throwaway_catalog helper, and the empty-stringsdata guard now applies to both callers.
`positionalize_untyped_arguments` sat inside `platform :ios do` in `localization_catalog.rb`, so the repo's pure-Ruby minitest convention could not load it. Moving it to an `AppIntentsStrings` module makes it testable and puts it where `.buildkite/commands/test-localization-tooling.sh` already globs for a suite. The logic is unchanged, so the extraction can be reviewed as behaviour-preserving and the tests that follow pin what it does today. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Code Opus 4.8 <noreply@anthropic.com>
Two cases are asserted as they behave today rather than as they should, so a fix flips them deliberately instead of silently: - A value mixing explicit and untyped positions renumbers onto a duplicate index (`"%2$arg and %arg"` yields `%2$@` twice), collapsing two placeholders onto one argument. - An escaped percent immediately before the word "arg" is consumed as a placeholder (`"100%%arg"` yields `100%%1$@`). Neither is reachable from a current call site — no App Intents `defaultValue` interpolates anything yet — which is precisely why they would otherwise surface as mangled translator originals at some future code freeze. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Code Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Gio Lodi <giovanni.lodi42@gmail.com>
The two bug cases asserted what the code does today, which makes the defects permanent: a fix has to edit the tests to land. Inverted, they now fail and go green when the rewrite is fixed. The mixed-form case asserts that no two placeholders resolve to the same argument rather than an exact string, because which index the untyped one should take is a design call for whoever fixes it. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Code Opus 4.8 <noreply@anthropic.com>
--- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Code Opus 4.8 <noreply@anthropic.com>
A test class's name and its cases say what it covers, so the cop only ever buys a comment written to satisfy it — which is what the line this removes was. --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Code Opus 4.8 <noreply@anthropic.com>
| Style/Documentation: | ||
| Exclude: | ||
| - '**/*_test.rb' | ||
|
|
There was a problem hiding this comment.
Could be out of scope for this PR, but Danger -> RuboCop barked at me for not having documentation in the new test file. But, IMHO, test files should not need a documentation header. The file name and the test methods names and implementations should be short and self-explanatory.
|
| App Name | WordPress | |
| Configuration | Release-Alpha | |
| Build Number | 33265 | |
| Version | PR #25805 | |
| Bundle ID | org.wordpress.alpha | |
| Commit | 71305e8 | |
| Installation URL | 208lgenij4nt8 |
|
| App Name | Jetpack | |
| Configuration | Release-Alpha | |
| Build Number | 33265 | |
| Version | PR #25805 | |
| Bundle ID | com.jetpack.alpha | |
| Commit | 71305e8 | |
| Installation URL | 06opvngidk7ao |
There was a problem hiding this comment.
Pull request overview
This PR extracts the App Intents string placeholder-rewriting logic into a plain-Ruby helper module so it can be unit-tested, and adds a minitest suite that codifies (and currently exposes) edge cases that can corrupt English source strings before they reach GlotPress.
Changes:
- Move
positionalize_untyped_argumentsout oflocalization_catalog.rbintoAppIntentsStringsfor direct unit testing. - Add a new minitest suite covering placeholder rewriting behavior, including two edge cases that currently fail.
- Adjust RuboCop configuration related to documentation requirements for test files.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| fastlane/lanes/localization_catalog.rb | Switch to calling the extracted helper module for App Intents placeholder rewriting. |
| fastlane/lanes/app_intents_strings_helper.rb | Introduce a unit-testable module containing positionalize_untyped_arguments. |
| fastlane/lanes/app_intents_strings_helper_test.rb | Add minitest coverage for placeholder rewriting, including regression cases. |
| .rubocop.yml | Exclude test files from Style/Documentation (broad configuration change). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # | ||
| # Two bugs, red in the adjacent suite: the counter advances on placeholders that already carry an explicit | ||
| # position, and an escaped percent before "arg" is consumed as a placeholder. | ||
| def positionalize_untyped_arguments(value) | ||
| index = 0 | ||
| value.gsub(/%(\d+\$)?arg/) do | ||
| index += 1 | ||
| "%#{Regexp.last_match(1) || "#{index}$"}@" | ||
| end | ||
| end |
| require 'minitest/autorun' | ||
| require_relative 'app_intents_strings_helper' | ||
|
|
||
| class AppIntentsStringsHelperTest < Minitest::Test |
| # FAILING. Asserted as an invariant, not an exact string: which index the untyped placeholder should take | ||
| # is the fixer's call, that it must not duplicate an explicit one is not. | ||
| def test_no_two_placeholders_resolve_to_the_same_argument | ||
| indices = positionalize('%2$arg and %arg').scan(/%(\d+)\$@/).flatten | ||
|
|
||
| assert_equal indices.uniq, indices | ||
| end | ||
|
|
||
| # FAILING. `%%` is a literal percent, so "100%arg" is prose, not an interpolation. | ||
| def test_an_escaped_percent_before_arg_is_not_a_placeholder | ||
| assert_equal '100%%arg', positionalize('100%%arg') | ||
| end |
| Style/Documentation: | ||
| Exclude: | ||
| - '**/*_test.rb' | ||
|
|
|
Version |
d523a8f to
5778339
Compare


I finally got to have a look at #25766. Or rather, I first handed it to Opus 4.8 for a look. It found two possible issues and I asked it to add tests to guard against them. They are currently red.
test_no_two_placeholders_resolve_to_the_same_argument— the counter advances on explicitly-positioned placeholders too, so%2$arg and %argrenumbers both onto argument 2 and one interpolation silently disappears from the translated string.test_an_escaped_percent_before_arg_is_not_a_placeholder—100%%argis prose containing a literal percent, but the regex matches the second%and swallows the word, producing100%%1$@and inventing a format argument that doesn't exist.The shared consequence is the same: both corrupt the English originals before they reach GlotPress, so translators work from a broken string and the damage lands in every locale at once, with no compile error and no runtime crash to catch it.
AI-generated details
Description
Stacked on #25766 — review that one first; this PR's diff is only the commits on top.
positionalize_untyped_argumentsis the fiddliest logic in #25766: a regex that renumbers the extraction's untyped%argplaceholders into positional printf specifiers before they reach GlotPress. It sat insideplatform :ios doinlocalization_catalog.rb, which the repo's pure-Ruby minitest convention cannot load, so it shipped untested. This moves it to anAppIntentsStringsmodule — where.buildkite/commands/test-localization-tooling.shalready globs for a suite — and specifies its behaviour.The extraction itself is verbatim, so it reviews as behaviour-preserving.
This suite is intentionally red. Two cases describe bugs the rewrite has today and fail against it; they go green when it is fixed:
"%2$arg and %arg"collapses two placeholders onto argument 2. Asserted as an invariant (no two placeholders resolve to the same argument) rather than an exact string, because which index the untyped one should take is a design call for whoever fixes it."100%%arg"becomes100%%1$@.Neither is reachable today, since no App Intents
defaultValueinterpolates anything yet. That is the argument for specifying them now: the first time one is written, the failure mode is mangled originals sent to translators at a code freeze, with nothing in the loop to catch it.I left the fixes out so the extraction stays reviewable on its own — happy to add them here instead.
Testing instructions
Runs on CI as the
Localization tooling unit testsstep; needs no bundle, Xcode, or network. Expect exactly two failures, both inapp_intents_strings_helper_test.rb; every other suite passes.