-
Notifications
You must be signed in to change notification settings - Fork 2.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
feat(@aws-amplify/ui-components): I18n Support #4979
feat(@aws-amplify/ui-components): I18n Support #4979
Conversation
802261e
to
6122571
Compare
@ashika01 A lot of our tests are really just validating the default You recently removed emotion &
tldr; Remove diff --git a/packages/amplify-ui-components/src/components/amplify-sign-out/amplify-sign-out.spec.ts b/packages/amplify-ui-components/src/components/amplify-sign-out/amplify-sign-out.spec.ts
index 8fcf8a670..b8ea3259f 100644
--- a/packages/amplify-ui-components/src/components/amplify-sign-out/amplify-sign-out.spec.ts
+++ b/packages/amplify-ui-components/src/components/amplify-sign-out/amplify-sign-out.spec.ts
@@ -1,6 +1,7 @@
+import { I18n } from '@aws-amplify/core';
import { newSpecPage } from '@stencil/core/testing';
import { AmplifySignOut } from './amplify-sign-out';
-import { SIGN_OUT } from '../../common/constants';
+import { AuthMessages } from '../../common/types/AuthMessages';
describe('amplify-sign-out spec:', () => {
describe('Component logic ->', () => {
@@ -11,7 +12,7 @@ describe('amplify-sign-out spec:', () => {
});
it('should render default `buttonText`', () => {
- expect(signOut.buttonText).toEqual(SIGN_OUT);
+ expect(signOut.buttonText).toEqual(I18n.get(AuthMessages.SIGN_OUT));
});
it('should render `overrideStyle` to false by default', () => { |
…-js into amplify-ui-components-i18n
In retrospective, I believe my points are valid but can see emotional responses to "losing" tests. The problem with our "component logic" tests is that they're not testing logic. But it's a relatively small lift to get them passing without removing them. |
Codecov Report
@@ Coverage Diff @@
## ui-components/master #4979 +/- ##
=====================================================
Coverage 75.85% 75.85%
=====================================================
Files 177 177
Lines 9683 9683
Branches 1910 1860 -50
=====================================================
Hits 7345 7345
- Misses 2187 2198 +11
+ Partials 151 140 -11
Continue to review full report at Codecov.
|
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 like this approach. Could you just do some knowledge sharing on the comments?
Also, a general question, I like the blogs you posted, it makes sense in cases of abstraction, but having strings in constant doesn't completely fall in the abstraction world. I like having the strings used in constant.ts
so in future it makes changing the string value easier than going to all the place inside the code.
What are your thoughts there? Especially in similar cases like NO_AUTH_MODULE_FOUND
make it as util might be unwanted abstraction vs keeping it in string constant. (Maybe I am missing clarity here :))
import * as languages from '../common/translations'; | ||
|
||
I18n.putVocabularies(languages); | ||
I18n.setLanguage(window?.navigator?.language || 'en-US'); |
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.
Why do we set this as en-US
? Could we make it something generic so anyone can inject another language too?
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.
This default is largely here for one reason: newSpecPage()
doesn't set window.navigator.language
by default, like browsers do.
This code needs to change, as there's a bigger problem: we only have an English translation, so I18n
defaulting to fr-ca
will return enum values (e.g. SIGN_OUT
) instead of the English "defaults".
Based on how I18n.get
works (https://aws-amplify.github.io/docs/js/i18n#get), we can address this a couple of ways:
-
Use a custom
I18n
wrapper for UI components that falls back to English when a translated value doesn't exist. -
Document and encourage customization via the
AuthMessages
enum, but move the "default" English phrases to enum values:Customers will do this:
I18n.putVocabulariesForLanguage('en-US', { [AuthMessages.SIGN_OUT]: 'Howdy', });
Behind the scenes, we have:
enum AuthMessages { SIGN_OUT = 'Sign out' }
-
Update our
I18n.get(...)
calls to provide the English phrase as a default value:https://aws-amplify.github.io/amplify-js/api/classes/i18n.html#get
The biggest takeaway is that UI components should render default phrases even if I18n.setLanguage
isn't English.
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.
UI components should render default phrases even if I18n.setLanguage isn't English.
Agreed. I like the concept of wrapper to fallback to english. Do you want to add it to this PR?
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.
@ashika01 I ended doing something "simpler", which can be described as _migrating constants.ts
to Translations.ts
:
-
Customers can customize translations:
import { Translations } from "@aws-amplify/ui-components"; I18n.putVocabulariesForLanguage('en-US', { [Translations.SIGN_OUT]: 'Howdy', });
-
No more need to default a language via
I18n.setLanguage
-
The values of the Translation enums are the default English values.
-
No need for a wrapper.
-
Still adds
I18n.get
to the components, so we're ready for customer-provided translations. -
No need to instrument tests.
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.
Confirmed:
$ node
> require('@aws-amplify/ui-components').Translations
{ BACK_TO_SIGN_IN: 'Back to Sign In',
CHANGE_PASSWORD_ACTION: 'Change',
CHANGE_PASSWORD: 'Change Password',
CODE_LABEL: 'Verification code',
CODE_PLACEHOLDER: 'Enter code',
...
@ashika01 Great questions! I'd like to take the time to answer this, since my opinions have changed over the years on this:
You're right, as this is where things get subjective. We agree that it's about abstractions that make maintaining code easier & clearer. (My litmus test for when to or when not to abstract directly correlates with the friction/frustration to maintain code in it's present state.) For example, IME bulk changes typically happen in dictionaries (e.g. I18n) or in per-env config files (e.g. node-config). The majority of the time, changes were within the context of the UI (e.g. label text, button aesthetics, # of retries, etc.). Co-locating/inlining values served to:
Ultimately, my opinions stem from frustration with this PR taking more effort than it should have, because one-time use values were removed from the context of the UI and coupled with real constants.
In this case, we have 24 occurrences of code like: if (!Auth || typeof Auth.federatedSignIn !== 'function' || typeof Auth.currentAuthenticatedUser !== 'function') {
throw new Error(NO_AUTH_MODULE_FOUND);
} I think we agree that 24 occurrences of copy/pasta is a bit extra. I can't even say that this check even needs to exist, since we rely on More importantly, the code isn't clear on why this check needs to happen and when it should happen in the UX. Abtracting to an invariant check would be the first step to answering these questions. |
Cool. Thanks for explaining this @ericclemmons . I am good with this right now. Lets think more on the Auth-Module one and maybe revisit in later PR if needed. |
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.
This is looking good @ericclemmons 🌮 🎉 🚢
This pull request has been automatically locked since there hasn't been any recent activity after it was closed. Please open a new issue for related bugs. Looking for a help forum? We recommend joining the Amplify Community Discord server |
What This Does
@aws-amplify/ui-components
injectsI18n
withen
translations set.Move UI strings from
constants.ts
toTranslations
enum for typechecking and usage within components.All components have been retrofitted to use
I18n.get(Translations.*)
.Inlined single-use strings from
constants.ts
.Supports custom messages via:
Callouts
Why only English?
Originally, I attempted to re-use translations from https://github.com/aws-amplify/amplify-js/blob/master/packages/aws-amplify-react/src/AmplifyI18n.tsx.
The problem is that those languages have ~41 translated strings, while the new UI components have ~65, many new & others being different from what existed before.
Because the new components weren't created to truly be 1-to-1, getting the two translations at parity would require a costly deep-dive into the two implementations to reconcile the differences (if even possible).
Supporting other languages would require effort similar to what it took for React to translate their docs:
Punting on I18n was a costly mistake
By delaying I18n, this PR required a significant level of visual & automated (TypeScript, Jest) validation touching every component created. Had we opted for translation strings early via
I18n.get
, each component would've supported I18n out of the box with the same level of effort as it took to abstract strings intoconstants.ts
.constants.ts
was an incorrect abstractionMuch has been written on this top that's worth reading, as engineers really love abstractions:
In retrospect, there are seemingly clear abstraction boundaries to abide by:
constant.ts
for sharing true constants, such as response codes/strings from services like Cognito.AuthMessages
for enumerating translation constants based on placement & intent (e.g. a header or submit button), rather than English phrases.Inlining strings that are used once or a few times meant for developers (and not end-users). These are typically
logger.debug
messages, or an exception likethrow new Error(...)
. Co-locating messages with the context in which they happen is a feature, not a bug to abstract away.There are several calls to
throw new Error(NO_AUTH_MODULE_FOUND)
throughout the code. There are a couple ways to address this:if
logic as well:Error
:[Delivers #171445892]
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.