Skip to content

Commit cd970b7

Browse files
refactor(mosaic): nest localization keys under their public component (#10458)
1 parent b867ed0 commit cd970b7

99 files changed

Lines changed: 422 additions & 335 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@clerk/mosaic': patch
3+
---
4+
5+
Nest localization keys under the component they appear in, for example `userProfileMfa.label` is now `userProfile.mfa.label`. Update any `localization` overrides that use the old keys; TypeScript flags each one.

‎packages/mosaic/ARCHITECTURE.md‎

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -208,8 +208,10 @@ user-button.styles.ts // `stylex.create` atoms (see the StyleX authoring
208208
model nor the view owns it, and the two cannot drift.
209209

210210
`*.messages.ts` holds strings only, exported as one `<feature>Messages` object
211-
declared `as const` and registered under a namespace in `src/localization/registry.ts`.
212-
Views read their namespace inside the component with `useMessages('userButton')`, so
211+
declared `as const` and mounted in `src/localization/registry.ts` under the public
212+
component it belongs to, such as `userProfile.mfa.backupCodes`. A root is a public
213+
component or a shared block, never a file name. Views read their root inside the
214+
component with `useMessages('userProfile').mfa`, so
213215
the `localization` prop on `MosaicProvider` can replace any string; a module-level
214216
helper takes the messages as a parameter rather than importing them.
215217

@@ -247,6 +249,19 @@ so a consumer's overrides type-check against the real keys, as a dot path
247249
Renaming or moving a key breaks every override that names it; treat it like renaming
248250
a prop.
249251

252+
The key tree follows what the user sees, so a key can be found from the screen:
253+
254+
- A root is a public component (`userProfile`) or a shared block (`form`, `errors`).
255+
There is no `common` root. The same English in two components is two keys,
256+
because a translation may need to differ by context.
257+
- Below the root, keys nest by section, then by the dialog or step inside it:
258+
`userProfile.email.addDialog`, `userProfile.email.removeDialog`.
259+
- Copy that several views render through one shared component belongs to that
260+
component (`userProfile.contact` for the row both email and phone use).
261+
- Every segment is camelCase. A map keyed by data ids (`errors`, `roles`,
262+
`userProfile.mfa.methods`) keeps the ids as its keys;
263+
`src/localization/__tests__/registry.test.ts` lists those maps and checks the rest.
264+
250265
### Composition
251266

252267
Two shapes, chosen by whether the slice fetches its own data.

‎packages/mosaic/docs/views.md‎

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,13 +48,26 @@ Every string a view renders comes from the feature's `*.messages.ts`, shaped the
4848
way `@clerk/i18n` takes a base definition. A plural message is its forms; a
4949
parameterized one is its template.
5050

51-
Register the file as a namespace in `src/localization/registry.ts`, then read it
52-
with `useMessages` in the view or controller that renders it:
51+
To add copy for a new section, step or dialog:
52+
53+
1. Put the strings in the `*.messages.ts` next to the view that renders them. Name
54+
each key for what the string is (`label`, `resend`, `confirm`), not where it
55+
sits in the markup.
56+
2. Mount the object in `src/localization/registry.ts` under the public component
57+
the user sees it in: a section of `UserProfile` goes at `userProfile.<section>`,
58+
and a dialog inside it at `userProfile.<section>.addDialog` or `.removeDialog`.
59+
Copy two views share goes under the component that renders it, never under a
60+
new `common` root.
61+
3. Read it with `useMessages` and the root, in the view or controller that
62+
renders it:
5363

5464
```tsx
55-
const m = useMessages('userButton');
65+
const m = useMessages('userProfile').mfa;
5666
```
5767

68+
The rules behind these steps are in [ARCHITECTURE.md](../ARCHITECTURE.md), and
69+
`src/localization/__tests__/registry.test.ts` fails on a key that is not camelCase.
70+
5871
Never import a `*.messages.ts` directly. A direct import skips the customer's
5972
locale and overrides, so the copy can't be localized.
6073

‎packages/mosaic/src/features/api-keys/__tests__/api-keys-table.feature.test.tsx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -247,9 +247,9 @@ describe('APIKeysTable', () => {
247247
<MosaicProvider
248248
localization={{
249249
overrides: {
250-
'apiKeysTable.search': 'Find a key',
251-
'apiKeysTable.expires': 'Expiration: {expiresDate}',
252-
'apiKeysTable.revokeTitle': 'Retirer {name} ?',
250+
'apiKeys.search': 'Find a key',
251+
'apiKeys.expires': 'Expiration: {expiresDate}',
252+
'apiKeys.revokeTitle': 'Retirer {name} ?',
253253
},
254254
}}
255255
>

‎packages/mosaic/src/features/api-keys/api-keys-table.messages.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ export const apiKeysTableMessages = {
6969
} as const;
7070

7171
export function resolveAPIKeysTableMessages(
72-
catalog: MosaicMessages['apiKeysTable'],
72+
catalog: MosaicMessages['apiKeys'],
7373
subjectKind: APIKeysTableSubjectKind,
7474
overrides?: Partial<APIKeysTableMessages>,
7575
): APIKeysTableMessages {

‎packages/mosaic/src/features/api-keys/api-keys-table.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ function SubjectAPIKeysTable({
3838
fallback,
3939
}: APIKeysTableProps) {
4040
const messages = resolveAPIKeysTableMessages(
41-
useMessages('apiKeysTable'),
41+
useMessages('apiKeys'),
4242
isOrganizationId(subject) ? 'organization' : 'user',
4343
overrides,
4444
);

‎packages/mosaic/src/features/api-keys/api-keys-table.types.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export type APIKeysTableSubjectKind = 'user' | 'organization';
2020

2121
type Strings<T> = { readonly [K in keyof T]: T[K] extends string ? string : Strings<T[K]> };
2222

23-
export type APIKeysTableMessages = Strings<Omit<MosaicMessages['apiKeysTable'], 'noKeysDescription'>> & {
23+
export type APIKeysTableMessages = Strings<Omit<MosaicMessages['apiKeys'], 'noKeysDescription'>> & {
2424
readonly noKeysDescription: string;
2525
};
2626

‎packages/mosaic/src/features/organization-profile/organization-profile-danger-section/organization-profile-danger-section.view.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ export function OrganizationProfileDangerSectionView({
1717
leave,
1818
destroy,
1919
}: OrganizationProfileDangerSectionViewProps) {
20-
const m = useMessages('organizationProfileDangerSection');
20+
const m = useMessages('organizationProfile').danger;
2121
const locale = useLocale();
2222

2323
if (!leave && !destroy) {
@@ -78,7 +78,7 @@ function DangerRow({
7878
dialogTitle: string;
7979
dialogDescription: string;
8080
}) {
81-
const m = useMessages('organizationProfileDangerSection');
81+
const m = useMessages('organizationProfile').danger;
8282

8383
return (
8484
<Section.Row>

‎packages/mosaic/src/features/organization-profile/organization-profile-members-panel/invitations-table-tab.controller.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export function useInvitationsTableController(
1212
): InvitationsTableTabViewProps {
1313
const locale = useLocale();
1414
const roleNames = useMessages('roles');
15-
const m = useMessages('invitationsTableTab');
15+
const m = useMessages('organizationProfile').members.invitations;
1616
const invitations = model.rows.map(({ id, emailAddress, createdAt, role, roleName }) => ({
1717
id,
1818
email: emailAddress,

‎packages/mosaic/src/features/organization-profile/organization-profile-members-panel/invitations-table-tab.skeleton.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export function InvitationsTableTabSkeleton({
4444
hasRevoke: boolean;
4545
hasBulkAction: boolean;
4646
}) {
47-
const m = useMessages('invitationsTableTab');
47+
const m = useMessages('organizationProfile').members.invitations;
4848
return (
4949
<div
5050
{...mergeStyleProps(themeProps('invitations-table-tab'), stylex.props(tableTabStyles.root))}

0 commit comments

Comments
 (0)