Files
gruperly/.agents/skills/zod/references/error-i18n.md
2026-09-04 16:49:24 -03:00

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

Reference: Zod Error Customization - Internationalization