From cc70f83d0931255a7aeb949a3d8c6f7c3507322d Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 9 Jul 2026 12:06:14 +1200 Subject: [PATCH 1/9] Stop shipping widget configuration translations for iOS 17 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. --- .../AppIntents/SelectSiteIntent.swift | 14 ++++++-------- .../AppIntents/SiteEntity.swift | 4 ++-- .../Resources/ar.lproj/Localizable.strings | 12 ------------ .../Resources/bg.lproj/Localizable.strings | 12 ------------ .../Resources/cs.lproj/Localizable.strings | 12 ------------ .../Resources/cy.lproj/Localizable.strings | 9 --------- .../Resources/da.lproj/Localizable.strings | 9 --------- .../Resources/de.lproj/Localizable.strings | 12 ------------ .../Resources/en-AU.lproj/Localizable.strings | 12 ------------ .../Resources/en-CA.lproj/Localizable.strings | 12 ------------ .../Resources/en-GB.lproj/Localizable.strings | 12 ------------ .../Resources/en.lproj/Localizable.strings | 12 ------------ .../Resources/es.lproj/Localizable.strings | 12 ------------ .../Resources/fr.lproj/Localizable.strings | 12 ------------ .../Resources/he.lproj/Localizable.strings | 12 ------------ .../Resources/hr.lproj/Localizable.strings | 9 --------- .../Resources/hu.lproj/Localizable.strings | 9 --------- .../Resources/id.lproj/Localizable.strings | 12 ------------ .../Resources/is.lproj/Localizable.strings | 9 --------- .../Resources/it.lproj/Localizable.strings | 12 ------------ .../Resources/ja.lproj/Localizable.strings | 12 ------------ .../Resources/ko.lproj/Localizable.strings | 12 ------------ .../Resources/nb.lproj/Localizable.strings | 9 --------- .../Resources/nl.lproj/Localizable.strings | 12 ------------ .../Resources/pl.lproj/Localizable.strings | 12 ------------ .../Resources/pt-BR.lproj/Localizable.strings | 12 ------------ .../Resources/pt.lproj/Localizable.strings | 9 --------- .../Resources/ro.lproj/Localizable.strings | 12 ------------ .../Resources/ru.lproj/Localizable.strings | 12 ------------ .../Resources/sk.lproj/Localizable.strings | 9 --------- .../Resources/sq.lproj/Localizable.strings | 12 ------------ .../Resources/sv.lproj/Localizable.strings | 12 ------------ .../Resources/th.lproj/Localizable.strings | 9 --------- .../Resources/tr.lproj/Localizable.strings | 12 ------------ .../Resources/zh-Hans.lproj/Localizable.strings | 12 ------------ .../Resources/zh-Hant.lproj/Localizable.strings | 12 ------------ 36 files changed, 8 insertions(+), 391 deletions(-) delete mode 100644 Sources/JetpackStatsWidgets/Resources/ar.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/bg.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/cs.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/cy.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/da.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/de.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/en-AU.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/en-CA.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/en-GB.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/en.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/es.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/fr.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/he.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/hr.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/hu.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/id.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/is.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/it.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/ja.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/ko.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/nb.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/nl.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/pl.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/pt-BR.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/pt.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/ro.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/ru.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/sk.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/sq.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/sv.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/th.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/tr.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/zh-Hans.lproj/Localizable.strings delete mode 100644 Sources/JetpackStatsWidgets/Resources/zh-Hant.lproj/Localizable.strings diff --git a/Modules/Sources/JetpackStatsWidgetsCore/AppIntents/SelectSiteIntent.swift b/Modules/Sources/JetpackStatsWidgetsCore/AppIntents/SelectSiteIntent.swift index a2cafa9a4b81..02f7e55ca74b 100644 --- a/Modules/Sources/JetpackStatsWidgetsCore/AppIntents/SelectSiteIntent.swift +++ b/Modules/Sources/JetpackStatsWidgetsCore/AppIntents/SelectSiteIntent.swift @@ -12,14 +12,12 @@ import AppIntents /// `ios-widget.` prefix, and the downloaded translations land in the app's /// `Localizable.strings` under those prefixed keys. /// -/// The keys must resolve in two bundles, because the OS picks a different one depending on -/// version (verified on simulators): iOS 26 resolves the widget configuration UI's strings -/// against the app bundle (whose GlotPress-managed `Localizable.strings` carries the prefixed -/// keys in every locale), while iOS 17 resolves against the widget extension bundle, which -/// ships static copies of the same prefixed keys in -/// `Sources/JetpackStatsWidgets/Resources/.lproj/Localizable.strings`. -/// A nested SPM package resource bundle is never consulted, so the strings cannot live in -/// this package. +/// iOS 18 and later resolve the widget configuration UI's strings against the app bundle, +/// whose GlotPress-managed `Localizable.strings` carries the prefixed keys in every locale +/// (verified on simulators; a nested SPM package resource bundle is never consulted). +/// iOS 17 resolves against the widget extension bundle only, with no fallback to the app +/// bundle, so it shows the English default values; we accept English there rather than +/// shipping per-locale copies in the extension. public struct SelectSiteIntent: WidgetConfigurationIntent, CustomIntentMigratedAppIntent { public static let intentClassName = "SelectSiteIntent" diff --git a/Modules/Sources/JetpackStatsWidgetsCore/AppIntents/SiteEntity.swift b/Modules/Sources/JetpackStatsWidgetsCore/AppIntents/SiteEntity.swift index cf5dbaf3dd9f..56bfa768cd4e 100644 --- a/Modules/Sources/JetpackStatsWidgetsCore/AppIntents/SiteEntity.swift +++ b/Modules/Sources/JetpackStatsWidgetsCore/AppIntents/SiteEntity.swift @@ -7,8 +7,8 @@ import Foundation /// the `identifier` of the legacy SiriKit `Site` object so that widget configurations created /// before the App Intents migration keep resolving to the same site. /// -/// The "ios-widget.ILcGmf" localization key resolves against the app bundle on iOS 26 and -/// the widget extension bundle on iOS 17; see `SelectSiteIntent` for the details. +/// The "ios-widget.ILcGmf" localization key resolves against the app bundle on iOS 18 and +/// later; see `SelectSiteIntent` for the details. public struct SiteEntity: AppEntity { public static let typeDisplayRepresentation = TypeDisplayRepresentation( name: LocalizedStringResource("ios-widget.ILcGmf", defaultValue: "Site") diff --git a/Sources/JetpackStatsWidgets/Resources/ar.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/ar.lproj/Localizable.strings deleted file mode 100644 index a8f732c074ed..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/ar.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "تحديد موقع"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "الموقع"; diff --git a/Sources/JetpackStatsWidgets/Resources/bg.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/bg.lproj/Localizable.strings deleted file mode 100644 index 552c82bf3b20..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/bg.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Select Site"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "Сайт"; diff --git a/Sources/JetpackStatsWidgets/Resources/cs.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/cs.lproj/Localizable.strings deleted file mode 100644 index 325ea1843745..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/cs.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Vybrat web"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "Web"; diff --git a/Sources/JetpackStatsWidgets/Resources/cy.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/cy.lproj/Localizable.strings deleted file mode 100644 index d196ac9d5a8f..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/cy.lproj/Localizable.strings +++ /dev/null @@ -1,9 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Select Site"; diff --git a/Sources/JetpackStatsWidgets/Resources/da.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/da.lproj/Localizable.strings deleted file mode 100644 index d196ac9d5a8f..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/da.lproj/Localizable.strings +++ /dev/null @@ -1,9 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Select Site"; diff --git a/Sources/JetpackStatsWidgets/Resources/de.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/de.lproj/Localizable.strings deleted file mode 100644 index c5767a6451cf..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/de.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Website auswählen"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "Website"; diff --git a/Sources/JetpackStatsWidgets/Resources/en-AU.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/en-AU.lproj/Localizable.strings deleted file mode 100644 index 6e01cd7104f1..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/en-AU.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Select Site"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "Site"; diff --git a/Sources/JetpackStatsWidgets/Resources/en-CA.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/en-CA.lproj/Localizable.strings deleted file mode 100644 index 6e01cd7104f1..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/en-CA.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Select Site"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "Site"; diff --git a/Sources/JetpackStatsWidgets/Resources/en-GB.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/en-GB.lproj/Localizable.strings deleted file mode 100644 index 6e01cd7104f1..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/en-GB.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Select Site"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "Site"; diff --git a/Sources/JetpackStatsWidgets/Resources/en.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/en.lproj/Localizable.strings deleted file mode 100644 index 6e01cd7104f1..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/en.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Select Site"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "Site"; diff --git a/Sources/JetpackStatsWidgets/Resources/es.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/es.lproj/Localizable.strings deleted file mode 100644 index 527eaa94847a..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/es.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Seleccionar sitio"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "Sitio"; diff --git a/Sources/JetpackStatsWidgets/Resources/fr.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/fr.lproj/Localizable.strings deleted file mode 100644 index e6080f30a845..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/fr.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Sélectionnez le site"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "Site"; diff --git a/Sources/JetpackStatsWidgets/Resources/he.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/he.lproj/Localizable.strings deleted file mode 100644 index ee7bf6f59ebf..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/he.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "יש לבחור אתר"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "אתר"; diff --git a/Sources/JetpackStatsWidgets/Resources/hr.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/hr.lproj/Localizable.strings deleted file mode 100644 index d196ac9d5a8f..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/hr.lproj/Localizable.strings +++ /dev/null @@ -1,9 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Select Site"; diff --git a/Sources/JetpackStatsWidgets/Resources/hu.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/hu.lproj/Localizable.strings deleted file mode 100644 index d196ac9d5a8f..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/hu.lproj/Localizable.strings +++ /dev/null @@ -1,9 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Select Site"; diff --git a/Sources/JetpackStatsWidgets/Resources/id.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/id.lproj/Localizable.strings deleted file mode 100644 index c62842496db1..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/id.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Pilih Situs"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "Situs"; diff --git a/Sources/JetpackStatsWidgets/Resources/is.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/is.lproj/Localizable.strings deleted file mode 100644 index d196ac9d5a8f..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/is.lproj/Localizable.strings +++ /dev/null @@ -1,9 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Select Site"; diff --git a/Sources/JetpackStatsWidgets/Resources/it.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/it.lproj/Localizable.strings deleted file mode 100644 index c5104f566177..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/it.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Seleziona sito"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "Sito"; diff --git a/Sources/JetpackStatsWidgets/Resources/ja.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/ja.lproj/Localizable.strings deleted file mode 100644 index 90f0604bfdb2..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/ja.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "サイトを選択"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "サイト"; diff --git a/Sources/JetpackStatsWidgets/Resources/ko.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/ko.lproj/Localizable.strings deleted file mode 100644 index b81570c41edb..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/ko.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "사이트 선택"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "사이트"; diff --git a/Sources/JetpackStatsWidgets/Resources/nb.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/nb.lproj/Localizable.strings deleted file mode 100644 index d196ac9d5a8f..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/nb.lproj/Localizable.strings +++ /dev/null @@ -1,9 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Select Site"; diff --git a/Sources/JetpackStatsWidgets/Resources/nl.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/nl.lproj/Localizable.strings deleted file mode 100644 index 81677301fb0c..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/nl.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Selecteer site"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "Site"; diff --git a/Sources/JetpackStatsWidgets/Resources/pl.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/pl.lproj/Localizable.strings deleted file mode 100644 index 5e6e647d8cca..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/pl.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Select Site"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "Witryna"; diff --git a/Sources/JetpackStatsWidgets/Resources/pt-BR.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/pt-BR.lproj/Localizable.strings deleted file mode 100644 index b2b742328c8d..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/pt-BR.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Selecionar site"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "Site"; diff --git a/Sources/JetpackStatsWidgets/Resources/pt.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/pt.lproj/Localizable.strings deleted file mode 100644 index d196ac9d5a8f..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/pt.lproj/Localizable.strings +++ /dev/null @@ -1,9 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Select Site"; diff --git a/Sources/JetpackStatsWidgets/Resources/ro.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/ro.lproj/Localizable.strings deleted file mode 100644 index bd59551e5426..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/ro.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Selectează site-ul"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "Site"; diff --git a/Sources/JetpackStatsWidgets/Resources/ru.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/ru.lproj/Localizable.strings deleted file mode 100644 index 4be547f037ce..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/ru.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Выбрать сайт"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "Сайт"; diff --git a/Sources/JetpackStatsWidgets/Resources/sk.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/sk.lproj/Localizable.strings deleted file mode 100644 index d196ac9d5a8f..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/sk.lproj/Localizable.strings +++ /dev/null @@ -1,9 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Select Site"; diff --git a/Sources/JetpackStatsWidgets/Resources/sq.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/sq.lproj/Localizable.strings deleted file mode 100644 index 746a1090d5d8..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/sq.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Përzgjidhni Sajt"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "Sajt"; diff --git a/Sources/JetpackStatsWidgets/Resources/sv.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/sv.lproj/Localizable.strings deleted file mode 100644 index 6ffe3cc51457..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/sv.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Välj webbplats"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "Webbplats"; diff --git a/Sources/JetpackStatsWidgets/Resources/th.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/th.lproj/Localizable.strings deleted file mode 100644 index d196ac9d5a8f..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/th.lproj/Localizable.strings +++ /dev/null @@ -1,9 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Select Site"; diff --git a/Sources/JetpackStatsWidgets/Resources/tr.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/tr.lproj/Localizable.strings deleted file mode 100644 index 997c8668b402..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/tr.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "Site seçin"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "Site"; diff --git a/Sources/JetpackStatsWidgets/Resources/zh-Hans.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/zh-Hans.lproj/Localizable.strings deleted file mode 100644 index 1882a074f760..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/zh-Hans.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "选择站点"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "站点"; diff --git a/Sources/JetpackStatsWidgets/Resources/zh-Hant.lproj/Localizable.strings b/Sources/JetpackStatsWidgets/Resources/zh-Hant.lproj/Localizable.strings deleted file mode 100644 index ede8667b35b2..000000000000 --- a/Sources/JetpackStatsWidgets/Resources/zh-Hant.lproj/Localizable.strings +++ /dev/null @@ -1,12 +0,0 @@ -/* Static copies of the widget configuration strings, under the same `ios-widget.`-prefixed - keys that GlotPress maintains in the app's Localizable.strings. iOS 17 resolves the widget - configuration UI's App Intents strings against this extension bundle (newer iOS versions - use the app bundle), so these files must ship here. To refresh after translations change, - copy the `ios-widget.gpCwrM` / `ios-widget.ILcGmf` entries from - WordPress/Resources/.lproj/Localizable.strings. */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"ios-widget.gpCwrM" = "選取網站"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ios-widget.ILcGmf" = "網站"; From 7ef094c4962476b9478941f7f0f971f0fb0174d5 Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 9 Jul 2026 12:10:19 +1200 Subject: [PATCH 2/9] Extract the App Intents strings from code at code freeze 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. --- .../AppIntents/SelectSiteIntent.swift | 22 ++++-- .../AppIntents/SiteEntity.swift | 7 +- .../en.lproj/Localizable.strings | 14 ---- fastlane/lanes/localization.rb | 18 ++++- fastlane/lanes/localization_catalog.rb | 67 +++++++++++++++++++ 5 files changed, 106 insertions(+), 22 deletions(-) delete mode 100644 WordPress/JetpackStatsWidgets/en.lproj/Localizable.strings diff --git a/Modules/Sources/JetpackStatsWidgetsCore/AppIntents/SelectSiteIntent.swift b/Modules/Sources/JetpackStatsWidgetsCore/AppIntents/SelectSiteIntent.swift index 02f7e55ca74b..43f337f64dc9 100644 --- a/Modules/Sources/JetpackStatsWidgetsCore/AppIntents/SelectSiteIntent.swift +++ b/Modules/Sources/JetpackStatsWidgetsCore/AppIntents/SelectSiteIntent.swift @@ -8,9 +8,9 @@ import AppIntents /// users' widgets to the default site. /// /// The localization keys are the app-bundle names of the identifiers Xcode generated for the -/// legacy `.intentdefinition` ("gpCwrM", "ILcGmf"): GlotPress uploads them under the -/// `ios-widget.` prefix, and the downloaded translations land in the app's -/// `Localizable.strings` under those prefixed keys. +/// legacy `.intentdefinition` ("gpCwrM", "ILcGmf"): The code freeze extracts these call sites +/// into the GlotPress upload (`generate_app_intents_strings_for_glotpress`), and the downloaded +/// translations land in the app's `Localizable.strings` under the prefixed keys. /// /// iOS 18 and later resolve the widget configuration UI's strings against the app bundle, /// whose GlotPress-managed `Localizable.strings` carries the prefixed keys in every locale @@ -21,13 +21,25 @@ import AppIntents public struct SelectSiteIntent: WidgetConfigurationIntent, CustomIntentMigratedAppIntent { public static let intentClassName = "SelectSiteIntent" - public static let title = LocalizedStringResource("ios-widget.gpCwrM", defaultValue: "Select Site") + public static let title = LocalizedStringResource( + "ios-widget.gpCwrM", + defaultValue: "Select Site", + comment: + "This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for" + ) // The legacy intent was ineligible for Siri suggestions; keep this // configuration-only intent out of Shortcuts and Spotlight the same way. public static let isDiscoverable = false - @Parameter(title: LocalizedStringResource("ios-widget.ILcGmf", defaultValue: "Site")) + @Parameter( + title: LocalizedStringResource( + "ios-widget.ILcGmf", + defaultValue: "Site", + comment: + "This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for" + ) + ) public var site: SiteEntity? public init() {} diff --git a/Modules/Sources/JetpackStatsWidgetsCore/AppIntents/SiteEntity.swift b/Modules/Sources/JetpackStatsWidgetsCore/AppIntents/SiteEntity.swift index 56bfa768cd4e..5a7d5dbcb88c 100644 --- a/Modules/Sources/JetpackStatsWidgetsCore/AppIntents/SiteEntity.swift +++ b/Modules/Sources/JetpackStatsWidgetsCore/AppIntents/SiteEntity.swift @@ -11,7 +11,12 @@ import Foundation /// later; see `SelectSiteIntent` for the details. public struct SiteEntity: AppEntity { public static let typeDisplayRepresentation = TypeDisplayRepresentation( - name: LocalizedStringResource("ios-widget.ILcGmf", defaultValue: "Site") + name: LocalizedStringResource( + "ios-widget.ILcGmf", + defaultValue: "Site", + comment: + "This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for" + ) ) public static var defaultQuery: SiteEntityQuery { SiteEntityQuery() } diff --git a/WordPress/JetpackStatsWidgets/en.lproj/Localizable.strings b/WordPress/JetpackStatsWidgets/en.lproj/Localizable.strings deleted file mode 100644 index cc870542d162..000000000000 --- a/WordPress/JetpackStatsWidgets/en.lproj/Localizable.strings +++ /dev/null @@ -1,14 +0,0 @@ -/* Source strings for the stats widget configuration UI, uploaded to GlotPress under the - "ios-widget." prefix (see MANUALLY_MAINTAINED_STRINGS_FILES in fastlane/lanes/localization.rb). - The keys are the identifiers Xcode generated for the legacy Sites.intentdefinition; they are - kept so the existing GlotPress translations keep matching after the migration to App Intents. - - This file is not shipped in any bundle. At runtime the prefixed keys are resolved from the - app's Localizable.strings (iOS 26) or from the static copies in - Sources/JetpackStatsWidgets/Resources/.lproj/Localizable.strings (iOS 17). */ - -/* This text is used when the user is configuring the iOS widget to suggest them to select the site to configure the widget for */ -"gpCwrM" = "Select Site"; - -/* This text is used when the user is configuring the iOS widget, as a label for the dropdown to select the site to configure it for */ -"ILcGmf" = "Site"; diff --git a/fastlane/lanes/localization.rb b/fastlane/lanes/localization.rb index a6897b7dc1ac..12a30becac68 100644 --- a/fastlane/lanes/localization.rb +++ b/fastlane/lanes/localization.rb @@ -96,8 +96,7 @@ MANUALLY_MAINTAINED_STRINGS_FILES = { File.join('WordPress', 'Resources', 'en.lproj', 'InfoPlist.strings') => 'infoplist.', # For now WordPress and Jetpack share the same InfoPlist.strings File.join('WordPress', 'WordPressDraftActionExtension', 'en.lproj', 'InfoPlist.strings') => 'ios-sharesheet.', # CFBundleDisplayName for the "Save as Draft" share action - File.join('WordPress', 'JetpackDraftActionExtension', 'en.lproj', 'InfoPlist.strings') => 'ios-jetpack-sharesheet.', # CFBundleDisplayName for the "Save to Jetpack" share action - File.join('WordPress', 'JetpackStatsWidgets', 'en.lproj', 'Localizable.strings') => 'ios-widget.' # Strings for the App Intents UI used when configuring the iOS Widget; resolved at runtime via the prefixed keys in the app's Localizable.strings + File.join('WordPress', 'JetpackDraftActionExtension', 'en.lproj', 'InfoPlist.strings') => 'ios-jetpack-sharesheet.' # CFBundleDisplayName for the "Save to Jetpack" share action }.freeze # Remote Swift Packages whose localizable strings we want to extract (they're checked out under Derived Data @@ -195,6 +194,21 @@ def upload_to_app_store_common_params ) end + # Merge the App Intents strings into the same destination. These are extracted from code by + # xcstringstool (genstrings cannot parse LocalizedStringResource) and are transient like the plural + # originals above, but deliberately kept separate from the plurals plumbing — and unlike the plural + # step this is NOT allowed to fail softly: a silently missing App Intents string would ship + # English-only in every locale. + Dir.mktmpdir do |app_intents_tmp| + app_intents_originals = File.join(app_intents_tmp, 'AppIntents.strings') + File.write(app_intents_originals, generate_app_intents_strings_for_glotpress) + + ios_merge_strings_files( + paths_to_merge: { app_intents_originals => '' }, # keys are self-qualified with their ios-* prefixes + destination: File.join(WORDPRESS_EN_LPROJ, 'Localizable.strings') + ) + end + git_commit(path: [WORDPRESS_EN_LPROJ], message: 'Update strings for localization', allow_nothing_to_commit: true) unless skip_commit end diff --git a/fastlane/lanes/localization_catalog.rb b/fastlane/lanes/localization_catalog.rb index a4f45b614fcf..0a1647ebf69b 100644 --- a/fastlane/lanes/localization_catalog.rb +++ b/fastlane/lanes/localization_catalog.rb @@ -4,6 +4,7 @@ require 'tmpdir' require 'fileutils' require_relative 'catalog_helper' +require_relative 'plural_strings_helper' ################################################# # Catalog generation (forward / extraction) @@ -39,6 +40,17 @@ # The custom localization routine to additionally extract (same as the genstrings `routines:` today). CATALOG_LOCALIZATION_ROUTINE = 'AppLocalizedString' +# App Intents display strings (LocalizedStringResource) can't be parsed by genstrings, so the code +# freeze extracts them from these dirs with xcstringstool and merges them into the GlotPress upload +# (see generate_app_intents_strings_for_glotpress). Keys in code are self-qualified with these prefixes. +APP_INTENTS_STRINGS_ROOTS = [ + File.join(PROJECT_ROOT_FOLDER, 'Modules', 'Sources', 'JetpackStatsWidgetsCore', 'AppIntents') +].freeze + +APP_INTENTS_KEY_PREFIXES = [ + 'ios-widget.' +].freeze + platform :ios do # Extracts English source strings from code into Localizable.xcstrings (build-free; replaces genstrings). # @@ -200,4 +212,59 @@ def report_catalog(path, extracted_count:, reconciled_count:) message += " Re-flagged #{reconciled_count} for review (English source changed)." if reconciled_count.positive? UI.success(message) end + + # Extracts the App Intents LocalizedStringResource strings (key, English defaultValue, translator + # comment) from source and returns them as `.strings` file content for the GlotPress merge. The keys + # are already prefixed in code, so the caller merges this with an empty prefix, like the plural + # originals. Fails loudly rather than silently shipping an untranslated intent string. + # Declared as a lane like generate_plural_strings_for_glotpress: the freeze lane calls it as a + # function, and it can be run standalone as a smoke check (prints the count; errors on unprefixed keys). + desc 'Generates the App Intents strings content that the code freeze merges into the GlotPress upload' + lane :generate_app_intents_strings_for_glotpress do + files = catalog_source_files(APP_INTENTS_STRINGS_ROOTS) + UI.user_error!('No App Intents source files found — APP_INTENTS_STRINGS_ROOTS out of date?') if files.empty? + + entries = Dir.mktmpdir do |tmp| + extract_stringsdata(files: files, output_dir: tmp) + app_intents_entries(stringsdata_dir: tmp) + end + UI.user_error!('No App Intents strings extracted') if entries.empty? + + unprefixed = entries.keys.reject { |key| APP_INTENTS_KEY_PREFIXES.any? { |prefix| key.start_with?(prefix) } } + UI.user_error!("App Intents strings without a known prefix (add one, or extend APP_INTENTS_KEY_PREFIXES): #{unprefixed.sort}") unless unprefixed.empty? + + UI.message("Extracted #{entries.count} App Intents strings for the GlotPress upload.") + PluralStrings.serialize_legacy_strings(entries.sort.to_h) # sorted for stable output + end + + # { key => { value:, comment: } } from a scoped extraction, via a throwaway catalog (like + # current_english_values). Entries without an explicit English value are interpolation-only + # resources (e.g. DisplayRepresentation(title: "\(name)")) — not translatable text — and are skipped. + def app_intents_entries(stringsdata_dir:) + Dir.mktmpdir do |tmp| + fresh = File.join(tmp, 'Localizable.xcstrings') + File.write(fresh, "#{JSON.pretty_generate('sourceLanguage' => 'en', 'strings' => {}, 'version' => '1.0')}\n") + stringsdata = stringsdata_files(stringsdata_dir) + UI.user_error!('xcstringstool produced no .stringsdata for the App Intents sources') if stringsdata.empty? + sh('xcrun', 'xcstringstool', 'sync', fresh, *stringsdata.flat_map { |f| ['--stringsdata', f] }) + + JSON.parse(File.read(fresh))['strings'].each_with_object({}) do |(key, entry), acc| + value = entry.dig('localizations', 'en', 'stringUnit', 'value') + acc[key] = { value: positionalize_untyped_arguments(value), comment: entry['comment'] } unless value.nil? + end + end + end + + # The build-free extraction cannot type interpolations (a defaultValue's `\(…)` segments), so it + # emits untyped `%arg` placeholders. Rewrite them as positional printf specifiers (`%1$@`, `%2$@`, + # …), which is what GlotPress translators and the runtime's format-style resolution expect. This is + # only correct for String-valued interpolations, so App Intents defaultValues must interpolate + # preformatted Strings, never raw numbers or dates (see docs/localization.md). + def positionalize_untyped_arguments(value) + index = 0 + value.gsub(/%(\d+\$)?arg/) do + index += 1 + "%#{Regexp.last_match(1) || "#{index}$"}@" + end + end end From d523a8fbc7616c5c0ddef09e438db2ff0e16ed5e Mon Sep 17 00:00:00 2001 From: Tony Li Date: Thu, 9 Jul 2026 14:15:33 +1200 Subject: [PATCH 3/9] Share the throwaway catalog sync between extraction helpers 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. --- fastlane/lanes/localization_catalog.rb | 34 ++++++++++++-------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/fastlane/lanes/localization_catalog.rb b/fastlane/lanes/localization_catalog.rb index 0a1647ebf69b..eee23a339fd3 100644 --- a/fastlane/lanes/localization_catalog.rb +++ b/fastlane/lanes/localization_catalog.rb @@ -184,19 +184,25 @@ def reconcile_changed_sources(stringsdata_dir:) reconciled.count end - # Current English value per key, by syncing the extraction into a throwaway empty catalog (every key is - # 'new', so its English is populated straight from source — which is what `sync` won't do for keys that - # already exist in the real catalog). - def current_english_values(stringsdata_dir) + # Syncs the extracted .stringsdata into a fresh throwaway catalog and returns it parsed. A fresh + # catalog means every key is 'new', so its English value is populated straight from source — which + # is what `sync` won't do for keys that already exist in a persistent catalog. + def synced_throwaway_catalog(stringsdata_dir:) Dir.mktmpdir do |tmp| fresh = File.join(tmp, 'Localizable.xcstrings') File.write(fresh, "#{JSON.pretty_generate('sourceLanguage' => 'en', 'strings' => {}, 'version' => '1.0')}\n") stringsdata = stringsdata_files(stringsdata_dir) + UI.user_error!('xcstringstool produced no .stringsdata') if stringsdata.empty? sh('xcrun', 'xcstringstool', 'sync', fresh, *stringsdata.flat_map { |f| ['--stringsdata', f] }) - english_values(JSON.parse(File.read(fresh))) + JSON.parse(File.read(fresh)) end end + # Current English value per key, from a throwaway-catalog sync of the extraction. + def current_english_values(stringsdata_dir) + english_values(synced_throwaway_catalog(stringsdata_dir: stringsdata_dir)) + end + # { key => English value } for every catalog entry that has one (skips key-as-source entries). def english_values(catalog) catalog['strings'].each_with_object({}) do |(key, entry), acc| @@ -237,21 +243,13 @@ def report_catalog(path, extracted_count:, reconciled_count:) PluralStrings.serialize_legacy_strings(entries.sort.to_h) # sorted for stable output end - # { key => { value:, comment: } } from a scoped extraction, via a throwaway catalog (like - # current_english_values). Entries without an explicit English value are interpolation-only + # { key => { value:, comment: } } from a scoped extraction, via `synced_throwaway_catalog`. + # Entries without an explicit English value are interpolation-only # resources (e.g. DisplayRepresentation(title: "\(name)")) — not translatable text — and are skipped. def app_intents_entries(stringsdata_dir:) - Dir.mktmpdir do |tmp| - fresh = File.join(tmp, 'Localizable.xcstrings') - File.write(fresh, "#{JSON.pretty_generate('sourceLanguage' => 'en', 'strings' => {}, 'version' => '1.0')}\n") - stringsdata = stringsdata_files(stringsdata_dir) - UI.user_error!('xcstringstool produced no .stringsdata for the App Intents sources') if stringsdata.empty? - sh('xcrun', 'xcstringstool', 'sync', fresh, *stringsdata.flat_map { |f| ['--stringsdata', f] }) - - JSON.parse(File.read(fresh))['strings'].each_with_object({}) do |(key, entry), acc| - value = entry.dig('localizations', 'en', 'stringUnit', 'value') - acc[key] = { value: positionalize_untyped_arguments(value), comment: entry['comment'] } unless value.nil? - end + synced_throwaway_catalog(stringsdata_dir: stringsdata_dir)['strings'].each_with_object({}) do |(key, entry), acc| + value = entry.dig('localizations', 'en', 'stringUnit', 'value') + acc[key] = { value: positionalize_untyped_arguments(value), comment: entry['comment'] } unless value.nil? end end From 9f9867202ee671c3c26dd9e47194b263724f871b Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Mon, 20 Jul 2026 11:37:28 +1000 Subject: [PATCH 4/9] Extract App Intents placeholder rewrite into a helper `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 --- fastlane/lanes/app_intents_strings_helper.rb | 24 ++++++++++++++++++++ fastlane/lanes/localization_catalog.rb | 16 ++----------- 2 files changed, 26 insertions(+), 14 deletions(-) create mode 100644 fastlane/lanes/app_intents_strings_helper.rb diff --git a/fastlane/lanes/app_intents_strings_helper.rb b/fastlane/lanes/app_intents_strings_helper.rb new file mode 100644 index 000000000000..f4653c058fb1 --- /dev/null +++ b/fastlane/lanes/app_intents_strings_helper.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# Logic for the App Intents (`LocalizedStringResource`) leg of the GlotPress upload. Plain Ruby with no +# fastlane dependencies, so it's unit-testable directly — the lanes in `localization_catalog.rb` call into it. +module AppIntentsStrings + module_function + + # The build-free extraction cannot type interpolations (a defaultValue's `\(…)` segments), so it + # emits untyped `%arg` placeholders. Rewrite them as positional printf specifiers (`%1$@`, `%2$@`, + # …), which is what GlotPress translators and the runtime's format-style resolution expect. This is + # only correct for String-valued interpolations, so App Intents defaultValues must interpolate + # preformatted Strings, never raw numbers or dates (see docs/localization.md). + # + # Known limitation: the counter advances on every placeholder, including ones that already carry an + # explicit position, so a value mixing the two forms ("%2$arg and %arg") renumbers onto a duplicate + # index. Pinned by `test_known_bug_*` in the adjacent suite; no current call site mixes the forms. + def positionalize_untyped_arguments(value) + index = 0 + value.gsub(/%(\d+\$)?arg/) do + index += 1 + "%#{Regexp.last_match(1) || "#{index}$"}@" + end + end +end diff --git a/fastlane/lanes/localization_catalog.rb b/fastlane/lanes/localization_catalog.rb index eee23a339fd3..6e1888eeb251 100644 --- a/fastlane/lanes/localization_catalog.rb +++ b/fastlane/lanes/localization_catalog.rb @@ -3,6 +3,7 @@ require 'json' require 'tmpdir' require 'fileutils' +require_relative 'app_intents_strings_helper' require_relative 'catalog_helper' require_relative 'plural_strings_helper' @@ -249,20 +250,7 @@ def report_catalog(path, extracted_count:, reconciled_count:) def app_intents_entries(stringsdata_dir:) synced_throwaway_catalog(stringsdata_dir: stringsdata_dir)['strings'].each_with_object({}) do |(key, entry), acc| value = entry.dig('localizations', 'en', 'stringUnit', 'value') - acc[key] = { value: positionalize_untyped_arguments(value), comment: entry['comment'] } unless value.nil? - end - end - - # The build-free extraction cannot type interpolations (a defaultValue's `\(…)` segments), so it - # emits untyped `%arg` placeholders. Rewrite them as positional printf specifiers (`%1$@`, `%2$@`, - # …), which is what GlotPress translators and the runtime's format-style resolution expect. This is - # only correct for String-valued interpolations, so App Intents defaultValues must interpolate - # preformatted Strings, never raw numbers or dates (see docs/localization.md). - def positionalize_untyped_arguments(value) - index = 0 - value.gsub(/%(\d+\$)?arg/) do - index += 1 - "%#{Regexp.last_match(1) || "#{index}$"}@" + acc[key] = { value: AppIntentsStrings.positionalize_untyped_arguments(value), comment: entry['comment'] } unless value.nil? end end end From 415f5349eb1315395319aa983feb6ae2804a3166 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Mon, 20 Jul 2026 11:37:35 +1000 Subject: [PATCH 5/9] Pin placeholder rewrite behaviour, bugs included MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../lanes/app_intents_strings_helper_test.rb | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 fastlane/lanes/app_intents_strings_helper_test.rb diff --git a/fastlane/lanes/app_intents_strings_helper_test.rb b/fastlane/lanes/app_intents_strings_helper_test.rb new file mode 100644 index 000000000000..2cdfa1ad9ce2 --- /dev/null +++ b/fastlane/lanes/app_intents_strings_helper_test.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +# Pure-Ruby unit suite for AppIntentsStrings.positionalize_untyped_arguments — the rewrite that turns the +# build-free extraction's untyped `%arg` placeholders into the positional printf specifiers GlotPress and the +# runtime expect. Run directly: `ruby fastlane/lanes/app_intents_strings_helper_test.rb`. No bundle / network. +require 'minitest/autorun' +require_relative 'app_intents_strings_helper' + +# Pins the current behaviour of the placeholder rewrite, including the mixed-form numbering bug documented on +# the method. The `test_known_bug_*` cases assert what the code does today, NOT what it should do — they exist +# so a fix flips them deliberately rather than silently. +class AppIntentsStringsHelperTest < Minitest::Test + def positionalize(value) + AppIntentsStrings.positionalize_untyped_arguments(value) + end + + def test_leaves_a_value_without_placeholders_untouched + assert_equal 'Select Site', positionalize('Select Site') + end + + def test_numbers_a_single_untyped_placeholder + assert_equal 'Stats for %1$@', positionalize('Stats for %arg') + end + + def test_numbers_untyped_placeholders_left_to_right + assert_equal '%1$@ on %2$@', positionalize('%arg on %arg') + end + + def test_preserves_an_explicit_position + assert_equal '%1$@', positionalize('%1$arg') + end + + def test_preserves_explicit_positions_out_of_order + # Reordering is how translators adapt to a target language's word order, so it must survive the rewrite. + assert_equal '%2$@ then %1$@', positionalize('%2$arg then %1$arg') + end + + def test_an_escaped_percent_elsewhere_in_the_value_is_untouched + assert_equal '100%% done: %1$@', positionalize('100%% done: %arg') + end + + def test_leaves_an_already_typed_specifier_alone + # `%@`/`%d` come from hand-written keys, not the untyped extraction, and are already GlotPress-ready. + assert_equal '%1$@ of %2$d', positionalize('%1$@ of %2$d') + end + + def test_does_not_match_arg_outside_a_placeholder + assert_equal 'Largest argument', positionalize('Largest argument') + end + + def test_known_bug_mixed_forms_renumber_onto_a_duplicate_index + # The counter advances on the explicit `%2$arg` too, so the following untyped placeholder is numbered 2 + # as well: two placeholders collapse onto one argument. Should be '%2$@ and %1$@'. + assert_equal '%2$@ and %2$@', positionalize('%2$arg and %arg') + end + + def test_mixed_forms_are_correct_only_when_the_explicit_position_matches_the_counter + # Same code path as the case above, but here the explicit `1$` happens to equal the counter, so the + # output is right by coincidence — which is why the bug is easy to miss. + assert_equal '%1$@ and %2$@', positionalize('%1$arg and %arg') + end + + def test_known_bug_an_escaped_percent_before_arg_is_read_as_a_placeholder + # `%%` is a literal percent, so "100%arg" is prose, not an interpolation — but the regex matches the + # second `%` and swallows the word. Should be '100%%arg'. + assert_equal '100%%1$@', positionalize('100%%arg') + end +end From 232ed44942fe3c7d6299eda9467b81c11a4d4d8f Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Mon, 20 Jul 2026 11:42:30 +1000 Subject: [PATCH 6/9] Remove verbose AI comments Co-authored-by: Gio Lodi --- fastlane/lanes/app_intents_strings_helper_test.rb | 6 ------ 1 file changed, 6 deletions(-) diff --git a/fastlane/lanes/app_intents_strings_helper_test.rb b/fastlane/lanes/app_intents_strings_helper_test.rb index 2cdfa1ad9ce2..972af6b7ac96 100644 --- a/fastlane/lanes/app_intents_strings_helper_test.rb +++ b/fastlane/lanes/app_intents_strings_helper_test.rb @@ -1,14 +1,8 @@ # frozen_string_literal: true -# Pure-Ruby unit suite for AppIntentsStrings.positionalize_untyped_arguments — the rewrite that turns the -# build-free extraction's untyped `%arg` placeholders into the positional printf specifiers GlotPress and the -# runtime expect. Run directly: `ruby fastlane/lanes/app_intents_strings_helper_test.rb`. No bundle / network. require 'minitest/autorun' require_relative 'app_intents_strings_helper' -# Pins the current behaviour of the placeholder rewrite, including the mixed-form numbering bug documented on -# the method. The `test_known_bug_*` cases assert what the code does today, NOT what it should do — they exist -# so a fix flips them deliberately rather than silently. class AppIntentsStringsHelperTest < Minitest::Test def positionalize(value) AppIntentsStrings.positionalize_untyped_arguments(value) From e8455fcd45b3967b35cb44118e4e78653265f3d1 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Mon, 20 Jul 2026 11:48:09 +1000 Subject: [PATCH 7/9] Assert correct placeholder behaviour, not the bugs 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 --- fastlane/lanes/app_intents_strings_helper.rb | 5 ++-- .../lanes/app_intents_strings_helper_test.rb | 23 +++++++++---------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/fastlane/lanes/app_intents_strings_helper.rb b/fastlane/lanes/app_intents_strings_helper.rb index f4653c058fb1..3808a76bc65d 100644 --- a/fastlane/lanes/app_intents_strings_helper.rb +++ b/fastlane/lanes/app_intents_strings_helper.rb @@ -11,9 +11,8 @@ module AppIntentsStrings # only correct for String-valued interpolations, so App Intents defaultValues must interpolate # preformatted Strings, never raw numbers or dates (see docs/localization.md). # - # Known limitation: the counter advances on every placeholder, including ones that already carry an - # explicit position, so a value mixing the two forms ("%2$arg and %arg") renumbers onto a duplicate - # index. Pinned by `test_known_bug_*` in the adjacent suite; no current call site mixes the forms. + # 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 diff --git a/fastlane/lanes/app_intents_strings_helper_test.rb b/fastlane/lanes/app_intents_strings_helper_test.rb index 972af6b7ac96..95041b84e74d 100644 --- a/fastlane/lanes/app_intents_strings_helper_test.rb +++ b/fastlane/lanes/app_intents_strings_helper_test.rb @@ -42,21 +42,20 @@ def test_does_not_match_arg_outside_a_placeholder assert_equal 'Largest argument', positionalize('Largest argument') end - def test_known_bug_mixed_forms_renumber_onto_a_duplicate_index - # The counter advances on the explicit `%2$arg` too, so the following untyped placeholder is numbered 2 - # as well: two placeholders collapse onto one argument. Should be '%2$@ and %1$@'. - assert_equal '%2$@ and %2$@', positionalize('%2$arg and %arg') + def test_mixed_forms_are_correct_when_the_explicit_position_matches_the_counter + assert_equal '%1$@ and %2$@', positionalize('%1$arg and %arg') end - def test_mixed_forms_are_correct_only_when_the_explicit_position_matches_the_counter - # Same code path as the case above, but here the explicit `1$` happens to equal the counter, so the - # output is right by coincidence — which is why the bug is easy to miss. - assert_equal '%1$@ and %2$@', positionalize('%1$arg and %arg') + # 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 - def test_known_bug_an_escaped_percent_before_arg_is_read_as_a_placeholder - # `%%` is a literal percent, so "100%arg" is prose, not an interpolation — but the regex matches the - # second `%` and swallows the word. Should be '100%%arg'. - assert_equal '100%%1$@', positionalize('100%%arg') + # 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 end From 732b924709879efb3e43c8167c422cf735e5b849 Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Mon, 20 Jul 2026 19:34:19 +1000 Subject: [PATCH 8/9] Satisfy Style/Documentation on the test class --- Generated with the help of Claude Code, https://claude.ai/code Co-Authored-By: Claude Code Opus 4.8 --- fastlane/lanes/app_intents_strings_helper_test.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/fastlane/lanes/app_intents_strings_helper_test.rb b/fastlane/lanes/app_intents_strings_helper_test.rb index 95041b84e74d..f2d854c6154e 100644 --- a/fastlane/lanes/app_intents_strings_helper_test.rb +++ b/fastlane/lanes/app_intents_strings_helper_test.rb @@ -3,6 +3,7 @@ require 'minitest/autorun' require_relative 'app_intents_strings_helper' +# The two cases marked FAILING are red until the rewrite is fixed. class AppIntentsStringsHelperTest < Minitest::Test def positionalize(value) AppIntentsStrings.positionalize_untyped_arguments(value) From 71305e89b223d2fa7461fa9cba2be2a8bc2a4c7b Mon Sep 17 00:00:00 2001 From: Gio Lodi Date: Mon, 20 Jul 2026 19:37:12 +1000 Subject: [PATCH 9/9] Exempt test files from Style/Documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .rubocop.yml | 4 ++++ fastlane/lanes/app_intents_strings_helper_test.rb | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.rubocop.yml b/.rubocop.yml index fd109537d10c..a542feabfe99 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -27,6 +27,10 @@ Layout/EmptyLines: Style/AsciiComments: Exclude: *xfiles +Style/Documentation: + Exclude: + - '**/*_test.rb' + Naming/FileName: Exclude: - fastlane/Matchfile diff --git a/fastlane/lanes/app_intents_strings_helper_test.rb b/fastlane/lanes/app_intents_strings_helper_test.rb index f2d854c6154e..95041b84e74d 100644 --- a/fastlane/lanes/app_intents_strings_helper_test.rb +++ b/fastlane/lanes/app_intents_strings_helper_test.rb @@ -3,7 +3,6 @@ require 'minitest/autorun' require_relative 'app_intents_strings_helper' -# The two cases marked FAILING are red until the rewrite is fixed. class AppIntentsStringsHelperTest < Minitest::Test def positionalize(value) AppIntentsStrings.positionalize_untyped_arguments(value)