From 3096ff4683791921f9640bc3cb10712d8c50dbf3 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 27 Oct 2026 12:33:36 -0400 Subject: [PATCH 1/5] refactor(mosaic): nest localization keys under their public component --- .changeset/mosaic-localization-key-tree.md | 5 ++ packages/mosaic/ARCHITECTURE.md | 6 +- .../__tests__/api-keys-table.feature.test.tsx | 6 +- .../api-keys/api-keys-table.messages.ts | 2 +- .../src/features/api-keys/api-keys-table.tsx | 2 +- .../features/api-keys/api-keys-table.types.ts | 2 +- ...ganization-profile-danger-section.view.tsx | 4 +- .../invitations-table-tab.controller.ts | 2 +- .../invitations-table-tab.skeleton.tsx | 2 +- .../invitations-table-tab.view.tsx | 4 +- .../members-table-tab.controller.ts | 2 +- .../members-table-tab.skeleton.tsx | 2 +- .../members-table-tab.view.tsx | 8 +-- ...ation-profile-invite-members.controller.ts | 2 +- ...nization-profile-invite-members.dialog.tsx | 2 +- ...rganization-profile-members-panel.view.tsx | 6 +- .../requests-table-tab.skeleton.tsx | 2 +- .../requests-table-tab.view.tsx | 6 +- .../organization-profile-edit-name.dialog.tsx | 2 +- .../organization-profile-edit-slug.dialog.tsx | 2 +- .../organization-profile-logo-row.view.tsx | 4 +- .../organization-profile-name-row.view.tsx | 4 +- ...anization-profile-profile-section.view.tsx | 2 +- .../organization-profile-slug-row.view.tsx | 4 +- ...le-active-devices-section.feature.test.tsx | 16 +++-- ...ofile-active-devices-section.controller.ts | 2 +- ...er-profile-active-devices-section.model.ts | 2 +- ...rofile-active-devices-section.skeleton.tsx | 2 +- ...er-profile-active-devices-section.view.tsx | 6 +- .../user-profile-device-details.dialog.tsx | 2 +- ...onnected-accounts.connect.feature.test.tsx | 2 +- ...ser-profile-connected-account-row.view.tsx | 4 +- ...e-connected-accounts-section.controller.ts | 2 +- ...rofile-connected-accounts-section.view.tsx | 2 +- .../user-profile-contact-list-row.view.tsx | 13 ++-- .../user-profile-contact.messages.ts | 33 +-------- .../user-profile-danger-section.view.tsx | 2 +- .../user-profile-add-email.dialog.tsx | 8 +-- .../user-profile-email-section.messages.ts | 14 ++++ .../user-profile-email-section.view.tsx | 17 ++--- ...ofile-enterprise-accounts.feature.test.tsx | 2 +- ...er-profile-enterprise-account-row.view.tsx | 2 +- ...er-profile-enterprise-accounts-section.tsx | 2 +- ...ofile-enterprise-accounts-section.view.tsx | 2 +- .../user-profile-add-authenticator.view.tsx | 2 +- .../user-profile-add-mfa.dialog.tsx | 2 +- .../user-profile-add-mfa.view.tsx | 2 +- .../user-profile-add-sms.view.tsx | 4 +- .../user-profile-authenticator-setup.view.tsx | 2 +- .../user-profile-backup-codes.view.tsx | 2 +- .../user-profile-mfa-row.view.tsx | 2 +- ...ser-profile-mfa-section-leaf.controller.ts | 2 +- .../user-profile-mfa-section.controller.ts | 4 +- .../user-profile-mfa-section.view.tsx | 4 +- ...rofile-passkeys-lifecycle.feature.test.tsx | 6 +- ...-profile-passkeys-section.feature.test.tsx | 54 +++++++------- .../user-profile-passkey-row.view.tsx | 2 +- ...ser-profile-passkeys-section.controller.ts | 2 +- .../user-profile-passkeys-section.model.ts | 2 +- .../user-profile-passkeys-section.view.tsx | 2 +- .../user-profile-rename-passkey.dialog.tsx | 2 +- .../user-profile-password-feedback.test.ts | 10 +-- .../user-profile-edit-password.controller.ts | 2 +- .../user-profile-edit-password.dialog.tsx | 4 +- .../user-profile-password-errors.ts | 2 +- .../user-profile-password-feedback.ts | 2 +- .../user-profile-password-row.view.tsx | 2 +- .../user-profile-password-section.model.ts | 2 +- .../user-profile-password-section.tsx | 2 +- .../user-profile-password-section.view.tsx | 2 +- .../user-profile-phone-section.messages.ts | 14 ++++ .../user-profile-phone-section.view.tsx | 17 ++--- .../user-profile/user-profile-phone.steps.tsx | 4 +- .../user-profile-edit-name.dialog.tsx | 2 +- .../user-profile-edit-username.dialog.tsx | 4 +- .../user-profile-name-row.view.tsx | 2 +- .../user-profile-picture-row.view.tsx | 4 +- .../user-profile-profile-section.tsx | 4 +- .../user-profile-profile-section.view.tsx | 2 +- .../user-profile-username-row.view.tsx | 2 +- ...file-web3-wallets-section.feature.test.tsx | 12 ++-- .../user-profile-solana-wallet.view.tsx | 2 +- .../user-profile-web3-wallet-row.view.tsx | 2 +- ...profile-web3-wallets-section.controller.ts | 2 +- ...user-profile-web3-wallets-section.view.tsx | 2 +- packages/mosaic/src/localization/registry.ts | 70 +++++++++++-------- 86 files changed, 258 insertions(+), 229 deletions(-) create mode 100644 .changeset/mosaic-localization-key-tree.md create mode 100644 packages/mosaic/src/features/user-profile/user-profile-email-section/user-profile-email-section.messages.ts create mode 100644 packages/mosaic/src/features/user-profile/user-profile-phone-section/user-profile-phone-section.messages.ts diff --git a/.changeset/mosaic-localization-key-tree.md b/.changeset/mosaic-localization-key-tree.md new file mode 100644 index 00000000000..b97eb8e5817 --- /dev/null +++ b/.changeset/mosaic-localization-key-tree.md @@ -0,0 +1,5 @@ +--- +'@clerk/mosaic': minor +--- + +Reorganize localization keys under the component they belong to. Section and dialog strings now nest under their component instead of sitting at the top level, for example `userProfileMfa.label` is now `userProfile.mfa.label`, `membersTableTab.title` is now `organizationProfile.members.list.title`, and `apiKeysTable.search` is now `apiKeys.search`. Email and phone strings moved from `userProfileContact.email` and `userProfileContact.phone` to `userProfile.email` and `userProfile.phone`. Update any `localization` overrides that use the old keys; TypeScript flags each one. diff --git a/packages/mosaic/ARCHITECTURE.md b/packages/mosaic/ARCHITECTURE.md index 4e892d1270e..104a617f38c 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. 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/user-profile/user-profile-active-devices-section/__tests__/user-profile-active-devices-section.feature.test.tsx b/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/__tests__/user-profile-active-devices-section.feature.test.tsx index fd42ecf450d..da0ef7e1f9c 100644 --- a/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/__tests__/user-profile-active-devices-section.feature.test.tsx +++ b/packages/mosaic/src/features/user-profile/user-profile-active-devices-section/__tests__/user-profile-active-devices-section.feature.test.tsx @@ -405,7 +405,7 @@ describe('Active devices', () => { ), ); 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 ( ) : 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({ { + it('names every key segment in camelCase', () => { + const malformed = keyPaths(mosaicMessages).filter(path => { + const named = namedSegments(path); + return path.length < 2 || named.some(segment => !CAMEL_CASE.test(segment)); + }); + + expect(malformed.map(path => path.join('.'))).toEqual([]); + }); +}); + +function namedSegments(path: string[]): string[] { + const joined = path.join('.'); + const map = ID_KEYED_MAPS.find(prefix => joined.startsWith(`${prefix}.`)); + return map === undefined ? path : map.split('.'); +} + +function keyPaths(value: unknown, prefix: string[] = []): string[][] { + if (typeof value !== 'object' || value === null || isPluralForms(value)) { + return [prefix]; + } + return Object.entries(value).flatMap(([key, child]) => keyPaths(child, [...prefix, key])); +} + +function isPluralForms(value: object): boolean { + return 'other' in value && typeof Reflect.get(value, 'other') === 'string'; +} From a9ecb4ba1376089bb73c41482472931bc748c27b Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 27 Oct 2026 13:17:19 -0400 Subject: [PATCH 3/5] docs(mosaic): document how localization keys are named and placed --- .changeset/mosaic-localization-key-tree.md | 2 +- packages/mosaic/ARCHITECTURE.md | 13 +++++++++++++ packages/mosaic/docs/views.md | 19 ++++++++++++++++--- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/.changeset/mosaic-localization-key-tree.md b/.changeset/mosaic-localization-key-tree.md index b97eb8e5817..f760b6d4335 100644 --- a/.changeset/mosaic-localization-key-tree.md +++ b/.changeset/mosaic-localization-key-tree.md @@ -2,4 +2,4 @@ '@clerk/mosaic': minor --- -Reorganize localization keys under the component they belong to. Section and dialog strings now nest under their component instead of sitting at the top level, for example `userProfileMfa.label` is now `userProfile.mfa.label`, `membersTableTab.title` is now `organizationProfile.members.list.title`, and `apiKeysTable.search` is now `apiKeys.search`. Email and phone strings moved from `userProfileContact.email` and `userProfileContact.phone` to `userProfile.email` and `userProfile.phone`. Update any `localization` overrides that use the old keys; TypeScript flags each one. +Reorganize localization keys under the component they belong to. Section and dialog strings now nest under their component instead of sitting at the top level, for example `userProfileMfa.label` is now `userProfile.mfa.label`, `membersTableTab.title` is now `organizationProfile.members.list.title`, and `apiKeysTable.search` is now `apiKeys.search`. Email and phone strings moved from `userProfileContact.email` and `userProfileContact.phone` to `userProfile.email` and `userProfile.phone`. Reverification keys now use plain names, for example `reverification.alternativeMethods.blockButton__password` is now `reverification.methods.password` and `reverification.formButtonPrimary` is now `reverification.continue`; keys no reverification screen rendered were removed. Update any `localization` overrides that use the old keys; TypeScript flags each one. diff --git a/packages/mosaic/ARCHITECTURE.md b/packages/mosaic/ARCHITECTURE.md index 104a617f38c..2236b7ca16a 100644 --- a/packages/mosaic/ARCHITECTURE.md +++ b/packages/mosaic/ARCHITECTURE.md @@ -245,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. From 5d0ff39b5e70ce2490200dfd58081b6f25165850 Mon Sep 17 00:00:00 2001 From: Alex Carpenter Date: Tue, 27 Oct 2026 13:43:21 -0400 Subject: [PATCH 4/5] chore(swingset): read Mosaic messages from the nested key tree --- packages/swingset/src/stories/fixtures/api-keys-table.tsx | 2 +- .../fixtures/organization-profile-invite-members.ts | 2 +- .../src/stories/fixtures/user-profile-edit-password.tsx | 2 +- .../stories/fixtures/user-profile-mfa-example.test.tsx | 4 +++- .../src/stories/fixtures/user-profile-profile.tsx | 2 +- packages/swingset/src/stories/localization.mdx | 8 ++++++-- packages/swingset/src/stories/localization.stories.tsx | 2 +- 7 files changed, 14 insertions(+), 8 deletions(-) diff --git a/packages/swingset/src/stories/fixtures/api-keys-table.tsx b/packages/swingset/src/stories/fixtures/api-keys-table.tsx index 409f5156fa7..beec2b19415 100644 --- a/packages/swingset/src/stories/fixtures/api-keys-table.tsx +++ b/packages/swingset/src/stories/fixtures/api-keys-table.tsx @@ -74,7 +74,7 @@ export function useAPIKeysTableFixture({ showDescription?: boolean; } = {}): APIKeysTableViewProps { const locale = useLocale(); - const messages = resolveAPIKeysTableMessages(useMessages('apiKeysTable'), subjectKind); + const messages = resolveAPIKeysTableMessages(useMessages('apiKeys'), subjectKind); const seed = useChaosFixture(initialKeys, items => chaosRows(items).map((key, index) => ({ ...key, name: chaosPick(chaosAPIKeyNames, index) })), ); diff --git a/packages/swingset/src/stories/fixtures/organization-profile-invite-members.ts b/packages/swingset/src/stories/fixtures/organization-profile-invite-members.ts index 081cee9dcff..a8647d14662 100644 --- a/packages/swingset/src/stories/fixtures/organization-profile-invite-members.ts +++ b/packages/swingset/src/stories/fixtures/organization-profile-invite-members.ts @@ -18,7 +18,7 @@ export function useInviteMembersFixture(): { onInvite: (event: MouseEvent) => void; inviteDialog: OrganizationProfileInviteMembersDialogProps; } { - const m = useMessages('organizationProfileInviteMembers'); + const m = useMessages('organizationProfile').members.invite; const locale = useLocale(); const roles = useChaosFixture(exampleRoles, items => items.map((role, index) => ({ ...role, ...chaosRole(index) }))); const toast = useToastManager(); diff --git a/packages/swingset/src/stories/fixtures/user-profile-edit-password.tsx b/packages/swingset/src/stories/fixtures/user-profile-edit-password.tsx index c4344428ccc..5440b810f52 100644 --- a/packages/swingset/src/stories/fixtures/user-profile-edit-password.tsx +++ b/packages/swingset/src/stories/fixtures/user-profile-edit-password.tsx @@ -24,7 +24,7 @@ export function useUserProfileEditPasswordFixture({ latency = 800, failWith, }: UserProfileEditPasswordFixtureOptions = {}) { - const m = useMessages('userProfilePasswordSection'); + const m = useMessages('userProfile').password; const [hasPassword, setHasPassword] = useState(initialHasPassword); const [hasFailed, setHasFailed] = useState(false); const policy: UserProfilePasswordPolicy = hasPassword diff --git a/packages/swingset/src/stories/fixtures/user-profile-mfa-example.test.tsx b/packages/swingset/src/stories/fixtures/user-profile-mfa-example.test.tsx index 43d4b1a753e..9a0f1e9ef71 100644 --- a/packages/swingset/src/stories/fixtures/user-profile-mfa-example.test.tsx +++ b/packages/swingset/src/stories/fixtures/user-profile-mfa-example.test.tsx @@ -43,7 +43,9 @@ describe('Profile MFA flows', () => { 'returns from %s to the method picker without closing the dialog', async method => { const user = userEvent.setup(); - render(); + render( + , + ); const add = screen.getByRole('button', { name: 'Add a second factor' }); await user.click(add); const dialog = screen.getByRole('dialog', { name: 'Add 2-step verification' }); diff --git a/packages/swingset/src/stories/fixtures/user-profile-profile.tsx b/packages/swingset/src/stories/fixtures/user-profile-profile.tsx index 7546181de9e..4069d2f2a39 100644 --- a/packages/swingset/src/stories/fixtures/user-profile-profile.tsx +++ b/packages/swingset/src/stories/fixtures/user-profile-profile.tsx @@ -39,7 +39,7 @@ export function useUserProfileProfileFixture({ nameFailWith, usernameFailWith, }: UserProfileProfileFixtureOptions = {}): UserProfileProfileSectionViewProps { - const m = useMessages('userProfileProfileSection'); + const m = useMessages('userProfile').profile; const seed = useChaosFixture({ firstName: initialFirstName, lastName: initialLastName }, () => chaosPerson(0)); const [value, setValue] = useState(seed); const usernameSeed = useChaosFixture(initialUsername, () => chaosUsername(0)); diff --git a/packages/swingset/src/stories/localization.mdx b/packages/swingset/src/stories/localization.mdx index c7506aa97cb..81f512ea2c3 100644 --- a/packages/swingset/src/stories/localization.mdx +++ b/packages/swingset/src/stories/localization.mdx @@ -90,8 +90,12 @@ is left unused, and a placeholder the values do not cover is rendered literally. "members": { "one": "{count} miembro", "other": "{count} miembros" } } }, - "userProfileAddEmail": { - "link": { "resendCountdown": "¿No recibiste el enlace? Reenviar ({seconds})" } + "userProfile": { + "email": { + "addDialog": { + "link": { "resendCountdown": "¿No recibiste el enlace? Reenviar ({seconds})" } + } + } } } ``` diff --git a/packages/swingset/src/stories/localization.stories.tsx b/packages/swingset/src/stories/localization.stories.tsx index 1b025e1aa12..bd09e7498e4 100644 --- a/packages/swingset/src/stories/localization.stories.tsx +++ b/packages/swingset/src/stories/localization.stories.tsx @@ -129,7 +129,7 @@ function Rows({ rows }: { rows: Array<[string, ReactNode]> }) { // helper its placeholders call for. function Strings() { const m = useMessages('userButton'); - const link = useMessages('userProfileAddEmail'); + const link = useMessages('userProfile').email.addDialog; const locale = useLocale(); return ( Date: Wed, 28 Oct 2026 19:28:19 -0400 Subject: [PATCH 5/5] chore(repo): use an empty changeset for the Mosaic key tree --- .changeset/mosaic-localization-key-tree.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/.changeset/mosaic-localization-key-tree.md b/.changeset/mosaic-localization-key-tree.md index f760b6d4335..a845151cc84 100644 --- a/.changeset/mosaic-localization-key-tree.md +++ b/.changeset/mosaic-localization-key-tree.md @@ -1,5 +1,2 @@ --- -'@clerk/mosaic': minor --- - -Reorganize localization keys under the component they belong to. Section and dialog strings now nest under their component instead of sitting at the top level, for example `userProfileMfa.label` is now `userProfile.mfa.label`, `membersTableTab.title` is now `organizationProfile.members.list.title`, and `apiKeysTable.search` is now `apiKeys.search`. Email and phone strings moved from `userProfileContact.email` and `userProfileContact.phone` to `userProfile.email` and `userProfile.phone`. Reverification keys now use plain names, for example `reverification.alternativeMethods.blockButton__password` is now `reverification.methods.password` and `reverification.formButtonPrimary` is now `reverification.continue`; keys no reverification screen rendered were removed. Update any `localization` overrides that use the old keys; TypeScript flags each one.