diff --git a/.changeset/mosaic-localization-key-tree.md b/.changeset/mosaic-localization-key-tree.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/mosaic-localization-key-tree.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/packages/mosaic/ARCHITECTURE.md b/packages/mosaic/ARCHITECTURE.md index 4e892d1270e..2236b7ca16a 100644 --- a/packages/mosaic/ARCHITECTURE.md +++ b/packages/mosaic/ARCHITECTURE.md @@ -204,8 +204,10 @@ user-button.styles.ts // `stylex.create` atoms (see the StyleX authoring model nor the view owns it, and the two cannot drift. `*.messages.ts` holds strings only, exported as one `Messages` object -declared `as const` and registered under a namespace in `src/localization/registry.ts`. -Views read their namespace inside the component with `useMessages('userButton')`, so +declared `as const` and mounted in `src/localization/registry.ts` under the public +component it belongs to, such as `userProfile.mfa.backupCodes`. A root is a public +component or a shared block, never a file name. Views read their root inside the +component with `useMessages('userProfile').mfa`, so the `localization` prop on `MosaicProvider` can replace any string; a module-level helper takes the messages as a parameter rather than importing them. @@ -243,6 +245,19 @@ so a consumer's overrides type-check against the real keys, as a dot path Renaming or moving a key breaks every override that names it; treat it like renaming a prop. +The key tree follows what the user sees, so a key can be found from the screen: + +- A root is a public component (`userProfile`) or a shared block (`form`, `errors`). + There is no `common` root. The same English in two components is two keys, + because a translation may need to differ by context. +- Below the root, keys nest by section, then by the dialog or step inside it: + `userProfile.email.addDialog`, `userProfile.email.removeDialog`. +- Copy that several views render through one shared component belongs to that + component (`userProfile.contact` for the row both email and phone use). +- Every segment is camelCase. A map keyed by data ids (`errors`, `roles`, + `userProfile.mfa.methods`) keeps the ids as its keys; + `src/localization/__tests__/registry.test.ts` lists those maps and checks the rest. + ### Composition Two shapes, chosen by whether the slice fetches its own data. diff --git a/packages/mosaic/docs/views.md b/packages/mosaic/docs/views.md index 9b58f75ec1e..3d20e6fb13f 100644 --- a/packages/mosaic/docs/views.md +++ b/packages/mosaic/docs/views.md @@ -48,13 +48,26 @@ Every string a view renders comes from the feature's `*.messages.ts`, shaped the way `@clerk/i18n` takes a base definition. A plural message is its forms; a parameterized one is its template. -Register the file as a namespace in `src/localization/registry.ts`, then read it -with `useMessages` in the view or controller that renders it: +To add copy for a new section, step or dialog: + +1. Put the strings in the `*.messages.ts` next to the view that renders them. Name + each key for what the string is (`label`, `resend`, `confirm`), not where it + sits in the markup. +2. Mount the object in `src/localization/registry.ts` under the public component + the user sees it in: a section of `UserProfile` goes at `userProfile.
`, + and a dialog inside it at `userProfile.
.addDialog` or `.removeDialog`. + Copy two views share goes under the component that renders it, never under a + new `common` root. +3. Read it with `useMessages` and the root, in the view or controller that + renders it: ```tsx -const m = useMessages('userButton'); +const m = useMessages('userProfile').mfa; ``` +The rules behind these steps are in [ARCHITECTURE.md](../ARCHITECTURE.md), and +`src/localization/__tests__/registry.test.ts` fails on a key that is not camelCase. + Never import a `*.messages.ts` directly. A direct import skips the customer's locale and overrides, so the copy can't be localized. diff --git a/packages/mosaic/src/features/api-keys/__tests__/api-keys-table.feature.test.tsx b/packages/mosaic/src/features/api-keys/__tests__/api-keys-table.feature.test.tsx index 9da43587fb7..44d86062fd8 100644 --- a/packages/mosaic/src/features/api-keys/__tests__/api-keys-table.feature.test.tsx +++ b/packages/mosaic/src/features/api-keys/__tests__/api-keys-table.feature.test.tsx @@ -247,9 +247,9 @@ describe('APIKeysTable', () => { diff --git a/packages/mosaic/src/features/api-keys/api-keys-table.messages.ts b/packages/mosaic/src/features/api-keys/api-keys-table.messages.ts index eedc49ae070..37e2f21d815 100644 --- a/packages/mosaic/src/features/api-keys/api-keys-table.messages.ts +++ b/packages/mosaic/src/features/api-keys/api-keys-table.messages.ts @@ -69,7 +69,7 @@ export const apiKeysTableMessages = { } as const; export function resolveAPIKeysTableMessages( - catalog: MosaicMessages['apiKeysTable'], + catalog: MosaicMessages['apiKeys'], subjectKind: APIKeysTableSubjectKind, overrides?: Partial, ): APIKeysTableMessages { diff --git a/packages/mosaic/src/features/api-keys/api-keys-table.tsx b/packages/mosaic/src/features/api-keys/api-keys-table.tsx index d89ff58592f..264dc047e80 100644 --- a/packages/mosaic/src/features/api-keys/api-keys-table.tsx +++ b/packages/mosaic/src/features/api-keys/api-keys-table.tsx @@ -38,7 +38,7 @@ function SubjectAPIKeysTable({ fallback, }: APIKeysTableProps) { const messages = resolveAPIKeysTableMessages( - useMessages('apiKeysTable'), + useMessages('apiKeys'), isOrganizationId(subject) ? 'organization' : 'user', overrides, ); diff --git a/packages/mosaic/src/features/api-keys/api-keys-table.types.ts b/packages/mosaic/src/features/api-keys/api-keys-table.types.ts index 7611966a521..165fb1ca52c 100644 --- a/packages/mosaic/src/features/api-keys/api-keys-table.types.ts +++ b/packages/mosaic/src/features/api-keys/api-keys-table.types.ts @@ -20,7 +20,7 @@ export type APIKeysTableSubjectKind = 'user' | 'organization'; type Strings = { readonly [K in keyof T]: T[K] extends string ? string : Strings }; -export type APIKeysTableMessages = Strings> & { +export type APIKeysTableMessages = Strings> & { readonly noKeysDescription: string; }; diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-danger-section/organization-profile-danger-section.view.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-danger-section/organization-profile-danger-section.view.tsx index c078bde9677..bd02789857e 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-danger-section/organization-profile-danger-section.view.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile-danger-section/organization-profile-danger-section.view.tsx @@ -17,7 +17,7 @@ export function OrganizationProfileDangerSectionView({ leave, destroy, }: OrganizationProfileDangerSectionViewProps) { - const m = useMessages('organizationProfileDangerSection'); + const m = useMessages('organizationProfile').danger; const locale = useLocale(); if (!leave && !destroy) { @@ -78,7 +78,7 @@ function DangerRow({ dialogTitle: string; dialogDescription: string; }) { - const m = useMessages('organizationProfileDangerSection'); + const m = useMessages('organizationProfile').danger; return ( diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/invitations-table-tab.controller.ts b/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/invitations-table-tab.controller.ts index 1942999e2d9..b0985c9318d 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/invitations-table-tab.controller.ts +++ b/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/invitations-table-tab.controller.ts @@ -12,7 +12,7 @@ export function useInvitationsTableController( ): InvitationsTableTabViewProps { const locale = useLocale(); const roleNames = useMessages('roles'); - const m = useMessages('invitationsTableTab'); + const m = useMessages('organizationProfile').members.invitations; const invitations = model.rows.map(({ id, emailAddress, createdAt, role, roleName }) => ({ id, email: emailAddress, diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/invitations-table-tab.skeleton.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/invitations-table-tab.skeleton.tsx index 7b08548ce8c..fdcfe5ee358 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/invitations-table-tab.skeleton.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/invitations-table-tab.skeleton.tsx @@ -44,7 +44,7 @@ export function InvitationsTableTabSkeleton({ hasRevoke: boolean; hasBulkAction: boolean; }) { - const m = useMessages('invitationsTableTab'); + const m = useMessages('organizationProfile').members.invitations; return (
1; return ( <> @@ -136,7 +136,7 @@ function InvitationsTableBody({ onRevokeClick?: (invitation: OrganizationProfileInvitation) => void; registerTrigger: (id: string) => Ref; }) { - const m = useMessages('invitationsTableTab'); + const m = useMessages('organizationProfile').members.invitations; return ( ({ diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/members-table-tab.skeleton.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/members-table-tab.skeleton.tsx index 61aa25a8cdc..7dd767af098 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/members-table-tab.skeleton.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/members-table-tab.skeleton.tsx @@ -51,7 +51,7 @@ export function MembersTableTabSkeleton({ hasRemove: boolean; hasBulkAction: boolean; }) { - const m = useMessages('membersTableTab'); + const m = useMessages('organizationProfile').members.list; return (
{m.loading} diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/members-table-tab.view.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/members-table-tab.view.tsx index b03344c1354..a7d88647dfb 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/members-table-tab.view.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/members-table-tab.view.tsx @@ -51,7 +51,7 @@ export function MembersTableTabView({ isChangingRole = false, roleError, }: MembersTableTabViewProps) { - const m = useMessages('membersTableTab'); + const m = useMessages('organizationProfile').members.list; const query = searchValue.trim(); const showPagination = Math.ceil(totalCount / pageSize) > 1; return ( @@ -148,7 +148,7 @@ function MembersTableNotices({ hasRoleSetMigration, roleError, }: Pick) { - const m = useMessages('membersTableTab'); + const m = useMessages('organizationProfile').members.list; return ( <> {hasRoleSetMigration ? ( @@ -194,7 +194,7 @@ function MembersTableBody({ onRemoveClick?: (member: OrganizationProfileMember) => void; registerTrigger: (id: string) => Ref; }) { - const m = useMessages('membersTableTab'); + const m = useMessages('organizationProfile').members.list; return ( void; triggerRef?: Ref; }) { - const m = useMessages('membersTableTab'); + const m = useMessages('organizationProfile').members.list; const manageable = canManageMember(member); return ( (null); const isValid = (value: string) => isEmail(value) && !rejectedEmailAddresses.includes(value); diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/organization-profile-members-panel.view.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/organization-profile-members-panel.view.tsx index e8d6be44698..d54f6353367 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/organization-profile-members-panel.view.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/organization-profile-members-panel.view.tsx @@ -31,9 +31,9 @@ export function OrganizationProfileMembersPanelView({ inviteDialog, }: OrganizationProfileMembersPanelViewProps) { const m = useMessages('organizationProfile'); - const membersMessages = useMessages('membersTableTab'); - const invitationsMessages = useMessages('invitationsTableTab'); - const requestsMessages = useMessages('requestsTableTab'); + const membersMessages = useMessages('organizationProfile').members.list; + const invitationsMessages = useMessages('organizationProfile').members.invitations; + const requestsMessages = useMessages('organizationProfile').members.requests; const tabs = [ { id: 'members', diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/requests-table-tab.skeleton.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/requests-table-tab.skeleton.tsx index e7aaa456273..69b5d27dada 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/requests-table-tab.skeleton.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile-members-panel/requests-table-tab.skeleton.tsx @@ -47,7 +47,7 @@ export function RequestsTableTabSkeleton({ hasDecline: boolean; hasBulkAction: boolean; }) { - const m = useMessages('requestsTableTab'); + const m = useMessages('organizationProfile').members.requests; const hasActions = hasAccept || hasDecline; return (
1; const hasActions = Boolean(onAccept || onDecline); return ( @@ -137,7 +137,7 @@ function RequestsTableBody({ registerAcceptTrigger?: (id: string) => Ref; registerDeclineTrigger?: (id: string) => Ref; }) { - const m = useMessages('requestsTableTab'); + const m = useMessages('organizationProfile').members.requests; return ( Ref; registerDeclineTrigger?: (id: string) => Ref; }) { - const m = useMessages('requestsTableTab'); + const m = useMessages('organizationProfile').members.requests; const [acceptRef] = useState(() => registerAcceptTrigger?.(request.id)); const [declineRef] = useState(() => registerDeclineTrigger?.(request.id)); const actions = usePendingAction<'accept' | 'decline'>(); diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-edit-name.dialog.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-edit-name.dialog.tsx index 8e11c7f0f0e..e5d4d2b80d8 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-edit-name.dialog.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-edit-name.dialog.tsx @@ -28,7 +28,7 @@ export function OrganizationProfileEditNameDialog({ trigger, form, }: OrganizationProfileEditNameDialogProps) { - const m = useMessages('organizationProfileProfileSection'); + const m = useMessages('organizationProfile').profile; const inputRef = useRef(null); const { feedback } = form.fields.name; const { ref, ...control } = form.register('name'); diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-edit-slug.dialog.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-edit-slug.dialog.tsx index ca444513091..4cb4539ffed 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-edit-slug.dialog.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-edit-slug.dialog.tsx @@ -28,7 +28,7 @@ export function OrganizationProfileEditSlugDialog({ trigger, form, }: OrganizationProfileEditSlugDialogProps) { - const m = useMessages('organizationProfileProfileSection'); + const m = useMessages('organizationProfile').profile; const inputRef = useRef(null); const { feedback } = form.fields.slug; const { ref, ...control } = form.register('slug'); diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-logo-row.view.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-logo-row.view.tsx index a49f0c9d279..8e676251729 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-logo-row.view.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-logo-row.view.tsx @@ -39,7 +39,7 @@ export function OrganizationProfileLogoRowView({ onReject, onRemove, }: OrganizationProfileLogoRowViewProps) { - const m = useMessages('organizationProfileProfileSection'); + const m = useMessages('organizationProfile').profile; const errorText = useErrorText(); const logo = useOrganizationProfileLogoController({ onChange, onRemove }); const [rejection, setRejection] = useState(); @@ -115,7 +115,7 @@ function LogoActions({ canChange: boolean; onRemove?: () => Promise; }) { - const m = useMessages('organizationProfileProfileSection'); + const m = useMessages('organizationProfile').profile; const { openFilePicker } = FileUpload.useFileUpload(); const actions: ActionMenuAction[] = []; diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-name-row.view.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-name-row.view.tsx index ed3d8cec645..2e2ff3f2a4f 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-name-row.view.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-name-row.view.tsx @@ -10,7 +10,7 @@ interface OrganizationProfileNameRowViewProps { } export function OrganizationProfileNameRowView({ name, onSubmit }: OrganizationProfileNameRowViewProps) { - const m = useMessages('organizationProfileProfileSection'); + const m = useMessages('organizationProfile').profile; return ( @@ -32,7 +32,7 @@ export function OrganizationProfileNameRowView({ name, onSubmit }: OrganizationP } function EditName({ name, onSubmit }: { name: string; onSubmit: (name: string) => Promise }) { - const m = useMessages('organizationProfileProfileSection'); + const m = useMessages('organizationProfile').profile; const controller = useOrganizationProfileEditNameController({ name, onSubmit }); return ( diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-profile-section.view.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-profile-section.view.tsx index 2c014df664f..3199d0dba9e 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-profile-section.view.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-profile-section.view.tsx @@ -28,7 +28,7 @@ export function OrganizationProfileProfileSectionView({ onSubmitName, onSubmitSlug, }: OrganizationProfileProfileSectionViewProps) { - const m = useMessages('organizationProfileProfileSection'); + const m = useMessages('organizationProfile').profile; return ( diff --git a/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-slug-row.view.tsx b/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-slug-row.view.tsx index 4c8cf81670c..1febaa52376 100644 --- a/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-slug-row.view.tsx +++ b/packages/mosaic/src/features/organization-profile/organization-profile-profile-section/organization-profile-slug-row.view.tsx @@ -15,7 +15,7 @@ interface OrganizationProfileSlugRowViewProps { } export function OrganizationProfileSlugRowView({ slug, onSubmit }: OrganizationProfileSlugRowViewProps) { - const m = useMessages('organizationProfileProfileSection'); + const m = useMessages('organizationProfile').profile; return ( @@ -52,7 +52,7 @@ export function OrganizationProfileSlugRowView({ slug, onSubmit }: OrganizationP } function EditSlug({ slug, onSubmit }: { slug: string; onSubmit: (slug: string) => Promise }) { - const m = useMessages('organizationProfileProfileSection'); + const m = useMessages('organizationProfile').profile; const controller = useOrganizationProfileEditSlugController({ slug, onSubmit }); return ( diff --git a/packages/mosaic/src/features/reverification/__tests__/reverification.feature.test.tsx b/packages/mosaic/src/features/reverification/__tests__/reverification.feature.test.tsx index 6aa155725c9..8b750370d0e 100644 --- a/packages/mosaic/src/features/reverification/__tests__/reverification.feature.test.tsx +++ b/packages/mosaic/src/features/reverification/__tests__/reverification.feature.test.tsx @@ -1666,7 +1666,7 @@ describe('Reverification', () => { guardedAction(), verificationSeed({ firstFactors: [passwordFactor] }), { - messages: { 'reverification.unstable__errors__generic': 'Something custom went wrong' }, + messages: { 'reverification.attemptFailed': 'Something custom went wrong' }, }, ); const user = await startVerification(); diff --git a/packages/mosaic/src/features/reverification/reverification.controller.ts b/packages/mosaic/src/features/reverification/reverification.controller.ts index 1fb1d62677c..65d6fef9f1f 100644 --- a/packages/mosaic/src/features/reverification/reverification.controller.ts +++ b/packages/mosaic/src/features/reverification/reverification.controller.ts @@ -495,7 +495,7 @@ function useReverificationState(model: ReverificationModel): ReverificationContr direction: context.direction, value: context.inputValue, onValueChange: (value: string) => send({ type: 'TYPE', value }), - errorMessage: context.error ? (context.error.detail ?? m.unstable__errors__generic) : undefined, + errorMessage: context.error ? (context.error.detail ?? m.attemptFailed) : undefined, isPending: pendingStates.has(state), onSubmit: () => send({ type: 'SUBMIT' }), onShowMethods: () => send({ type: 'SHOW_METHODS' }), diff --git a/packages/mosaic/src/features/reverification/reverification.messages.ts b/packages/mosaic/src/features/reverification/reverification.messages.ts index 1df87ac6755..2a6fe36547d 100644 --- a/packages/mosaic/src/features/reverification/reverification.messages.ts +++ b/packages/mosaic/src/features/reverification/reverification.messages.ts @@ -1,93 +1,79 @@ export const reverificationMessages = { - alternativeMethods: { - actionLink: 'Get help', - actionText: 'Don’t have any of these?', - blockButton__backupCode: 'Use a backup code', - blockButton__emailCode: 'Email code to {identifier}', - blockButton__passkey: 'Use your passkey', - blockButton__password: 'Continue with your password', - blockButton__phoneCode: 'Send SMS code to {identifier}', - blockButton__totp: 'Use your authenticator app', - getHelp: { - blockButton__emailSupport: 'Email support', - description: - 'If you have trouble verifying your account, email us and we will work with you to restore access as soon as possible.', - title: 'Get help', - }, - description: 'Facing issues? You can use any of these methods for verification.', - title: 'Use another method', - }, - backupCodeMfa: { - description: 'Enter the backup code you received when setting up two-step authentication', - title: 'Enter a backup code', - }, loading: { title: 'Verification required', }, - emailCode: { - description: 'Enter the code sent to your email to continue', - formTitle: 'Verification code', - resendButton: 'Resend', - resendCountdown: 'Resend ({seconds})', - resendText: 'Didn’t receive a code?', + password: { title: 'Verification required', + description: 'Enter your current password to continue', + label: 'Password', + placeholder: 'Enter your password', + showPassword: 'Show password', + hidePassword: 'Hide password', }, passkey: { - blockButton__passkey: 'Use your passkey', + title: 'Use your passkey', description: 'Using your passkey confirms your identity. Your device may ask for your fingerprint, face, or screen lock.', - title: 'Use your passkey', }, - password: { - actionLink: 'Use another method', - description: 'Enter your current password to continue', + emailCode: { title: 'Verification required', + description: 'Enter the code sent to your email to continue', + label: 'Verification code', + resendText: 'Didn’t receive a code?', + resend: 'Resend', + resendCountdown: 'Resend ({seconds})', }, phoneCode: { - description: 'Enter the code sent to your phone to continue', - formTitle: 'Verification code', - resendButton: 'Resend', - resendCountdown: 'Resend ({seconds})', - resendText: 'Didn’t receive a code?', title: 'Verification required', - }, - phoneCodeMfa: { description: 'Enter the code sent to your phone to continue', - formTitle: 'Verification code', - resendButton: 'Resend', - resendCountdown: 'Resend ({seconds})', + label: 'Verification code', resendText: 'Didn’t receive a code?', + resend: 'Resend', + resendCountdown: 'Resend ({seconds})', + }, + totp: { title: 'Verification required', + description: 'Enter the code generated by your authenticator app to continue', + label: 'Verification code', + }, + backupCode: { + title: 'Enter a backup code', + description: 'Enter the backup code you received when setting up two-step authentication', + label: 'Backup code', + }, + methods: { + title: 'Use another method', + description: 'Facing issues? You can use any of these methods for verification.', + password: 'Continue with your password', + passkey: 'Use your passkey', + emailCode: 'Email code to {identifier}', + phoneCode: 'Send SMS code to {identifier}', + totp: 'Use your authenticator app', + backupCode: 'Use a backup code', + helpText: 'Don’t have any of these?', + help: 'Get help', + }, + help: { + title: 'Get help', + description: + 'If you have trouble verifying your account, email us and we will work with you to restore access as soon as possible.', + emailSupport: 'Email support', }, error: { - closeButton: 'Close', + close: 'Close', generic: { - description: 'Please try again.', title: 'Something went wrong', + description: 'Please try again.', }, noFactors: { - description: 'Cannot proceed with verification. No suitable authentication factor is configured.', title: 'Cannot verify your account', + description: 'Cannot proceed with verification. No suitable authentication factor is configured.', }, }, - totpMfa: { - description: 'Enter the code generated by your authenticator app to continue', - formTitle: 'Verification code', - title: 'Verification required', - }, - backButton: 'Back', - footerActionLink__useAnotherMethod: 'Use another method', - formButtonPrimary: 'Continue', - formButtonReset: 'Cancel', - formFieldAction__hidePassword: 'Hide password', - formFieldAction__showPassword: 'Show password', - formFieldInputPlaceholder__password: 'Enter your password', - formFieldLabel__backupCode: 'Backup code', - formFieldLabel__password: 'Password', - identityPreviewEditButton__emailAddress: 'Edit email address', - identityPreviewEditButton__identifier: 'Edit identifier', - identityPreviewEditButton__phoneNumber: 'Edit phone number', - unstable__errors__generic: 'Something went wrong. Please try again.', + continue: 'Continue', + useAnotherMethod: 'Use another method', + back: 'Back', + attemptFailed: 'Something went wrong. Please try again.', /** Announced while an attempt is in flight; the button's own label stays visible. */ verifying: 'Verifying', } as const; diff --git a/packages/mosaic/src/features/reverification/reverification.view.tsx b/packages/mosaic/src/features/reverification/reverification.view.tsx index 863a17448b6..6493eb40caa 100644 --- a/packages/mosaic/src/features/reverification/reverification.view.tsx +++ b/packages/mosaic/src/features/reverification/reverification.view.tsx @@ -42,17 +42,17 @@ function methodLabel(method: ReverificationMethod, m: Messages): string { const identifier = 'identifier' in method ? method.identifier : ''; switch (method.strategy) { case 'password': - return m.alternativeMethods.blockButton__password; + return m.methods.password; case 'passkey': - return m.alternativeMethods.blockButton__passkey; + return m.methods.passkey; case 'email_code': - return fill(m.alternativeMethods.blockButton__emailCode, { identifier }); + return fill(m.methods.emailCode, { identifier }); case 'phone_code': - return fill(m.alternativeMethods.blockButton__phoneCode, { identifier }); + return fill(m.methods.phoneCode, { identifier }); case 'totp': - return m.alternativeMethods.blockButton__totp; + return m.methods.totp; case 'backup_code': - return m.alternativeMethods.blockButton__backupCode; + return m.methods.backupCode; } } @@ -76,7 +76,7 @@ export function ReverificationError({ reason, onClose }: { reason: Reverificatio fullWidth onClick={onClose} > - {m.error.closeButton} + {m.error.close} ) : null} @@ -105,7 +105,7 @@ export function ReverificationPending() { disabled xstyle={styles.pendingAction} > - {m.formButtonPrimary} + {m.continue} @@ -119,7 +119,7 @@ function otpCopy(channel: ReverificationOtpChannel | undefined, m: Messages) { if (channel === 'phone') { return m.phoneCode; } - return m.totpMfa; + return m.totp; } export function ReverificationView({ @@ -147,8 +147,8 @@ export function ReverificationView({ const otp = otpCopy(otpChannel, m); const actions = { - secondaryActionLabel: m.footerActionLink__useAnotherMethod, - primaryActionLabel: m.formButtonPrimary, + secondaryActionLabel: m.useAnotherMethod, + primaryActionLabel: m.continue, pendingLabel: m.verifying, }; const hasAlternatives = methods.length > 0; @@ -166,7 +166,7 @@ export function ReverificationView({ })} ) : ( - resendCopy.resendButton + resendCopy.resend ), disabled: !canResend || isPending, onClick: onResend, @@ -187,10 +187,10 @@ export function ReverificationView({ messages={{ title: m.password.title, description: m.password.description, - fieldLabel: m.formFieldLabel__password, - fieldPlaceholder: m.formFieldInputPlaceholder__password, - showPasswordLabel: m.formFieldAction__showPassword, - hidePasswordLabel: m.formFieldAction__hidePassword, + fieldLabel: m.password.label, + fieldPlaceholder: m.password.placeholder, + showPasswordLabel: m.password.showPassword, + hidePasswordLabel: m.password.hidePassword, ...actions, }} value={value} @@ -223,7 +223,7 @@ export function ReverificationView({ messages={{ title: otp.title, description: otp.description, - fieldLabel: otp.formTitle, + fieldLabel: otp.label, ...actions, }} value={value} @@ -244,9 +244,9 @@ export function ReverificationView({ ({ id: method.id, @@ -283,10 +283,10 @@ export function ReverificationView({ { ), ); const messages = { - userProfileActiveDevices: { detailsDialog: { signOutError: 'Erreur de déconnexion.' } }, + userProfile: { activeDevices: { detailsDialog: { signOutError: 'Erreur de déconnexion.' } } }, errors: { invalid_action_for_session: 'Cet appareil est indisponible.' }, }; await renderWithClerk( @@ -602,7 +602,7 @@ describe('Active devices', () => { @@ -795,10 +795,12 @@ describe('Change language while viewing device details', () => { messages: locale === 'fr-FR' ? { - userProfileActiveDevices: { - deviceName: '{browser} sur {device}', - lastSeen: 'Vu {date}', - detailsDialog: { lastActive: 'Dernière activité {lastActive}', signOut: 'Déconnecter' }, + userProfile: { + activeDevices: { + deviceName: '{browser} sur {device}', + lastSeen: 'Vu {date}', + detailsDialog: { lastActive: 'Dernière activité {lastActive}', signOut: 'Déconnecter' }, + }, }, } : undefined, @@ -890,7 +892,7 @@ describe('Sign out an unavailable device', () => { locale, messages: { errors: locale === 'fr-FR' ? { active_device_unavailable: expectedMessage } : undefined, - userProfileActiveDevices: { detailsDialog: { signOutError: 'Unexpected device failure.' } }, + userProfile: { activeDevices: { detailsDialog: { signOutError: 'Unexpected device failure.' } } }, }, })} > diff --git a/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/user-profile-active-devices-section.controller.ts b/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/user-profile-active-devices-section.controller.ts index 3bfcff4a8ec..40b9b1f7df8 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/user-profile-active-devices-section.controller.ts +++ b/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/user-profile-active-devices-section.controller.ts @@ -38,7 +38,7 @@ export function useUserProfileActiveDevicesController({ revokeAll, reverification, }: UserProfileActiveDevicesControllerOptions): UserProfileActiveDevicesController { - const m = useMessages('userProfileActiveDevices'); + const m = useMessages('userProfile').activeDevices; const [removedIds, setRemovedIds] = useState([]); const visibleDevices = devices.filter(device => !removedIds.includes(device.id)); const currentDeviceTriggerRef = useRef(null); diff --git a/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/user-profile-active-devices-section.model.ts b/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/user-profile-active-devices-section.model.ts index 92fb3eaefed..93b7bda5c21 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/user-profile-active-devices-section.model.ts +++ b/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/user-profile-active-devices-section.model.ts @@ -42,7 +42,7 @@ export function useUserProfileActiveDevicesModel(): UserProfileActiveDevicesMode const { isLoaded: isSessionLoaded, session } = useSession(); const now = useNow({ updateInterval: 60_000 }); const locale = useLocale(); - const m = useMessages('userProfileActiveDevices'); + const m = useMessages('userProfile').activeDevices; const userId = user?.id; const sessionId = session?.id; const [revokeSession, reverification] = useReverificationFlow((target: SessionWithActivitiesResource) => diff --git a/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/user-profile-active-devices-section.skeleton.tsx b/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/user-profile-active-devices-section.skeleton.tsx index 6beda8adfcf..fc16ebac172 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/user-profile-active-devices-section.skeleton.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/user-profile-active-devices-section.skeleton.tsx @@ -12,7 +12,7 @@ const PLACEHOLDER_DEVICES: UserProfileDevice[] = [ ]; export function UserProfileActiveDevicesSectionSkeleton() { - const m = useMessages('userProfileActiveDevices'); + const m = useMessages('userProfile').activeDevices; return ( <> {m.loading} diff --git a/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/user-profile-active-devices-section.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/user-profile-active-devices-section.view.tsx index 1ed4e72c410..f95b6830764 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/user-profile-active-devices-section.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/user-profile-active-devices-section.view.tsx @@ -32,7 +32,7 @@ export function UserProfileActiveDevicesSectionView({ signOutAll, signOutAllFinalFocus, }: UserProfileActiveDevicesSectionViewProps) { - const m = useMessages('userProfileActiveDevices'); + const m = useMessages('userProfile').activeDevices; const locale = useLocale(); const currentDevices = devices.filter(device => device.isCurrent); const otherDevices = devices.filter(device => !device.isCurrent); @@ -123,7 +123,7 @@ export function UserProfileActiveDevicesSectionView({ ); } -function deviceBadges(device: UserProfileDevice, m: MosaicMessages['userProfileActiveDevices']): string[] { +function deviceBadges(device: UserProfileDevice, m: MosaicMessages['userProfile']['activeDevices']): string[] { const labels: (string | null)[] = [ device.isCurrent ? m.thisDevice : null, device.isUserDevice ? m.userDevice : null, @@ -143,7 +143,7 @@ function DeviceItem({ onViewDetails: (device: UserProfileDevice) => void; onSignOut?: (device: UserProfileDevice) => void; }) { - const m = useMessages('userProfileActiveDevices'); + const m = useMessages('userProfile').activeDevices; const actions: ActionMenuAction[] = [{ label: m.viewDetails, onClick: () => onViewDetails(device) }]; if (onSignOut) { diff --git a/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/user-profile-device-details.dialog.tsx b/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/user-profile-device-details.dialog.tsx index 8a066cf2660..aadc6f9c539 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/user-profile-device-details.dialog.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/user-profile-device-details.dialog.tsx @@ -96,7 +96,7 @@ function DeviceDetails({ isSigningOut: boolean; errorMessage: string | undefined; }) { - const m = useMessages('userProfileActiveDevices'); + const m = useMessages('userProfile').activeDevices; const fields: { label: string; value: string | undefined }[] = [ { label: m.detailsDialog.model, value: device.model }, { label: m.detailsDialog.browser, value: device.browser }, diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.connect.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.connect.feature.test.tsx index 2595e112eaf..05a25dfb7de 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.connect.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/__tests__/user-profile-connected-accounts.connect.feature.test.tsx @@ -565,7 +565,7 @@ describe('connected accounts', () => { serveFapi(signedIn([])); await renderWithClerk( , diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-account-row.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-account-row.view.tsx index cd82e7776e2..75288f5e092 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-account-row.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-account-row.view.tsx @@ -30,7 +30,7 @@ export function UserProfileConnectProviderRowView({ onConnect: (id: string) => void; triggerRef?: Ref; }) { - const m = useMessages('userProfileConnectedAccounts'); + const m = useMessages('userProfile').connectedAccounts; return ( @@ -89,7 +89,7 @@ export function UserProfileConnectedAccountRowView({ onReconnect?: (id: string) => void; onRemove?: (account: UserProfileConnectedAccount) => void; }) { - const m = useMessages('userProfileConnectedAccounts'); + const m = useMessages('userProfile').connectedAccounts; const actions: ActionMenuAction[] = []; if ((account.status === 'reconnect' || account.status === 'error') && onReconnect && !isDisabled) { actions.push({ diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.controller.ts b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.controller.ts index 09a243bf42c..e8295ee1a13 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.controller.ts +++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.controller.ts @@ -54,7 +54,7 @@ export function useUserProfileConnectedAccountsController({ reconnectReverification, connectReverification, }: UserProfileConnectedAccountsControllerOptions): UserProfileConnectedAccountsController { - const messages = useMessages('userProfileConnectedAccounts'); + const messages = useMessages('userProfile').connectedAccounts; const actions = usePendingAction({ errorFallback: messages.errors.generic }); const sectionRef = useRef(null); const panelTitle = usePanelTitle(); diff --git a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.view.tsx index ea65ed6c7e9..0ce9ea67d93 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-connected-accounts-section/user-profile-connected-accounts-section.view.tsx @@ -29,7 +29,7 @@ export function UserProfileConnectedAccountsSectionView({ connectReverification, connectFinalFocus, }: UserProfileConnectedAccountsSectionViewProps) { - const m = useMessages('userProfileConnectedAccounts'); + const m = useMessages('userProfile').connectedAccounts; const hasRows = accounts.length > 0 || (availableProviders.length > 0 && Boolean(onConnect)); const isBusy = pendingId !== undefined; diff --git a/packages/mosaic/src/features/user-profile/user-profile-contact-list-row.view.tsx b/packages/mosaic/src/features/user-profile/user-profile-contact-list-row.view.tsx index 46e05dfc421..84098de9650 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-contact-list-row.view.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-contact-list-row.view.tsx @@ -42,8 +42,9 @@ export function UserProfileContactListRowView({ triggerRef, children, }: UserProfileContactListRowViewProps) { - const m = useMessages('userProfileContact'); - const emptyDescription = m[kind].empty; + const messages = useMessages('userProfile'); + const m = messages.contact; + const nouns = messages[kind]; return (