3.9 KiB
3.9 KiB
title, impact, impactDescription, tags
| title | impact | impactDescription | tags |
|---|---|---|---|
| Implement Internationalized Error Messages | HIGH | Hardcoded English messages exclude non-English users; error maps enable localized messages for global applications | error, i18n, localization, internationalization |
Implement Internationalized Error Messages
Hardcoded error messages in English exclude users who speak other languages. Use Zod's error map feature to provide localized messages based on user locale, making your application accessible globally.
Incorrect (hardcoded English messages):
import { z } from 'zod'
const userSchema = z.object({
name: z.string().min(1, 'Name is required'),
email: z.string().email('Invalid email address'),
age: z.number().min(18, 'You must be at least 18 years old'),
})
// French users see English errors - poor UX
Correct (localized error messages):
import { z } from 'zod'
// Translation dictionaries
const translations = {
en: {
required: 'This field is required',
invalidEmail: 'Please enter a valid email address',
tooShort: (min: number) => `Must be at least ${min} characters`,
tooYoung: (min: number) => `You must be at least ${min} years old`,
},
fr: {
required: 'Ce champ est obligatoire',
invalidEmail: 'Veuillez entrer une adresse email valide',
tooShort: (min: number) => `Doit contenir au moins ${min} caractères`,
tooYoung: (min: number) => `Vous devez avoir au moins ${min} ans`,
},
es: {
required: 'Este campo es requerido',
invalidEmail: 'Por favor ingrese un correo electrónico válido',
tooShort: (min: number) => `Debe tener al menos ${min} caracteres`,
tooYoung: (min: number) => `Debes tener al menos ${min} años`,
},
} as const
type Locale = keyof typeof translations
function createErrorMap(locale: Locale): z.ZodErrorMap {
const t = translations[locale]
return (issue, ctx) => {
switch (issue.code) {
case z.ZodIssueCode.invalid_type:
if (issue.received === 'undefined') {
return { message: t.required }
}
break
case z.ZodIssueCode.invalid_string:
if (issue.validation === 'email') {
return { message: t.invalidEmail }
}
break
case z.ZodIssueCode.too_small:
if (issue.type === 'string') {
return { message: t.tooShort(issue.minimum as number) }
}
if (issue.type === 'number') {
return { message: t.tooYoung(issue.minimum as number) }
}
break
}
return { message: ctx.defaultError }
}
}
// Usage with user's locale
const userLocale: Locale = 'fr'
const errorMap = createErrorMap(userLocale)
const userSchema = z.object({
name: z.string().min(1),
email: z.string().email(),
age: z.number().min(18),
})
const result = userSchema.safeParse(
{ name: '', email: 'bad', age: 15 },
{ errorMap }
)
// French error messages:
// - "Ce champ est obligatoire"
// - "Veuillez entrer une adresse email valide"
// - "Vous devez avoir au moins 18 ans"
Setting error map globally:
// At application startup
const userLocale = getUserLocale() // From cookie, header, etc.
z.setErrorMap(createErrorMap(userLocale))
// All schemas now use localized messages
With i18n libraries (react-intl, i18next):
import { useIntl } from 'react-intl'
function useZodErrorMap() {
const intl = useIntl()
return (issue: z.ZodIssue, ctx: z.ErrorMapCtx) => {
switch (issue.code) {
case z.ZodIssueCode.too_small:
return {
message: intl.formatMessage(
{ id: 'validation.tooShort' },
{ min: issue.minimum }
)
}
// ...
}
return { message: ctx.defaultError }
}
}
When NOT to use this pattern:
- Internal tools used only by your team
- Single-language applications