Skip to content

Commit 80fcdcb

Browse files
authored
refactor(ui): Enforce i18n across form fields (clerk#3836)
1 parent f20b0cd commit 80fcdcb

19 files changed

Lines changed: 119 additions & 169 deletions

.changeset/warm-spies-buy.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

packages/ui/src/common/backup-code-field.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
import * as Common from '@clerk/elements/common';
22
import React from 'react';
33

4-
import * as Field from '../primitives/field';
4+
import { useLocalizations } from '~/hooks/use-localizations';
5+
import * as Field from '~/primitives/field';
6+
7+
export function BackupCodeField(props: Omit<React.ComponentProps<typeof Common.Input>, 'type'>) {
8+
const { t } = useLocalizations();
59

6-
export function BackupCodeField({
7-
label = 'Backup code',
8-
...props
9-
}: { label?: React.ReactNode } & Omit<React.ComponentProps<typeof Common.Input>, 'type'>) {
1010
return (
1111
<Common.Field
1212
name='backup_code'
1313
asChild
1414
>
1515
<Field.Root>
1616
<Common.Label asChild>
17-
<Field.Label>{label}</Field.Label>
17+
<Field.Label>{t('formFieldLabel__backupCode')}</Field.Label>
1818
</Common.Label>
1919
<Common.FieldState>
2020
{({ state }) => {

packages/ui/src/common/checkbox-field.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as Common from '@clerk/elements/common';
22
import React from 'react';
33

4-
import * as Field from '../primitives/field';
4+
import * as Field from '~/primitives/field';
55

66
export function CheckboxField({
77
label,

packages/ui/src/common/connections.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as Common from '@clerk/elements/common';
22

3-
import { PROVIDERS } from '~/constants';
3+
import { PROVIDERS } from '~/constants/providers';
44
import { useEnabledConnections } from '~/hooks/use-enabled-connections';
55
import * as Connection from '~/primitives/connection';
66
import * as Icon from '~/primitives/icon';

packages/ui/src/common/email-field.tsx

Lines changed: 6 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -3,44 +3,26 @@ import React from 'react';
33

44
import { useAttributes } from '~/hooks/use-attributes';
55
import { useLocalizations } from '~/hooks/use-localizations';
6-
7-
import * as Field from '../primitives/field';
6+
import * as Field from '~/primitives/field';
87

98
const DEFAULT_FIELD_NAME = 'emailAddress';
10-
const DEFAULT_ERROR_NAME = 'email_address';
119

1210
export function EmailField({
1311
alternativeFieldTrigger,
14-
label,
1512
name = DEFAULT_FIELD_NAME,
16-
hintText,
1713
enabled,
18-
error,
1914
required,
2015
...props
2116
}: {
2217
alternativeFieldTrigger?: React.ReactNode;
23-
label?: React.ReactNode;
24-
hintText?: string;
2518
enabled?: boolean;
26-
error?: (message: string, code: string, name: string) => string;
2719
} & Omit<React.ComponentProps<typeof Common.Input>, 'type'>) {
2820
const { t, translateError } = useLocalizations();
2921
const { enabled: attributeEnabled, required: attributeRequired } = useAttributes('email_address');
3022

31-
const renderLabel = label ? label : t('formFieldLabel__emailAddress');
32-
const renderError = error ? error : translateError;
3323
const isEnabled = enabled !== undefined ? enabled : attributeEnabled;
3424
const isRequired = required !== undefined ? required : attributeRequired;
35-
const renderHintText = () => {
36-
if (hintText) {
37-
return hintText;
38-
}
39-
if (!isRequired) {
40-
return t('formFieldHintText__optional');
41-
}
42-
return null;
43-
};
25+
4426
if (!isEnabled) {
4527
return null;
4628
}
@@ -53,11 +35,11 @@ export function EmailField({
5335
<Field.Root>
5436
<Common.Label asChild>
5537
<Field.Label>
56-
{renderLabel}
38+
{t('formFieldLabel__emailAddress')}
5739
{alternativeFieldTrigger ? (
5840
<Field.LabelEnd>{alternativeFieldTrigger}</Field.LabelEnd>
59-
) : renderHintText ? (
60-
<Field.Hint>{renderHintText()}</Field.Hint>
41+
) : !isRequired ? (
42+
<Field.Hint>{t('formFieldHintText__optional')}</Field.Hint>
6143
) : null}
6244
</Field.Label>
6345
</Common.Label>
@@ -77,7 +59,7 @@ export function EmailField({
7759
</Common.FieldState>
7860
<Common.FieldError asChild>
7961
{({ message, code }) => {
80-
return <Field.Message intent='error'>{renderError(message, code, DEFAULT_ERROR_NAME)}</Field.Message>;
62+
return <Field.Message intent='error'>{translateError(message, code, 'email_address')}</Field.Message>;
8163
}}
8264
</Common.FieldError>
8365
</Field.Root>

packages/ui/src/common/email-or-phone-number-field.tsx

Lines changed: 10 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@ import { PhoneNumberField } from './phone-number-field';
1010
export function EmailOrPhoneNumberField({
1111
className,
1212
name = undefined,
13-
labelEmail = 'Email address',
14-
labelPhoneNumber = 'Phone number',
15-
locationBasedCountryIso,
16-
toggleLabelEmail = 'Use email',
17-
toggleLabelPhoneNumber = 'Use phone',
13+
toggleLabelEmail,
14+
toggleLabelPhoneNumber,
1815
...props
1916
}: {
20-
labelEmail?: React.ReactNode;
21-
labelPhoneNumber?: React.ReactNode;
22-
locationBasedCountryIso: React.ComponentProps<typeof PhoneNumberField>['locationBasedCountryIso'];
23-
toggleLabelEmail?: string;
24-
toggleLabelPhoneNumber?: string;
17+
/**
18+
* **Note:** this prop is required as the translation differs between `signIn` and `signUp`
19+
*/
20+
toggleLabelEmail: React.ReactNode;
21+
/**
22+
* **Note:** this prop is required as the translation differs between `signIn` and `signUp`
23+
*/
24+
toggleLabelPhoneNumber: React.ReactNode;
2525
} & Omit<React.ComponentProps<typeof Common.Input>, 'type'>) {
2626
const [showPhoneNumberField, setShowPhoneNumberField] = React.useState(false);
2727

@@ -37,8 +37,6 @@ export function EmailOrPhoneNumberField({
3737

3838
return showPhoneNumberField ? (
3939
<PhoneNumberField
40-
label={labelPhoneNumber}
41-
locationBasedCountryIso={locationBasedCountryIso}
4240
alternativeFieldTrigger={toggle}
4341
name={name}
4442
{...props}
@@ -47,7 +45,6 @@ export function EmailOrPhoneNumberField({
4745
<EmailField
4846
{...props}
4947
name={name}
50-
label={labelEmail}
5148
alternativeFieldTrigger={toggle}
5249
/>
5350
);

packages/ui/src/common/email-or-username-field.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import * as Common from '@clerk/elements/common';
22
import React from 'react';
33

4-
import * as Field from '../primitives/field';
4+
import { useLocalizations } from '~/hooks/use-localizations';
5+
import * as Field from '~/primitives/field';
56

67
export function EmailOrUsernameField({
78
alternativeFieldTrigger,
8-
label = 'Email address or username',
9+
910
...props
1011
}: {
1112
alternativeFieldTrigger?: React.ReactNode;
12-
label?: React.ReactNode;
1313
} & Omit<React.ComponentProps<typeof Common.Input>, 'type'>) {
14+
const { t } = useLocalizations();
15+
1416
return (
1517
<Common.Field
1618
name='identifier'
@@ -19,7 +21,7 @@ export function EmailOrUsernameField({
1921
<Field.Root>
2022
<Common.Label asChild>
2123
<Field.Label>
22-
{label}{' '}
24+
{t('formFieldLabel__emailAddress_username')}{' '}
2325
{alternativeFieldTrigger && <span className='flex-grow self-end text-end'>{alternativeFieldTrigger}</span>}
2426
</Field.Label>
2527
</Common.Label>

packages/ui/src/common/email-or-username-or-phone-number-field.tsx

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import type * as Common from '@clerk/elements/common';
22
import * as React from 'react';
33
import { ToggleButton } from 'react-aria-components';
44

5+
import { LOCALIZATION_NEEDED } from '~/constants/localizations';
56
import { link } from '~/primitives/link';
67

78
import { EmailOrUsernameField } from './email-or-username-field';
@@ -10,20 +11,18 @@ import { PhoneNumberField } from './phone-number-field';
1011
export function EmailOrUsernameOrPhoneNumberField({
1112
className,
1213
name = undefined,
13-
labelEmailOrUsername = 'Email address or username',
14-
labelPhoneNumber = 'Phone number',
15-
toggleLabelEmailOrUsername = 'Use email or username',
16-
toggleLabelPhoneNumber = 'Use phone',
17-
locationBasedCountryIso,
18-
toggleDescription = 'Toggle between email or username, and phone.',
14+
toggleLabelEmailOrUsername,
15+
toggleLabelPhoneNumber,
1916
...props
2017
}: {
21-
labelEmailOrUsername?: React.ReactNode;
22-
labelPhoneNumber?: React.ReactNode;
23-
toggleLabelEmailOrUsername?: string;
24-
toggleLabelPhoneNumber?: string;
25-
locationBasedCountryIso: React.ComponentProps<typeof PhoneNumberField>['locationBasedCountryIso'];
26-
toggleDescription?: string;
18+
/**
19+
* **Note:** this prop is required as the translation differs between `signIn` and `signUp`
20+
*/
21+
toggleLabelEmailOrUsername: React.ReactNode;
22+
/**
23+
* **Note:** this prop is required as the translation differs between `signIn` and `signUp`
24+
*/
25+
toggleLabelPhoneNumber: React.ReactNode;
2726
} & Omit<React.ComponentProps<typeof Common.Input>, 'type'>) {
2827
const [showPhoneNumberField, setShowPhoneNumberField] = React.useState(false);
2928

@@ -33,24 +32,21 @@ export function EmailOrUsernameOrPhoneNumberField({
3332
onChange={setShowPhoneNumberField}
3433
className={link({ size: 'sm', disabled: props.disabled, focusVisible: 'data-attribute' })}
3534
>
36-
<span className='sr-only'>{toggleDescription}</span>
35+
<span className='sr-only'>{LOCALIZATION_NEEDED.formFieldAccessibleLabel__emailOrUsernameOrPhone}</span>
3736
{showPhoneNumberField ? toggleLabelEmailOrUsername : toggleLabelPhoneNumber}
3837
</ToggleButton>
3938
);
4039

4140
return showPhoneNumberField ? (
4241
<PhoneNumberField
43-
label={labelPhoneNumber}
4442
name={name}
45-
locationBasedCountryIso={locationBasedCountryIso}
4643
alternativeFieldTrigger={toggle}
4744
{...props}
4845
/>
4946
) : (
5047
<EmailOrUsernameField
5148
{...props}
5249
name={name}
53-
label={labelEmailOrUsername}
5450
alternativeFieldTrigger={toggle}
5551
/>
5652
);

packages/ui/src/common/first-name-field.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import * as Common from '@clerk/elements/common';
22
import React from 'react';
33

4-
import * as Field from '../primitives/field';
4+
import { useLocalizations } from '~/hooks/use-localizations';
5+
import * as Field from '~/primitives/field';
6+
7+
export function FirstNameField(props: Omit<React.ComponentProps<typeof Common.Input>, 'type'>) {
8+
const { t, translateError } = useLocalizations();
59

6-
export function FirstNameField({
7-
label = 'First name',
8-
hintText = 'Optional',
9-
...props
10-
}: { label?: React.ReactNode; hintText?: string } & Omit<React.ComponentProps<typeof Common.Input>, 'type'>) {
1110
return (
1211
<Common.Field
1312
name='firstName'
@@ -16,7 +15,8 @@ export function FirstNameField({
1615
<Field.Root>
1716
<Common.Label asChild>
1817
<Field.Label>
19-
{label} {!props?.required ? <Field.Hint>{hintText}</Field.Hint> : null}
18+
{t('formFieldLabel__firstName')}{' '}
19+
{!props?.required ? <Field.Hint>{t('formFieldHintText__optional')}</Field.Hint> : null}
2020
</Field.Label>
2121
</Common.Label>
2222
<Common.FieldState>
@@ -33,8 +33,8 @@ export function FirstNameField({
3333
}}
3434
</Common.FieldState>
3535
<Common.FieldError asChild>
36-
{({ message }) => {
37-
return <Field.Message intent='error'>{message}</Field.Message>;
36+
{({ message, code }) => {
37+
return <Field.Message intent='error'>{translateError(message, code, 'first_name')}</Field.Message>;
3838
}}
3939
</Common.FieldError>
4040
</Field.Root>

packages/ui/src/common/last-name-field.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
import * as Common from '@clerk/elements/common';
22
import React from 'react';
33

4-
import * as Field from '../primitives/field';
4+
import { useLocalizations } from '~/hooks/use-localizations';
5+
import * as Field from '~/primitives/field';
6+
7+
export function LastNameField(props: Omit<React.ComponentProps<typeof Common.Input>, 'type'>) {
8+
const { t, translateError } = useLocalizations();
59

6-
export function LastNameField({
7-
label = 'Last name',
8-
hintText = 'Optional',
9-
...props
10-
}: { label?: React.ReactNode; hintText?: string } & Omit<React.ComponentProps<typeof Common.Input>, 'type'>) {
1110
return (
1211
<Common.Field
1312
name='lastName'
@@ -16,7 +15,8 @@ export function LastNameField({
1615
<Field.Root>
1716
<Common.Label asChild>
1817
<Field.Label>
19-
{label} {!props?.required ? <Field.Hint>{hintText}</Field.Hint> : null}
18+
{t('formFieldLabel__lastName')}{' '}
19+
{!props?.required ? <Field.Hint>{t('formFieldHintText__optional')}</Field.Hint> : null}
2020
</Field.Label>
2121
</Common.Label>
2222
<Common.FieldState>
@@ -33,8 +33,8 @@ export function LastNameField({
3333
}}
3434
</Common.FieldState>
3535
<Common.FieldError asChild>
36-
{({ message }) => {
37-
return <Field.Message intent='error'>{message}</Field.Message>;
36+
{({ message, code }) => {
37+
return <Field.Message intent='error'>{translateError(message, code, 'last_name')}</Field.Message>;
3838
}}
3939
</Common.FieldError>
4040
</Field.Root>

0 commit comments

Comments
 (0)