-
Notifications
You must be signed in to change notification settings - Fork 1.1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Migrate iOS14 Stats Widgets to use app L10n #17570
Conversation
To be parsed and recognized by genstrings
This reverts commit 85fe5c7 – which was created after we ran `fastlane generate_strings_file_for_glotpress` to confirm that the changes in the Fastfile worked as expected and were able to detect strings in the Widget code. We need to revert those changes to the generated `Localizable.strings` file because this branch will be merged in `develop` and we should not update that `.strings` file – imported by GlotPress – outside of code freeze; otherwise newer strings, associated with not-yet-frozen code, would be imported in GlotPress too soon.
For each locale, I appended the content of the .strings from the Stats Widget to the .strings of the App, then removed the .strings from the Widget (which are not used anymore)
Generated by 🚫 dangerJS |
You can trigger an installable build for these changes by visiting CircleCI here. |
To make Hound happy.
You can trigger optional UI/connected tests for these changes by visiting CircleCI here. |
Note about this PR updating the
|
To make Hound happy (again)
/// This is mostly useful to express *intent* on your API. It does not provide any compile-time guarantee | ||
typealias LocalizedString = String |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm open for feedback about this, curious what you think about the idea.
During the migration I found this useful for me, in order to keep track of what was supposed to be localized (and have their value provided via a call to AppLocalizedString
) vs regular, non-localized String
variables/parameters.
In practice there's no compile-time guarantee, so this is just a signal for the reader of the code/API.
I tried to make it type-checked, by using a @propertyWrapper struct LocalizedString
instead… but realized that doesn't work because the annotation needs to then be at the property declaration, aka on the @LocalizedString(…) static var todayWidgetTitle
constants declared in LocalizableStrings.swift
, which would force us to change them from static let
to static var
😒 , also force us to provide the wrappedValue
as part of the annotation (and not via = "key as string literal
) because otherwise genstrings
would not catch it… and speaking of this not even sure genstrings's regex would catch a custom routine like @LocalizedString
with that @
prefix… and on top of all that, we'd still have our custom methods' argument (like here) take a String
not a LocalizedString
… so I abandoned the whole idea and settled for this semantic-only typealias
instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In practice there's no compile-time guarantee, so this is just a signal for the reader of the code/API.
This is my biggest frustration with typealias
. I wish there was a way to get the compiler's help 😕 Improved readability is still a useful feature, though, so 👍 from me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I really tried hard multiple solutions to get compile-time guarantee and was quite frustrated to not be able to achieve it too 😅 Like I even considered creating a struct LocString: StringProtocol
at some point… but as per the official doc of StringProtocol
, it's highly discouraged (borderline "forbidden") to make custom types conform to StringProtocol
– the doc clearly states that you should not do it and that only Swift-provided types like String
and SubString
should ever conform – so that was a no-no as well…
I think there have been discussions in Swift-Evolution to provide magic keywords or macro-driven solution to generate new types based on / transparently wrapping existing types (e.g. newtype Duration = Double
or newtype LocString = String
to solve this, but this still haven't seen the light of day in practice… So we'll have to accept our frustration for now and only rely on typealias
, better than nothing I guess 😅
extension Bundle { | ||
static let app: Bundle = { | ||
var url = Bundle.main.bundleURL | ||
while url.pathExtension != "app" && url.lastPathComponent != "/" { | ||
url.deleteLastPathComponent() | ||
} | ||
guard let appBundle = Bundle(url: url) else { fatalError("Unable to find the parent app bundle") } | ||
return appBundle | ||
}() | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I pondered putting this in its own Bundle+app.swift
dedicated file, to respect the "one class/extension per file" convention… but figure it was not much worth it.
Especially since, in the near future (in the soon-to-come PRs which will address other Widgets and App Extensions targets as I continue progressing on that project of mine), I plan to add that helper file to other targets (i.e. make this file be a member of not only the WordPressStatsWidgets
but also all other app extensions needing app localization like the pre-iOS14 widgets or the ShareExtension
) – as opposed to duplicate/recreate/re-type this code and helpers in each target.
Which means that having both the Bundle.app
extension
and the AppLocalizedString
helper function in the same file and having only one file to add new targets as its target memberships will make things easier.
PS: Obviously, when I get to that (in the future PRs) I'll start thinking on a better subdirectory to put that shared file in once I start sharing it with multiple targets
To put them in alphabetical order, so that during the next code-freeze or run of the generate_strings_file_for_glotpress lane, the diff will be smaller by not moving these lines around (since genstrings sort the keys in alphabetical order when generating its `.strings` file from parsing the code).
Seems they have always be missing in the original `.strings` too (but since the code used `value:` in its call to `NSLocalizedString` before that PR, those 2 missing in the development language didn't have any impact)
9946269
to
6a84db6
Compare
exclude: ['*Vendor*', 'WordPress/WordPressTest/I18n.swift', 'WordPress/WordPressStatsWidgets/Views/Localization/LocalizedStringKey+extension.swift', '*/Secrets.swift'], | ||
exclude: ['*Vendor*', 'WordPress/WordPressTest/I18n.swift', 'WordPress/WordPressStatsWidgets/Views/Localization/AppLocalizedString.swift'], | ||
routines: ['AppLocalizedString'], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Figured there was no chance to have localized strings in the Secrets.swift
file, and I'm actually not sure why it was excluded in the first place (back when it was in localize.py
which is why I initially put it there in the previous PR), so figured I'd use the occasion to remove it (I checked and the lane still works without error nor any new key it shouldn't have)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 the only reason I can think of for ignoring it is to avoid genstrings
accidentally printing out a secret that happened to match the localization regex but wasn't meant to. Maybe? I'm not even sure if that's something that could happen on the genstrings
end, though. 🤷
I agree that it should be 99.9% safe to remove it. 👍
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Went through the code and looks good 👍
I love the "DI for .strings
" approach. I resonates with my microapps dream world.
Leaving this here before starting manual testing.
/// This is mostly useful to express *intent* on your API. It does not provide any compile-time guarantee | ||
typealias LocalizedString = String |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In practice there's no compile-time guarantee, so this is just a signal for the reader of the code/API.
This is my biggest frustration with typealias
. I wish there was a way to get the compiler's help 😕 Improved readability is still a useful feature, though, so 👍 from me.
WordPress/WordPressStatsWidgets/Views/Localization/AppLocalizedString.swift
Outdated
Show resolved
Hide resolved
WordPress/WordPressStatsWidgets/Views/Localization/AppLocalizedString.swift
Outdated
Show resolved
Hide resolved
WordPress/WordPressStatsWidgets/Views/Localization/AppLocalizedString.swift
Outdated
Show resolved
Hide resolved
exclude: ['*Vendor*', 'WordPress/WordPressTest/I18n.swift', 'WordPress/WordPressStatsWidgets/Views/Localization/LocalizedStringKey+extension.swift', '*/Secrets.swift'], | ||
exclude: ['*Vendor*', 'WordPress/WordPressTest/I18n.swift', 'WordPress/WordPressStatsWidgets/Views/Localization/AppLocalizedString.swift'], | ||
routines: ['AppLocalizedString'], |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤔 the only reason I can think of for ignoring it is to avoid genstrings
accidentally printing out a secret that happened to match the localization regex but wasn't meant to. Maybe? I'm not even sure if that's something that could happen on the genstrings
end, though. 🤷
I agree that it should be 99.9% safe to remove it. 👍
🙃 |
18.7 has been finalized: #17573. Just waiting for review and merge.
Thank you for the heads up, I'll try to put extra care in looking at the |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Behaved as described. Thank you for the detailed test steps.
Note that I've found that changing the "App Language" and "App Region" in "Edit Scheme" > "Run" > "Options" in Xcode doesn't seem to work / to affect the Widget itself when debugging Widgets, so this was the only way I found to debug widgets in other locales 🤷
How disappointing 😞
I switched an iPhone 13 iOS 15.0 Simulator to Italian, 🤌, and selected the today widget kind env var in the scheme. Before running the scheme, I erased all content and settings on that Simulator.
There's no widget.today.unconfigured.view.title
key in the it
file: https://github.com/wordpress-mobile/WordPress-iOS/pull/17570/files#diff-017232c167b6dbaa61eb2be975fe2a5b5607c8b9923e13fd48ef459f70eb34b7. It fallback to English as expected:
One thing I'm not sure about, though, is that, unless I missed them in my eye-based inspection of the diff, _none of the .strings
include a widget.today.unconfigured.view.title
key 🤔
It's possible that's legit and that key was yet to be added to the source files. I'm raising this point because you mentioned using a script, so just wanted to make sure this is not something the script missed instead.
Ideally, repeat for a few locales, and also test for both a simulator and on-device
I run through the same checks after changing the Simulator language to es
. I also change the widget kind to "all time".
Auto-merge is disabled, so I'm going to approve this PR under the assumption that the missing widget.today.unconfigured.view.title
key is not a bug.
To be fair, even if it is a bug and a minor regression in the localization coverage, and it turns out to be something we can't address quickly, I think we can live with for a short while. Af few missing strings are not worth slowing down the release train, IMHO.
As an aside, this work you've done also benefits us in that by testing the localization we can surface values that haven't been localized yet.
E.g. "This Week" and "Stay up to date...":
"widget.alltime.posts.label" = "Articoli"; | ||
"widget.today.comments.label" = "Commenti"; | ||
"widget.today.likes.label" = "Like"; | ||
"widget.today.title" = "Oggi"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"widget.today.views.label" = "Visite"; | ||
"widget.today.visitors.label" = "Visitatori"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"widget.alltime.bestviews.label" = "Mayor número de visitas"; | ||
"widget.alltime.posts.label" = "Entradas"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot @mokagio for taking the time to test this thoroughly and so late in your day ❤️
Yes, this actually took me quite some time to investigate and to try to understand where those localizations came from back when we introduced the Widgets. It turns out that only the strings that were also already used in the Stats screen of the app were copied over in the Widgets, while new strings like this "unconfigured" one were specific to widgets hence why they were not (and still aren't… yet). Spelunking DetailsThose keys (the ones that were in the Widget's
So, TL;DR: what happened is that back then, Gio R copied any strings / copy that were already existing in the app, to copy them over into the widget's
(Phew, what a digging journey! But spelunking hat finally off! 😸 ) |
h/t @mokagio for the great wording suggestions Co-authored-by: Gio Lodi <[email protected]>
Part of project ref paaHJt-2Ib-p2 & paaHJt-2J8-p2
What's in this PR
This PR migrates the WordPressStatsWidget (our iOS14-style Widget) to use the app bundle's localized strings.
This is in an effort to:
.strings
files in both the App and the Widget, allowing us to shareHow to test:
Prepare your testing environment – Xcode, Simulator, Scheme
.app
installed in the simulator during your test if they were present in a previous build.strings
files it had from the previous installation_XCWidgetKind
Finally check the UI is localized as expected
At that point, you should finally be able to see the Widget and app in your Simulator or Device and be ready to check the behavior:
widget.today.unconfigured.view.title
key at the end of theWordPress/Resources/*.lproj/Localizable.strings
corresponding to the locale you're testing this in.value:
parameter inWordPress/WordPressStatsWidgets/Views/Localization/LocalizableStrings.swift
for the correspondingstatic let unconfiguredView*Title
constant)WordPress/Resources/*.lproj/Localizable.strings
of the tested localeIdeally, repeat for a few locales, and also test for both a simulator and on-device
Finally, also test the Fastfile changes
bundle exec fastlane generate_strings_file_for_glotpress
Update strings for localization
commit (it won't push it, don't worry!) which updated theWordPress/Resources/en.lproj/Localizable.strings
filedevelop
by recent PRs since the last code freeze (like"Join From Anywhere"
or"Work With Us"
), but did not delete or change any of the existing strings coming from theWordPressStatsWidgets
's codebase – aka that all the keys starting withwidget.*
are kept intact, showing thatgenstrings
properly found them while parsing the codebase.Regression Notes
1. Potential unintended areas of impact
Known issue: as long as our global tooling (in release-toolkit) still relies on
./Scripts/fix-translations
– which replaces any missing key in the.strings
files downloaded from GlotPress with the"key" = "key";
… which is not the right choice for the strings used in the Widget since all of them have a key that is not their English copy – we risk to have any key that would end up not being translated during the next sprint for 18.8, to display as their reverse-DNS-name key rather than falling back to the English copy as intended.For now I'm assuming/hoping that our translators will translate most if not all of those new keys in GlotPress during the next cycle which will make this a non-issue. Besides, this potential issue won't happen until the next "upload originals to GlotPress on code-freeze then download translation and run
fix-translations
on it during final release" full cycle (for 18.8), so that still gives us time to fix it.That being said, as part of one of the next steps of paaHJt-2Ib-p2, I will get soon rid of that bogus
fix-translations
script anyway – to entirely rely on iOS doing the right thing for us without the need for that post-processing – so I'm quite confident that even if our GlotPress translators didn't translate all those new Widget strings in all the locales, that particular hole in the tooling will be fixed by then.2. What I did to test those areas of impact (or what existing automated tests I relied on)
Tested manually in the Simulator with different locales
3. What automated tests I added (or what prevented me from doing so)
None (I didn't see any possible way to write a good Unit Test for that except one that would just end up being a test of Apple's
Bundle.localizedString(…)
built-in method itself)PR submission checklist:
RELEASE-NOTES.txt
if necessary.