@@ -18,7 +18,7 @@ import {
1818 EMAIL_MIN_LEN ,
1919 NAME_MAX_LEN ,
2020 NAME_MIN_LEN ,
21- PASSWORD_MAX_LEN ,
21+ PASSWORD_MAX_BYTES ,
2222 PASSWORD_MIN_LEN ,
2323 PASSWORD_POLICY_REGEX ,
2424} from '@/lib/auth/signup-constraints' ;
@@ -28,8 +28,13 @@ type SignupFormProps = {
2828 returnTo : string ;
2929} ;
3030
31+ function utf8ByteLength ( value : string ) : number {
32+ return new TextEncoder ( ) . encode ( value ) . length ;
33+ }
34+
3135export function SignupForm ( { locale, returnTo } : SignupFormProps ) {
3236 const t = useTranslations ( 'auth.signup' ) ;
37+ const tf = useTranslations ( 'auth.fields' ) ;
3338
3439 const [ loading , setLoading ] = useState ( false ) ;
3540 const [ error , setError ] = useState < string | null > ( null ) ;
@@ -68,15 +73,17 @@ export function SignupForm({ locale, returnTo }: SignupFormProps) {
6873 const passwordPolicyOk = useMemo ( ( ) => {
6974 if ( ! passwordValue ) return false ;
7075 if ( passwordValue . length < PASSWORD_MIN_LEN ) return false ;
71- if ( passwordValue . length > PASSWORD_MAX_LEN ) return false ;
72- return PASSWORD_POLICY_REGEX . test ( passwordValue ) ;
76+ if ( ! PASSWORD_POLICY_REGEX . test ( passwordValue ) ) return false ;
77+ if ( utf8ByteLength ( passwordValue ) > PASSWORD_MAX_BYTES ) return false ;
78+ return true ;
7379 } , [ passwordValue ] ) ;
7480
7581 const confirmPasswordPolicyOk = useMemo ( ( ) => {
7682 if ( ! confirmPasswordValue ) return false ;
7783 if ( confirmPasswordValue . length < PASSWORD_MIN_LEN ) return false ;
78- if ( confirmPasswordValue . length > PASSWORD_MAX_LEN ) return false ;
79- return PASSWORD_POLICY_REGEX . test ( confirmPasswordValue ) ;
84+ if ( ! PASSWORD_POLICY_REGEX . test ( confirmPasswordValue ) ) return false ;
85+ if ( utf8ByteLength ( confirmPasswordValue ) > PASSWORD_MAX_BYTES ) return false ;
86+ return true ;
8087 } , [ confirmPasswordValue ] ) ;
8188
8289 const passwordsMatch = useMemo ( ( ) => {
@@ -86,36 +93,37 @@ export function SignupForm({ locale, returnTo }: SignupFormProps) {
8693
8794 const nameErrorText =
8895 nameTouched && ! nameLooksValid
89- ? `Name must be at least ${ NAME_MIN_LEN } characters and at most ${ NAME_MAX_LEN } characters`
96+ ? tf ( 'validation.invalidName' , { NAME_MIN_LEN , NAME_MAX_LEN } )
9097 : null ;
9198
9299 const emailErrorText = useMemo ( ( ) => {
93100 if ( ! emailTouched ) return null ;
94-
95- if ( ! emailTrimmed ) return "Email is required" ;
101+ if ( ! emailTrimmed ) return null ;
96102
97103 if ( emailTrimmed . length > EMAIL_MAX_LEN ) {
98- return `Email must not exceed ${ EMAIL_MAX_LEN } characters.` ;
104+ return tf ( 'validation.emailTooLong' , { EMAIL_MAX_LEN } ) ;
99105 }
100106
101107 if ( ! emailFormatOk ) {
102- return 'Email format is invalid.' ;
108+ return tf ( 'validation.invalidEmail' ) ;
103109 }
104110
105111 return null ;
106- } , [ emailTouched , emailTrimmed , emailFormatOk ] ) ;
112+ } , [ emailTouched , emailTrimmed , emailFormatOk , tf ] ) ;
107113
108- const passwordRequirementsText =
109- '8–128 characters, at least one capital letter, and at least one special character.' ;
114+ const passwordRequirementsText = tf ( 'validation.passwordRequirements' , {
115+ PASSWORD_MIN_LEN ,
116+ PASSWORD_MAX_BYTES ,
117+ } ) ;
110118
111119 const passwordErrorText =
112120 passwordTouched && ! passwordPolicyOk
113- ? `Password must meet requirements: ${ passwordRequirementsText } `
121+ ? tf ( 'validation.invalidPassword' , { passwordRequirementsText } )
114122 : null ;
115123
116124 const confirmPolicyErrorText =
117125 confirmPasswordTouched && ! confirmPasswordPolicyOk
118- ? `Password must meet requirements: ${ passwordRequirementsText } `
126+ ? tf ( 'validation.invalidPassword' , { passwordRequirementsText } )
119127 : null ;
120128
121129 const mismatchErrorText =
@@ -124,11 +132,10 @@ export function SignupForm({ locale, returnTo }: SignupFormProps) {
124132 passwordValue . length > 0 &&
125133 confirmPasswordValue . length > 0 &&
126134 ! passwordsMatch
127- ? 'Passwords do not match.'
135+ ? tf ( 'validation.passwordsDontMatch' )
128136 : null ;
129137
130- const confirmPasswordErrorText =
131- mismatchErrorText ?? confirmPolicyErrorText ?? null ;
138+ const confirmPasswordErrorText = mismatchErrorText ?? confirmPolicyErrorText ?? null ;
132139
133140 const submitDisabled =
134141 loading ||
@@ -238,9 +245,7 @@ export function SignupForm({ locale, returnTo }: SignupFormProps) {
238245 onChange = { setNameValue }
239246 onBlur = { ( ) => setNameTouched ( true ) }
240247 />
241- { nameErrorText && (
242- < p className = "text-sm text-red-600" > { nameErrorText } </ p >
243- ) }
248+ { nameErrorText && < p className = "text-sm text-red-600" > { nameErrorText } </ p > }
244249 </ div >
245250
246251 < div className = "space-y-1" >
@@ -250,47 +255,53 @@ export function SignupForm({ locale, returnTo }: SignupFormProps) {
250255 onChange = { value => {
251256 setEmailValueLive ( value ) ;
252257 setEmail ( value ) ;
253-
254258 } }
255259 onBlur = { ( ) => setEmailTouched ( true ) }
256260 />
257- { emailErrorText && (
258- < p className = "text-sm text-red-600" > { emailErrorText } </ p >
259- ) }
261+ { emailErrorText && < p className = "text-sm text-red-600" > { emailErrorText } </ p > }
260262 </ div >
261263
262264 < div className = "space-y-1" >
263265 < PasswordField
264266 id = "password"
265267 name = "password"
266- placeholder = "Password"
268+ placeholder = { tf ( 'password' ) }
267269 autoComplete = "new-password"
268270 minLength = { PASSWORD_MIN_LEN }
269- maxLength = { PASSWORD_MAX_LEN }
270271 pattern = { PASSWORD_POLICY_REGEX . source }
271272 onChange = { setPasswordValue }
272273 onBlur = { ( ) => setPasswordTouched ( true ) }
273274 />
274275 { passwordErrorText && (
275276 < p className = "text-sm text-red-600" > { passwordErrorText } </ p >
276277 ) }
278+ { passwordTouched && utf8ByteLength ( passwordValue ) > PASSWORD_MAX_BYTES && (
279+ < p className = "text-sm text-red-600" >
280+ { tf ( 'validation.passwordTooLongBytes' , { PASSWORD_MAX_BYTES } ) }
281+ </ p >
282+ ) }
277283 </ div >
278284
279285 < div className = "space-y-1" >
280286 < PasswordField
281287 id = "confirmPassword"
282288 name = "confirmPassword"
283- placeholder = "Confirm password"
289+ placeholder = { tf ( 'confirmPassword' ) }
284290 autoComplete = "new-password"
285291 minLength = { PASSWORD_MIN_LEN }
286- maxLength = { PASSWORD_MAX_LEN }
287292 pattern = { PASSWORD_POLICY_REGEX . source }
288293 onChange = { setConfirmPasswordValue }
289294 onBlur = { ( ) => setConfirmPasswordTouched ( true ) }
290295 />
291296 { confirmPasswordErrorText && (
292297 < p className = "text-sm text-red-600" > { confirmPasswordErrorText } </ p >
293298 ) }
299+ { confirmPasswordTouched &&
300+ utf8ByteLength ( confirmPasswordValue ) > PASSWORD_MAX_BYTES && (
301+ < p className = "text-sm text-red-600" >
302+ { tf ( 'validation.passwordTooLongBytes' , { PASSWORD_MAX_BYTES } ) }
303+ </ p >
304+ ) }
294305 </ div >
295306
296307 { error && < AuthErrorBanner message = { error } /> }
0 commit comments