chore: Allow dynamic deferred loading of internationalization#4237
chore: Allow dynamic deferred loading of internationalization#4237avinashbot wants to merge 3 commits intomainfrom
Conversation
6af9e51 to
65f497e
Compare
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #4237 +/- ##
=======================================
Coverage 97.43% 97.43%
=======================================
Files 897 901 +4
Lines 26325 26359 +34
Branches 9514 9508 -6
=======================================
+ Hits 25650 25684 +34
- Misses 632 669 +37
+ Partials 43 6 -37 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
b380ceb to
107d893
Compare
107d893 to
a270b41
Compare
a270b41 to
afd8f0a
Compare
91e62bb to
16f6b31
Compare
16f6b31 to
3c12e2e
Compare
| <> | ||
| <AppLayoutStateProvider | ||
| forceDeduplicationType={forceDeduplicationType} | ||
| <RemoteI18nProvider loadFormatter={loadFormatter}> |
There was a problem hiding this comment.
Why do you need to wrap the whole thing with this provider? We can only wrap SkeletonLayout and call useAriaLabels inside
There was a problem hiding this comment.
It seemed cleaner and safer this way. I wasn't sure if the weird "hidden copy of breadcrumbs" thing should also be wrapped by the provider or if it's safe for its strings to not match the skeleton's strings, and if the deduplication breadcrumbs are included, the wrapped component ends up requiring passing along a lot of internal props and state tracking, which seemed unmaintainable to me.
If the breadcrumbs are 100% safe to exclude, I can go for this approach.
| it('loads the formatter and provides the context to the children', async () => { | ||
| document.documentElement.lang = 'es'; | ||
|
|
||
| const loadFormatter = jest.fn().mockResolvedValue(createMockFormatter()); |
There was a problem hiding this comment.
Why do you provide this function as a prop? Could it be an internal value in the provider?
There was a problem hiding this comment.
Not sure what you mean, could you expand? The loadFormatter function is a widget export — locally it just returns null; in production, it would return a promise with a formatter.
There was a problem hiding this comment.
I mean, why do you provide this function as a prop for the RemoteI18nProvider? It could be a part of its implementation
There was a problem hiding this comment.
It can, but this is better for bundle size (because intl-messageformat is not in the bundle):
RemoteI18nProvideris loaded by the applicationloadFormatteris part of the loader (in the application but not this package)loadFormatterthen fetchesI18nFormatterfrom the widget
| return <div data-testid="locale">{context?.locale || 'no-context'}</div>; | ||
| } | ||
|
|
||
| describe('RemoteI18nProvider', () => { |
There was a problem hiding this comment.
Let's add more realistic scenarios:
- what happens with provider's children i18n strings until loadFormatter is loaded?
- let's test it with our AppLayout component, especially with dynamically injected content
- let's make sure it works correctly if consumers wrap the content area with our i18nprovider
There was a problem hiding this comment.
Added some more tests!
| </RemoteI18nProvider> | ||
| ); | ||
|
|
||
| await waitFor(() => { |
There was a problem hiding this comment.
[non-blocking] Let's make it more explicit: await jest.runAllTimersAsync() waits until the async function is resolved
| // If formatter isn't available, do nothing. | ||
| }) | ||
| .catch(() => { | ||
| // Do nothing. Failure in fetching the formatter should not be fatal. |
There was a problem hiding this comment.
Do we want to send an operational metric here? same as here: https://github.com/cloudscape-design/components/blob/main/src/app-layout/visual-refresh-toolbar/state/use-app-layout.tsx#L227
There was a problem hiding this comment.
Oh absolutely, thanks for the callout; was wishing there was a way we could send a metric with included data. Let me see what the limits are, maybe I can include the error message.
| * where possible; a new instance must be created if locale or messages may | ||
| * have changed. | ||
| */ | ||
| export class I18nFormatter { |
There was a problem hiding this comment.
Should we name it more explicitly like RemoveI18nFormatter and put to the same folder as other widget parts?
There was a problem hiding this comment.
It's the common formatting logic — it's imported by both the local provider and the widget (which passes it to remote provider). So it's not specific to either scenario.
3c12e2e to
e999602
Compare
Description
Partial implementation of dynamically loaded internationalization. This lets us more readily embed the I18nProvider into performance-sensitive parts of the code (like the app layout toolbar) without worrying so much about added bundle size of the intl library, intl-messageformat (34.3 kB, 9.83 kB gzip) and the locale files (465kB, 68kB gzip). This also avoids dealing with any additional build-time considerations (intl-messageformat raises our bare minimum TypeScript version requirement, which we don't document publicly, though we do call out the raised minimum in our documentation).
The added cost is one asynchronously triggered full-application rerender.
"Dynamic loading" of the internationalization bundle isn't all that complicated, it's just a runtime import of
I18nFormatter(which this PR refactors out) preloaded with the appropriate locale file (which are already exposed under/messages/all.<locale>.js).Possible optimizations (either later in this PR or after release):
Wrapping the app rerender in a React transition: For React versions that support it, it avoids our rerender from blocking user actions and affecting web vitals.Removing ICU parser from the remote bundle (reference): If we can safely guarantee that the remote provider only accepts and handles pre-parsed AST-only strings, we can remove the parser from the dynamically loaded bundle and reduce the dynamic bundle's file size by 26.34 kB (7.1 kB gzip). But this is a non-blocking network request, so it's not high priority.See CR-253557644.Related links, issue #, if available: AWSUI-61508
How has this been tested?
Existing tests for locally (i.e. application) provided strings, additional tests for remote inclusion of logic will follow.
Review checklist
The following items are to be evaluated by the author(s) and the reviewer(s).
Correctness
CONTRIBUTING.md.CONTRIBUTING.md.Security
checkSafeUrlfunction.Testing
By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.