From d8bead3e1dddacc185f97a243338e7d55a9bdfdd Mon Sep 17 00:00:00 2001 From: Sajid Mannikeri Date: Wed, 12 Aug 2026 23:56:51 +0530 Subject: [PATCH] Fix validation error message translations Signed-off-by: Sajid Mannikeri --- .../auth/Recovery/BaseRecovery.tsx | 7 --- .../presentation/auth/SignIn/BaseSignIn.tsx | 17 +++--- .../presentation/auth/SignUp/BaseSignUp.tsx | 8 --- packages/react/src/hooks/useForm.ts | 52 ++++++++++++++++++- 4 files changed, 59 insertions(+), 25 deletions(-) diff --git a/packages/react/src/components/presentation/auth/Recovery/BaseRecovery.tsx b/packages/react/src/components/presentation/auth/Recovery/BaseRecovery.tsx index f34e3fe6..dc4774cf 100644 --- a/packages/react/src/components/presentation/auth/Recovery/BaseRecovery.tsx +++ b/packages/react/src/components/presentation/auth/Recovery/BaseRecovery.tsx @@ -189,13 +189,6 @@ const BaseRecoveryContent: FC = ({ if (component.required && (!value || value.trim() === '')) { return t('validations.required.field.error'); } - if ( - (component.type === EmbeddedFlowComponentType.EmailInput || component.variant === 'EMAIL') && - value && - !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) - ) { - return t('field.email.invalid'); - } // Evaluate declarative validation rules from meta.components[].validation. if (ruleValidator && value) { const ruleMessage = ruleValidator(value); diff --git a/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx b/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx index bad2812e..a0af0d23 100644 --- a/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx +++ b/packages/react/src/components/presentation/auth/SignIn/BaseSignIn.tsx @@ -255,7 +255,7 @@ const BaseSignInContent: FC = ({ const {meta, vendor} = useThunderID(); const {theme} = useTheme(); const customRenderers: ComponentRendererMap = useContext(ComponentRendererContext); - const {t} = useTranslation(); + const {t, currentLanguage} = useTranslation(); const {subtitle: flowSubtitle, title: flowTitle, messages: flowMessages, addMessage, clearMessages} = useFlow(); const styles: any = useStyles(theme, theme.vars.colors.text.primary); @@ -319,14 +319,6 @@ const BaseSignInContent: FC = ({ if (component.required && (!value || value.trim() === '')) { return t('validations.required.field.error'); } - // Add email validation if it's an email field - if ( - (component.type === 'EMAIL_INPUT' || component.variant === 'EMAIL') && - value && - !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) - ) { - return t('field.email.invalid'); - } // Run declarative rules from meta.components[].validation. if (ruleValidator && value) { const ruleMessage = ruleValidator(value); @@ -378,6 +370,7 @@ const BaseSignInContent: FC = ({ validateForm, touchAllFields, reset: resetForm, + revalidateTouchedFields, } = form; // Project server-side fieldErrors into form state. `setTouchedFields` is used instead @@ -400,6 +393,12 @@ const BaseSignInContent: FC = ({ setFormErrors(errors); }, [serverFieldErrors, setFormErrors, setTouchedFields, clearFormErrors]); + // Re-translate displayed validation errors when the UI language changes. + // revalidateTouchedFields is stable, so this effect fires only on language change. + useEffect(() => { + revalidateTouchedFields(); + }, [currentLanguage, revalidateTouchedFields]); + /** * Handle input value changes. * Only updates the value without marking as touched. diff --git a/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.tsx b/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.tsx index 4dc7315c..34bedb54 100644 --- a/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.tsx +++ b/packages/react/src/components/presentation/auth/SignUp/BaseSignUp.tsx @@ -427,14 +427,6 @@ const BaseSignUpContent: FC = ({ if (component.required && (!value || value.trim() === '')) { return t('validations.required.field.error'); } - // Add email validation if it's an email field - if ( - (component.type === EmbeddedFlowComponentType.EmailInput || component.variant === 'EMAIL') && - value && - !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) - ) { - return t('field.email.invalid'); - } // Evaluate declarative validation rules from meta.components[].validation. if (ruleValidator && value) { const ruleMessage = ruleValidator(value); diff --git a/packages/react/src/hooks/useForm.ts b/packages/react/src/hooks/useForm.ts index eb584f83..f21a8a1b 100644 --- a/packages/react/src/hooks/useForm.ts +++ b/packages/react/src/hooks/useForm.ts @@ -1,7 +1,7 @@ // Copyright 2025 The ThunderID Authors // SPDX-License-Identifier: Apache-2.0 -import {useState, useCallback, FormEvent} from 'react'; +import {useState, useCallback, useRef, FormEvent} from 'react'; /** * Generic form field configuration @@ -163,6 +163,11 @@ export interface UseFormReturn> { * Validate all fields */ validateForm: () => ValidationResult; + /** + * Re-run validation for all touched fields that have a client-side config, + * refreshing stored error strings to the current language. + */ + revalidateTouchedFields: () => void; /** * Current form values */ @@ -229,6 +234,9 @@ export const useForm = >(config: UseFormConfig< const [errors, setFormErrors] = useState>({} as Record); const [isSubmitted, setIsSubmitted] = useState(false); + // Ref to track which fields have client-side validation rules. Errors injected via + // serErrors are not added here, so revalidateTouchedFields preserves server-side errors. + const clientErrorFieldRef = useRef(new Set()); // Get field configuration by name const getFieldConfig: (name: keyof T) => FormField | undefined = useCallback( (name: keyof T): FormField | undefined => fields.find((field: FormField) => field.name === name), @@ -246,6 +254,40 @@ export const useForm = >(config: UseFormConfig< [values, getFieldConfig, requiredMessage], ); + // "Latest value" ref so revalidateTouchedFields can read fresh validators without + // appearing in any effect dep array (avoids exhaustive-deps violations at call sites). + const validateFieldRef = useRef(validateField); + validateFieldRef.current = validateField; + + // Re-translate all, client-validated error strings on language change. + // Stable identity ([] deps): uses only refs and the stable state setter. + const revalidateTouchedFields: () => void = useCallback((): void => { + setFormErrors((prevErrors: Record) => { + if (Object.keys(prevErrors).length === 0) return prevErrors; + + const newErrors: Record = {...prevErrors}; + let changed = false; + + (Object.keys(prevErrors) as Array).forEach((name: keyof T) => { + // Skip errors that were not produced by client-side validation - preserve server messages. + if (!clientErrorFieldRef.current.has(name)) return; + + const freshError: string | null = validateFieldRef.current(name); + if (freshError === prevErrors[name]) return; + + changed = true; + if (freshError) { + newErrors[name] = freshError; + } else { + delete newErrors[name]; + clientErrorFieldRef.current.delete(name); + } + }); + + return changed ? newErrors : prevErrors; + }); + }, []); + // Validate the entire form const validateForm: () => ValidationResult = useCallback((): ValidationResult => { const newErrors: Record = {} as Record; @@ -307,8 +349,10 @@ export const useForm = >(config: UseFormConfig< const newErrors: Record = {...prev}; if (error) { newErrors[name] = error; + clientErrorFieldRef.current.add(name); } else { delete newErrors[name]; + clientErrorFieldRef.current.delete(name); } return newErrors; }); @@ -339,8 +383,10 @@ export const useForm = >(config: UseFormConfig< const newErrors: Record = {...prev}; if (error) { newErrors[name] = error; + clientErrorFieldRef.current.add(name); } else { delete newErrors[name]; + clientErrorFieldRef.current.delete(name); } return newErrors; }); @@ -375,6 +421,7 @@ export const useForm = >(config: UseFormConfig< // Validate all fields const validation: ValidationResult = validateForm(); setFormErrors(validation.errors as Record); + clientErrorFieldRef.current = new Set(Object.keys(validation.errors) as Array); }, [fields, validateForm]); // Set a field error @@ -399,6 +446,7 @@ export const useForm = >(config: UseFormConfig< // Clear all errors const clearErrors: () => void = useCallback((): void => { setFormErrors({} as Record); + clientErrorFieldRef.current.clear(); }, []); // Reset form to initial state @@ -407,6 +455,7 @@ export const useForm = >(config: UseFormConfig< setFormTouched({} as Record); setFormErrors({} as Record); setIsSubmitted(false); + clientErrorFieldRef.current.clear(); }, [initialValues]); // Handle form submission @@ -455,6 +504,7 @@ export const useForm = >(config: UseFormConfig< isSubmitted, isValid, reset, + revalidateTouchedFields, setError, setErrors, setTouched,