├── .eslintrc.json
├── next.config.mjs
├── global.d.ts
├── messages
├── en.json
├── de.json
└── zod
│ ├── de.json
│ └── en.json
├── app
├── [locale]
│ ├── page.tsx
│ └── layout.tsx
└── globals.css
├── middleware.ts
├── lib
├── useI18nZodErrors.ts
└── zodErrorMap.ts
├── README.md
├── .gitignore
├── i18n.ts
├── tsconfig.json
├── package.json
└── components
└── ExampleForm.tsx
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | import createNextIntlPlugin from 'next-intl/plugin';
2 |
3 | const withNextIntl = createNextIntlPlugin();
4 |
5 | /** @type {import('next').NextConfig} */
6 | const nextConfig = {};
7 |
8 | export default withNextIntl(nextConfig);
--------------------------------------------------------------------------------
/global.d.ts:
--------------------------------------------------------------------------------
1 | // Use type safe message keys with `next-intl`
2 | type GlobalMessages = typeof import('./messages/en.json');
3 | type ZodMessages = typeof import('./messages/zod/en.json');
4 | type Messages = GlobalMessages & ZodMessages;
5 | declare interface IntlMessages extends Messages {}
--------------------------------------------------------------------------------
/messages/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "nav": {
3 | "english": "English",
4 | "german": "German"
5 | },
6 | "home": {
7 | "title": "next-intl zod example"
8 | },
9 | "form": {
10 | "username": "Username",
11 | "age": "Age"
12 | },
13 | "customErrors": {
14 | "admin_username_error": "Nice try!"
15 | }
16 | }
--------------------------------------------------------------------------------
/messages/de.json:
--------------------------------------------------------------------------------
1 | {
2 | "nav": {
3 | "english": "Englisch",
4 | "german": "Deutsch"
5 | },
6 | "home": {
7 | "title": "next-intl zod Beispiel"
8 | },
9 | "form": {
10 | "usernamex": "Nutzername",
11 | "age": "Alter"
12 | },
13 | "customErrors": {
14 | "admin_username_error": "Netter Versuch!"
15 | }
16 | }
--------------------------------------------------------------------------------
/app/[locale]/page.tsx:
--------------------------------------------------------------------------------
1 | import { ExampleForm } from '@/components/ExampleForm';
2 | import {useTranslations} from 'next-intl';
3 | import '../globals.css';
4 |
5 | export default function Index() {
6 | const t = useTranslations('home');
7 | return (
8 | <>
9 |
{t('title')}
10 |
11 | >
12 | );
13 | }
--------------------------------------------------------------------------------
/middleware.ts:
--------------------------------------------------------------------------------
1 | import createMiddleware from 'next-intl/middleware';
2 |
3 | export default createMiddleware({
4 | // A list of all locales that are supported
5 | locales: ['en', 'de'],
6 |
7 | // Used when no locale matches
8 | defaultLocale: 'en'
9 | });
10 |
11 | export const config = {
12 | // Match only internationalized pathnames
13 | matcher: ['/', '/(de|en)/:path*']
14 | };
--------------------------------------------------------------------------------
/app/globals.css:
--------------------------------------------------------------------------------
1 | main {
2 | display: flex;
3 | justify-content: center;
4 | align-items: center;
5 | flex-direction: column;
6 | }
7 |
8 | form {
9 | display: flex;
10 | flex-direction: column;
11 | align-items: center;
12 | justify-content: center;
13 | gap: 1rem;
14 | }
15 |
16 | nav {
17 | display: flex;
18 | align-items: center;
19 | padding: 1rem;
20 | gap: 1rem;
21 | }
--------------------------------------------------------------------------------
/lib/useI18nZodErrors.ts:
--------------------------------------------------------------------------------
1 | import { useTranslations } from 'next-intl';
2 | import { z } from 'zod';
3 | import { makeZodI18nMap } from './zodErrorMap';
4 |
5 | export const useI18nZodErrors = () => {
6 | const t = useTranslations('zod');
7 | const tForm = useTranslations('form');
8 | const tCustom = useTranslations('customErrors');
9 | z.setErrorMap(makeZodI18nMap({ t, tForm, tCustom }));
10 | };
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Translating zod errors with next-intl
2 |
3 | This is code is a demonstration of how to translate zod errors with next-intl using a zod error map. For more information see this [blog post](https://www.gcasc.io/blog/next-intl-zod).
4 |
5 | ## Getting Started
6 |
7 | First, run the development server:
8 |
9 | ```bash
10 | npm run dev
11 | # or
12 | yarn dev
13 | # or
14 | pnpm dev
15 | # or
16 | bun dev
17 | ```
18 |
19 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/i18n.ts:
--------------------------------------------------------------------------------
1 | import {notFound} from 'next/navigation';
2 | import {getRequestConfig} from 'next-intl/server';
3 |
4 | // Can be imported from a shared config
5 | const locales = ['en', 'de'];
6 |
7 | export default getRequestConfig(async ({locale}) => {
8 | // Validate that the incoming `locale` parameter is valid
9 | if (!locales.includes(locale as any)) notFound();
10 |
11 | return {
12 | messages: {
13 | ...(await import(`./messages/${locale}.json`)).default,
14 | ...(await import(`./messages/zod/${locale}.json`)).default
15 | }
16 | };
17 | });
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-intl-zod",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "@hookform/resolvers": "^3.3.4",
13 | "next": "14.1.1",
14 | "next-intl": "^3.9.1",
15 | "react": "^18",
16 | "react-dom": "^18",
17 | "react-hook-form": "^7.51.0",
18 | "zod": "^3.22.4"
19 | },
20 | "devDependencies": {
21 | "@types/node": "^20",
22 | "@types/react": "^18",
23 | "@types/react-dom": "^18",
24 | "eslint": "^8",
25 | "eslint-config-next": "14.1.1",
26 | "typescript": "^5"
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/[locale]/layout.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslations } from "next-intl";
2 | import { NextIntlClientProvider, useMessages } from 'next-intl';
3 |
4 | export default function LocaleLayout({
5 | children,
6 | params: {locale}
7 | }: {
8 | children: React.ReactNode;
9 | params: {locale: string};
10 | }) {
11 | const t = useTranslations('nav');
12 | const messages = useMessages();
13 |
14 | return (
15 |
16 |
17 |
23 |
24 |
28 | {children}
29 |
30 |
31 |
32 |
33 | );
34 | }
--------------------------------------------------------------------------------
/components/ExampleForm.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useForm } from 'react-hook-form';
4 | import { zodResolver } from '@hookform/resolvers/zod';
5 | import * as z from 'zod';
6 | import { useI18nZodErrors } from '@/lib/useI18nZodErrors';
7 |
8 | const schema = z.object({
9 | username: z
10 | .string()
11 | .min(3)
12 | .refine((value) => value !== 'admin', { params: { i18n: "admin_username_error" } }),
13 | age: z.number().min(18),
14 | });
15 |
16 | type Schema = z.infer;
17 |
18 | export const ExampleForm = () => {
19 | useI18nZodErrors();
20 | const {
21 | register,
22 | handleSubmit,
23 | formState: { errors },
24 | } = useForm({
25 | resolver: zodResolver(schema),
26 | defaultValues: {
27 | username: '',
28 | age: 0,
29 | }
30 | });
31 |
32 | return (
33 |
40 | );
41 | };
--------------------------------------------------------------------------------
/messages/zod/de.json:
--------------------------------------------------------------------------------
1 | {
2 | "zod": {
3 | "errors": {
4 | "invalid_type": "{expected} erwartet, {received} erhalten",
5 | "invalid_type_received_undefined": "Darf nicht leer sein",
6 | "invalid_literal": "Ungültiger Literalwert, {expected} erwartet",
7 | "unrecognized_keys": "Unbekannte Schlüssel im Objekt: {keys}",
8 | "invalid_union": "Ungültige Eingabe",
9 | "invalid_union_discriminator": "Ungültiger Diskriminatorwert, {options} erwartet",
10 | "invalid_enum_value": "Ungültiger Enum-Wert. {options} erwartet, {received} erhalten",
11 | "invalid_arguments": "Ungültige Funktionsargumente",
12 | "invalid_return_type": "Ungültiger Funktionsrückgabewert",
13 | "invalid_date": "Ungültiges Datum",
14 | "custom": "Ungültige Eingabe",
15 | "invalid_intersection_types": "Schnittmengenergebnisse konnten nicht zusammengeführt werden",
16 | "not_multiple_of": "Zahl muss ein Vielfaches von {multipleOf} sein",
17 | "not_finite": "Zahl muss endlich sein",
18 | "invalid_string": {
19 | "email": "Ungültige {validation}",
20 | "emoji": "Ungültige {validation}",
21 | "url": "Ungültige {validation}",
22 | "uuid": "Ungültige {validation}",
23 | "ulid": "Ungültige {validation}",
24 | "ip": "Ungültige {validation}",
25 | "cuid": "Ungültige {validation}",
26 | "cuid2": "Ungültige {validation}",
27 | "regex": "Ungültig",
28 | "datetime": "Ungültiger {validation}",
29 | "startsWith": "Ungültige Eingabe: muss mit \"{startsWith}\" beginnen",
30 | "endsWith": "Ungültige Eingabe: muss mit \"{endsWith}\" enden"
31 | },
32 | "too_small": {
33 | "array": {
34 | "exact": "{path, select, missingTranslation {Array} other {{path}}} muss genau {minimum} Element(e) enthalten",
35 | "inclusive": "{path, select, missingTranslation {Array} other {{path}}} muss mindestens {minimum} Element(e) enthalten",
36 | "not_inclusive": "{path, select, missingTranslation {Array} other {{path}}} muss mehr als {minimum} Element(e) enthalten"
37 | },
38 | "string": {
39 | "exact": "{path, select, missingTranslation {String} other {{path}}} muss genau {minimum} Zeichen enthalten",
40 | "inclusive": "{path, select, missingTranslation {String} other {{path}}} muss mindestens {minimum} Zeichen enthalten",
41 | "not_inclusive": "{path, select, missingTranslation {String} other {{path}}} muss mehr als {minimum} Zeichen enthalten"
42 | },
43 | "number": {
44 | "exact": "{path, select, missingTranslation {Zahl} other {{path}}} muss genau {minimum} sein",
45 | "inclusive": "{path, select, missingTranslation {Zahl} other {{path}}} muss größer oder gleich {minimum} sein",
46 | "not_inclusive": "{path, select, missingTranslation {Zahl} other {{path}}} muss größer als {minimum} sein"
47 | },
48 | "set": {
49 | "exact": "Ungültige Eingabe",
50 | "inclusive": "Ungültige Eingabe",
51 | "not_inclusive": "Ungültige Eingabe"
52 | },
53 | "date": {
54 | "exact": "Datum muss genau {minimum, date, short} sein",
55 | "inclusive": "Datum muss größer oder gleich {minimum, date, short} sein",
56 | "not_inclusive": "Datum muss größer als {minimum, date, short} sein"
57 | }
58 | },
59 | "too_big": {
60 | "array": {
61 | "exact": "{path, select, missingTranslation {Array} other {{path}}} muss genau {maximum} Element(e) enthalten",
62 | "inclusive": "{path, select, missingTranslation {Array} other {{path}}} darf höchstens {maximum} Element(e) enthalten",
63 | "not_inclusive": "{path, select, missingTranslation {Array} other {{path}}} muss weniger als {maximum} Element(e) enthalten"
64 | },
65 | "string": {
66 | "exact": "{path, select, missingTranslation {String} other {{path}}} muss genau {maximum} Zeichen enthalten",
67 | "inclusive": "{path, select, missingTranslation {String} other {{path}}} darf höchstens {maximum} Zeichen enthalten",
68 | "not_inclusive": "{path, select, missingTranslation {String} other {{path}}} muss weniger als {maximum} Zeichen enthalten"
69 | },
70 | "number": {
71 | "exact": "{path, select, missingTranslation {Zahl} other {{path}}} muss genau {maximum} sein",
72 | "inclusive": "{path, select, missingTranslation {Zahl} other {{path}}} muss kleiner oder gleich {maximum} sein",
73 | "not_inclusive": "{path, select, missingTranslation {Zahl} other {{path}}} muss kleiner als {maximum} sein"
74 | },
75 | "set": {
76 | "exact": "Ungültige Eingabe",
77 | "inclusive": "Ungültige Eingabe",
78 | "not_inclusive": "Ungültige Eingabe"
79 | },
80 | "date": {
81 | "exact": "Datum muss genau {maximum, date, short} sein",
82 | "inclusive": "Datum muss kleiner oder gleich {maximum, date, short} sein",
83 | "not_inclusive": "Datum muss kleiner als {maximum, date, short} sein"
84 | }
85 | }
86 | },
87 | "validations": {
88 | "email": "E-Mail-Adresse",
89 | "emoji": "Emoji",
90 | "url": "URL",
91 | "uuid": "UUID",
92 | "ulid": "ULID",
93 | "ip": "IP",
94 | "cuid": "CUID",
95 | "cuid2": "CUID2",
96 | "regex": "Regex",
97 | "datetime": "Datums- und Uhrzeitwert"
98 | },
99 | "types": {
100 | "function": "Funktion",
101 | "number": "Zahl",
102 | "string": "String",
103 | "nan": "NaN",
104 | "integer": "Ganzzahl",
105 | "float": "Gleitkommazahl",
106 | "boolean": "Boolean",
107 | "date": "Datum",
108 | "bigint": "Bigint",
109 | "undefined": "Undefined",
110 | "symbol": "Symbol",
111 | "null": "Nullwert",
112 | "array": "Array",
113 | "object": "Objekt",
114 | "unknown": "Unknown",
115 | "promise": "Promise",
116 | "void": "Void",
117 | "never": "Never",
118 | "map": "Map",
119 | "set": "Set"
120 | }
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/messages/zod/en.json:
--------------------------------------------------------------------------------
1 | {
2 | "zod": {
3 | "errors": {
4 | "invalid_type": "Expected {expected}, received {received}",
5 | "invalid_type_received_undefined": "Required",
6 | "invalid_literal": "Invalid literal value, expected {expected}",
7 | "unrecognized_keys": "Unrecognized key(s) in object: {keys}",
8 | "invalid_union": "Invalid input",
9 | "invalid_union_discriminator": "Invalid discriminator value. Expected {options}",
10 | "invalid_enum_value": "Invalid enum value. Expected {options}, received {received}",
11 | "invalid_arguments": "Invalid function arguments",
12 | "invalid_return_type": "Invalid function return type",
13 | "invalid_date": "Invalid date",
14 | "custom": "Invalid input",
15 | "invalid_intersection_types": "Intersection results could not be merged",
16 | "not_multiple_of": "Number must be a multiple of {multipleOf}",
17 | "not_finite": "Number must be finite",
18 | "invalid_string": {
19 | "email": "Invalid {validation}",
20 | "emoji": "Invalid {validation}",
21 | "url": "Invalid {validation}",
22 | "uuid": "Invalid {validation}",
23 | "ulid": "Invalid {validation}",
24 | "ip": "Invalid {validation}",
25 | "cuid": "Invalid {validation}",
26 | "cuid2": "Invalid {validation}",
27 | "regex": "Invalid",
28 | "datetime": "Invalid {validation}",
29 | "startsWith": "Invalid input: must start with \"{startsWith}\"",
30 | "endsWith": "Invalid input: must end with \"{endsWith}\""
31 | },
32 | "too_small": {
33 | "array": {
34 | "exact": "{path, select, missingTranslation {Array} other {{path}}} must contain exactly {minimum} element(s)",
35 | "inclusive": "{path, select, missingTranslation {Array} other {{path}}} must contain at least {minimum} element(s)",
36 | "not_inclusive": "{path, select, missingTranslation {Array} other {{path}}} must contain more than {minimum} element(s)"
37 | },
38 | "string": {
39 | "exact": "{path, select, missingTranslation {String} other {{path}}} must contain exactly {minimum} character(s)",
40 | "inclusive": "{path, select, missingTranslation {String} other {{path}}} must contain at least {minimum} character(s)",
41 | "not_inclusive": "{path, select, missingTranslation {String} other {{path}}} must contain over {minimum} character(s)"
42 | },
43 | "number": {
44 | "exact": "{path, select, missingTranslation {Number} other {{path}}} must be exactly {minimum}",
45 | "inclusive": "{path, select, missingTranslation {Number} other {{path}}} must be greater than or equal to {minimum}",
46 | "not_inclusive": "{path, select, missingTranslation {Number} other {{path}}} must be greater than {minimum}"
47 | },
48 | "bigint": {
49 | "exact": "{path, select, missingTranslation {Number} other {{path}}} must be exactly {minimum}",
50 | "inclusive": "{path, select, missingTranslation {Number} other {{path}}} must be greater than or equal to {minimum}",
51 | "not_inclusive": "{path, select, missingTranslation {Number} other {{path}}} must be greater than {minimum}"
52 | },
53 | "set": {
54 | "exact": "Invalid input",
55 | "inclusive": "Invalid input",
56 | "not_inclusive": "Invalid input"
57 | },
58 | "date": {
59 | "exact": "Date must be exactly {minimum, date, short}",
60 | "inclusive": "Date must be greater than or equal to {minimum, date, short}",
61 | "not_inclusive": "Date must be greater than {minimum, date, short}"
62 | }
63 | },
64 | "too_big": {
65 | "array": {
66 | "exact": "{path, select, missingTranslation {Array} other {{path}}} must contain exactly {maximum} element(s)",
67 | "inclusive": "{path, select, missingTranslation {Array} other {{path}}} must contain at most {maximum} element(s)",
68 | "not_inclusive": "{path, select, missingTranslation {Array} other {{path}}} must contain less than {maximum} element(s)"
69 | },
70 | "string": {
71 | "exact": "{path, select, missingTranslation {String} other {{path}}} must contain exactly {maximum} character(s)",
72 | "inclusive": "{path, select, missingTranslation {String} other {{path}}} must contain at most {maximum} character(s)",
73 | "not_inclusive": "{path, select, missingTranslation {String} other {{path}}} must contain under {maximum} character(s)"
74 | },
75 | "number": {
76 | "exact": "{path, select, missingTranslation {Number} other {{path}}} must be exactly {maximum}",
77 | "inclusive": "{path, select, missingTranslation {Number} other {{path}}} must be less than or equal to {maximum}",
78 | "not_inclusive": "{path, select, missingTranslation {Number} other {{path}}} must be less than {maximum}"
79 | },
80 | "bigint": {
81 | "exact": "{path, select, missingTranslation {Number} other {{path}}} must be exactly {maximum}",
82 | "inclusive": "{path, select, missingTranslation {Number} other {{path}}} must be less than or equal to {maximum}",
83 | "not_inclusive": "{path, select, missingTranslation {Number} other {{path}}} must be less than {maximum}"
84 | },
85 | "set": {
86 | "exact": "Invalid input",
87 | "inclusive": "Invalid input",
88 | "not_inclusive": "Invalid input"
89 | },
90 | "date": {
91 | "exact": "Date must be exactly {maximum, date, short}",
92 | "inclusive": "Date must be smaller than or equal to {maximum, date, short}",
93 | "not_inclusive": "Date must be smaller than {maximum, date, short}"
94 | }
95 | }
96 | },
97 | "validations": {
98 | "email": "email",
99 | "emoji": "emoji",
100 | "url": "url",
101 | "uuid": "uuid",
102 | "ulid": "ulid",
103 | "ip": "up",
104 | "cuid": "cuid",
105 | "cuid2": "cuid2",
106 | "regex": "regex",
107 | "datetime": "datetime"
108 | },
109 | "types": {
110 | "function": "function",
111 | "number": "number",
112 | "string": "string",
113 | "nan": "nan",
114 | "integer": "integer",
115 | "float": "float",
116 | "boolean": "boolean",
117 | "date": "date",
118 | "bigint": "bigint",
119 | "undefined": "undefined",
120 | "symbol": "symbol",
121 | "null": "null",
122 | "array": "array",
123 | "object": "object",
124 | "unknown": "unknown",
125 | "promise": "promise",
126 | "void": "void",
127 | "never": "never",
128 | "map": "map",
129 | "set": "set"
130 | }
131 | }
132 | }
133 |
--------------------------------------------------------------------------------
/lib/zodErrorMap.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * This error map is a modified version of the on used by zod-i18n
3 | * Checkout the original at: https://github.com/aiji42/zod-i18n
4 | */
5 |
6 | import {
7 | ZodIssueCode, ZodParsedType, defaultErrorMap, ZodErrorMap,
8 | } from 'zod';
9 | import { useTranslations } from 'next-intl';
10 |
11 | const jsonStringifyReplacer = (_: string, value: unknown): unknown => {
12 | if (typeof value === 'bigint') {
13 | return value.toString();
14 | }
15 | return value;
16 | };
17 |
18 | function joinValues(array: T, separator = ' | '): string {
19 | return array
20 | .map((val) => (typeof val === 'string' ? `'${val}'` : val))
21 | .join(separator);
22 | }
23 |
24 | const isRecord = (value: unknown): value is Record => {
25 | if (typeof value !== 'object' || value === null) return false;
26 |
27 | // eslint-disable-next-line no-restricted-syntax
28 | for (const key in value) {
29 | if (!Object.prototype.hasOwnProperty.call(value, key)) return false;
30 | }
31 |
32 | return true;
33 | };
34 |
35 | const getKeyAndValues = (
36 | param: unknown,
37 | defaultKey: string,
38 | ): {
39 | values: Record;
40 | key: string;
41 | } => {
42 | if (typeof param === 'string') return { key: param, values: {} };
43 |
44 | if (isRecord(param)) {
45 | const key =
46 | 'key' in param && typeof param.key === 'string' ? param.key : defaultKey;
47 | const values =
48 | 'values' in param && isRecord(param.values) ? param.values : {};
49 | return { key, values };
50 | }
51 |
52 | return { key: defaultKey, values: {} };
53 | };
54 |
55 | type ZodI18nMapOption = {
56 | t: ReturnType;
57 | tForm?: ReturnType;
58 | tCustom?: ReturnType;
59 | ns?: string | readonly string[];
60 | };
61 |
62 | type MakeZodI18nMap = (option: ZodI18nMapOption) => ZodErrorMap;
63 |
64 | export const makeZodI18nMap: MakeZodI18nMap = (option) => (issue, ctx) => {
65 | const { t, tForm, tCustom } = {
66 | ...option,
67 | };
68 |
69 | let message: string;
70 | message = defaultErrorMap(issue, ctx).message;
71 |
72 | const path = issue.path.length > 0 && !!tForm
73 | ? { path: tForm(issue.path.join('.') as any) }
74 | : {};
75 |
76 | switch (issue.code) {
77 | case ZodIssueCode.invalid_type:
78 | if (issue.received === ZodParsedType.undefined) {
79 | message = t('errors.invalid_type_received_undefined', {
80 | ...path,
81 | });
82 | } else {
83 | message = t('errors.invalid_type', {
84 | expected: t(`types.${issue.expected}`),
85 | received: t(`types.${issue.received}`),
86 | ...path,
87 | });
88 | }
89 | break;
90 | case ZodIssueCode.invalid_literal:
91 | message = t('errors.invalid_literal', {
92 | expected: JSON.stringify(issue.expected, jsonStringifyReplacer),
93 | ...path,
94 | });
95 | break;
96 | case ZodIssueCode.unrecognized_keys:
97 | message = t('errors.unrecognized_keys', {
98 | keys: joinValues(issue.keys, ', '),
99 | count: issue.keys.length,
100 | ...path,
101 | });
102 | break;
103 | case ZodIssueCode.invalid_union:
104 | message = t('errors.invalid_union', {
105 | ...path,
106 | });
107 | break;
108 | case ZodIssueCode.invalid_union_discriminator:
109 | message = t('errors.invalid_union_discriminator', {
110 | options: joinValues(issue.options),
111 | ...path,
112 | });
113 | break;
114 | case ZodIssueCode.invalid_enum_value:
115 | message = t('errors.invalid_enum_value', {
116 | options: joinValues(issue.options),
117 | received: issue.received,
118 | ...path,
119 | });
120 | break;
121 | case ZodIssueCode.invalid_arguments:
122 | message = t('errors.invalid_arguments', {
123 | ...path,
124 | });
125 | break;
126 | case ZodIssueCode.invalid_return_type:
127 | message = t('errors.invalid_return_type', {
128 | ...path,
129 | });
130 | break;
131 | case ZodIssueCode.invalid_date:
132 | message = t('errors.invalid_date', {
133 | ...path,
134 | });
135 | break;
136 | case ZodIssueCode.invalid_string:
137 | if (typeof issue.validation === 'object') {
138 | if ('startsWith' in issue.validation) {
139 | message = t('errors.invalid_string.startsWith', {
140 | startsWith: issue.validation.startsWith,
141 | ...path,
142 | });
143 | } else if ('endsWith' in issue.validation) {
144 | message = t('errors.invalid_string.endsWith', {
145 | endsWith: issue.validation.endsWith,
146 | ...path,
147 | });
148 | }
149 | } else {
150 | message = t(`errors.invalid_string.${issue.validation}`, {
151 | validation: t(`validations.${issue.validation}`),
152 | ...path,
153 | });
154 | }
155 | break;
156 | case ZodIssueCode.too_small: {
157 | const minimum =
158 | issue.type === 'date'
159 | ? new Date(issue.minimum as number)
160 | : issue.minimum as number;
161 | message = t(
162 | `errors.too_small.${issue.type}.${
163 | // eslint-disable-next-line no-nested-ternary
164 | issue.exact
165 | ? 'exact'
166 | : issue.inclusive
167 | ? 'inclusive'
168 | : 'not_inclusive'
169 | }`,
170 | {
171 | minimum,
172 | count: typeof minimum === 'number' ? minimum : undefined,
173 | ...path,
174 | },
175 | );
176 | break;
177 | }
178 | case ZodIssueCode.too_big: {
179 | const maximum =
180 | issue.type === 'date'
181 | ? new Date(issue.maximum as number)
182 | : issue.maximum as number;
183 | message = t(
184 | `errors.too_big.${issue.type}.${
185 | // eslint-disable-next-line no-nested-ternary
186 | issue.exact
187 | ? 'exact'
188 | : issue.inclusive
189 | ? 'inclusive'
190 | : 'not_inclusive'
191 | }`,
192 | {
193 | maximum,
194 | count: typeof maximum === 'number' ? maximum : undefined,
195 | ...path,
196 | },
197 | );
198 | break;
199 | }
200 | case ZodIssueCode.custom: {
201 | const { key, values } = getKeyAndValues(
202 | issue.params?.i18n,
203 | 'errors.custom',
204 | );
205 |
206 | message = (tCustom || t)(key as Parameters[0], {
207 | ...values,
208 | ...path,
209 | });
210 | break;
211 | }
212 | case ZodIssueCode.invalid_intersection_types:
213 | message = t('errors.invalid_intersection_types', {
214 | ...path,
215 | });
216 | break;
217 | case ZodIssueCode.not_multiple_of:
218 | message = t('errors.not_multiple_of', {
219 | multipleOf: issue.multipleOf as number,
220 | ...path,
221 | });
222 | break;
223 | case ZodIssueCode.not_finite:
224 | message = t('errors.not_finite', {
225 | ...path,
226 | });
227 | break;
228 | default:
229 | }
230 |
231 | return { message };
232 | };
233 |
--------------------------------------------------------------------------------