Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 29 additions & 28 deletions packages/docs/content/error-customization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -291,60 +291,60 @@ const result = schema.safeParse(12, {

## Internationalization

To support internationalization of error message, Zod provides several built-in **locales**. These are exported from the `zod/v4/core` package.
To support internationalization of error messages, Zod provides several built-in **locales**.

> **Note** — The regular `zod` library automatically loads the `en` locale automatically. Zod Mini does not load any locale by default; instead all error messages default to `Invalid input`.
> **Note** — Zod v4 automatically configures the `en` (English) locale by default. Zod Mini does not load any locale by default; instead all error messages default to `Invalid input`.

### Using a different locale

Import locales as named exports from `zod/v4/locales` (fully tree-shakeable):

<Tabs groupId="lib" items={["Zod", "Zod Mini"]}>
<Tab value="Zod">
```ts
import * as z from "zod";
import { en } from "zod/locales"
import * as z from "zod/v4";
import { de, fr } from "zod/v4/locales";

z.config(en());
z.config(de()); // Switch to German
```
</Tab>
<Tab value="Zod Mini">
```ts
import * as z from "zod/mini"
import { en } from "zod/locales";
import * as z from "zod/mini";
import { en } from "zod/v4/locales";

z.config(en());
z.config(en()); // English must be configured manually in Mini
```
</Tab>
</Tabs>

To lazily load a locale, consider dynamic imports:
### Lazy-loading locales

For optimal bundle size, you can dynamically import locales:

```ts
import * as z from "zod";
import * as z from "zod/v4";

async function loadLocale(locale: string) {
const { default: locale } = await import(`zod/v4/locales/${locale}.js`);
z.config(locale());
};
const { default: localeFn } = await import(`zod/v4/locales/${locale}.js`);
z.config(localeFn());
}

await loadLocale("fr");
await loadLocale("fr"); // Load French dynamically
```

For convenience, all locales are exported as `z.locales` from `"zod"`. In some bundlers, this may not be tree-shakable.
### Tree-shaking

<Tabs groupId="lib" items={["Zod", "Zod Mini"]}>
<Tab value="Zod">
```ts
import * as z from "zod";
Locales are tree-shakeable - only imported locales are bundled:

z.config(z.locales.en());
```
</Tab>
<Tab value="Zod Mini">
```ts
import * as z from "zod/mini"
import * as z from "zod/v4";
import { de, fr, ja } from "zod/v4/locales";

z.config(z.locales.en());
// Default bundle: ~24KB (core + English auto-configured)
// With 3 extra locales: ~36KB (core + English + de + fr + ja)
// Before v4.2: ~220KB (core + all 47 locales)
```
</Tab>
</Tabs>

### Locales

Expand Down Expand Up @@ -426,4 +426,5 @@ Below is a quick reference for determining error precedence: if multiple error c
4. **Locale error map** — A custom error map passed into `z.config()`.

```ts
z.config(z.locales.en());
import { en } from "zod/v4/locales";
z.config(en());
11 changes: 6 additions & 5 deletions packages/docs/content/packages/mini.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -226,14 +226,15 @@ mySchema.clone(mySchema._zod.def);

## No default locale

While regular Zod automatically loads the English (`en`) locale, Zod Mini does not. This reduces the bundle size in scenarios where error messages are unnecessary, localized to a non-English language, or otherwise customized.
While regular Zod automatically configures the English (`en`) locale, Zod Mini does not. This reduces the bundle size in scenarios where error messages are unnecessary, localized to a non-English language, or otherwise customized.

This means, by default the `message` property of all issues will simply read `"Invalid input"`. To load the English locale:
This means, by default the `message` property of all issues will simply read `"Invalid input"`. To configure the English locale:

```ts
import * as z from "zod/mini"
import * as z from "zod/mini";
import { en } from "zod/v4/locales";

z.config(z.locales.en());
z.config(en());
```

Refer to the [Locales](/error-customization#internationalization) docs for more on localization.
Refer to the [Internationalization](/error-customization#internationalization) docs for more on localization.
12 changes: 8 additions & 4 deletions packages/docs/content/v4/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -589,15 +589,19 @@ fileSchema.mime(["image/png"]); // MIME type

## Internationalization

Zod 4 introduces a new `locales` API for globally translating error messages into different languages.
Zod 4 provides built-in support for translating error messages into 47+ languages with automatic tree-shaking.

```ts
import * as z from "zod";
import * as z from "zod/v4";
import { de, fr } from "zod/v4/locales"; // Tree-shakeable named imports

// configure English locale (default)
z.config(z.locales.en());
// English is auto-configured by default
// Switch to a different locale:
z.config(de());
```

**Bundle impact:** Default English adds ~4KB. Each additional locale adds ~4KB. Only locales you import are bundled.

See the full list of supported locales in [Customizing errors](/error-customization#locales); this section is always updated with a list of supported languages as they become available.

## Error pretty-printing
Expand Down
3 changes: 3 additions & 0 deletions packages/treeshake/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
out.js
dist/
test-*.js
test-*.js.map
3 changes: 2 additions & 1 deletion packages/treeshake/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@
"scripts": {
"bundle": "rollup -c rollup.config.js --input",
"bundle:rollup": "rollup --input",
"bundle:esbuild": "esbuild --bundle ./in.ts --conditions=@zod/source --outfile=./out.js --bundle --format=esm"
"bundle:esbuild": "esbuild --bundle ./in.ts --conditions=@zod/source --outfile=./out.js --bundle --format=esm",
"verify:sizes": "tsx verify-sizes.ts"
},
"devDependencies": {
"@rollup/plugin-commonjs": "^28.0.3",
Expand Down
16 changes: 16 additions & 0 deletions packages/treeshake/test-five-used-locales.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Test: Core + English + 5 additional locales that are ACTUALLY USED
import * as z from "zod/v4";
import { de, fr, ja, es, it } from "zod/v4/locales";

// Store all locale functions so bundler can't strip them
const locales = [de, fr, ja, es, it];

// Use a random locale to prevent dead code elimination
const randomLocale = locales[Math.floor(Math.random() * locales.length)];
z.config(randomLocale());

const schema = z.string().min(5).email();
console.log(schema.parse("test@example.com"));

// Export locales to ensure they're kept in bundle
export { locales };
8 changes: 8 additions & 0 deletions packages/treeshake/test-many-locales.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Test: Core + English + 10 additional locales
import * as z from "zod/v4";
import { de, fr, ja, es, ru, zhCN, pt, it, pl, ko } from "zod/v4/locales";

z.config(de());

const schema = z.string().min(5).email();
console.log(schema.parse("test@example.com"));
5 changes: 5 additions & 0 deletions packages/treeshake/test-no-locales.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
// Test: Default import only (core + English auto-configured)
import * as z from "zod/v4";

const schema = z.string().min(5).email();
console.log(schema.parse("test@example.com"));
8 changes: 8 additions & 0 deletions packages/treeshake/test-one-locale.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
// Test: Core + English + 1 additional locale (German)
import * as z from "zod/v4";
import { de } from "zod/v4/locales";

z.config(de());

const schema = z.string().min(5).email();
console.log(schema.parse("test@example.com"));
9 changes: 9 additions & 0 deletions packages/treeshake/test-three-locales.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// Test: Core + English + 3 additional locales
import * as z from "zod/v4";
import { de, fr, ja } from "zod/v4/locales";

// Can switch between locales
z.config(de());

const schema = z.string().min(5).email();
console.log(schema.parse("test@example.com"));
74 changes: 74 additions & 0 deletions packages/treeshake/test-v4-large-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Test: Zod v4 with large schema (English locale auto-configured)
import * as z from "zod/v4";

// Large schema to exercise the library
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email().min(5).max(100),
username: z.string().min(3).max(30).regex(/^[a-zA-Z0-9_]+$/),
age: z.number().int().min(0).max(150).optional(),
role: z.enum(["admin", "user", "moderator"]),
profile: z.object({
firstName: z.string().min(1).max(50),
lastName: z.string().min(1).max(50),
bio: z.string().max(500).optional(),
avatar: z.string().url().optional(),
birthDate: z.date(),
address: z.object({
street: z.string(),
city: z.string(),
state: z.string().length(2),
zip: z.string().regex(/^\d{5}(-\d{4})?$/),
country: z.string().default("US"),
}).optional(),
}),
settings: z.object({
notifications: z.boolean().default(true),
theme: z.enum(["light", "dark", "auto"]).default("auto"),
language: z.string().default("en"),
timezone: z.string(),
}),
metadata: z.record(z.string(), z.unknown()).optional(),
tags: z.array(z.string()).min(0).max(10),
createdAt: z.date(),
updatedAt: z.date(),
});

const CompanySchema = z.object({
name: z.string().min(1).max(100),
employees: z.array(UserSchema).min(1).max(1000),
revenue: z.number().positive(),
founded: z.date(),
});

// Use the schemas
const user = {
id: "123e4567-e89b-12d3-a456-426614174000",
email: "test@example.com",
username: "testuser",
age: 25,
role: "user" as const,
profile: {
firstName: "John",
lastName: "Doe",
birthDate: new Date("1999-01-01"),
address: {
street: "123 Main St",
city: "San Francisco",
state: "CA",
zip: "94102",
country: "US",
},
},
settings: {
notifications: true,
theme: "dark" as const,
language: "en",
timezone: "America/Los_Angeles",
},
tags: ["developer", "typescript"],
createdAt: new Date(),
updatedAt: new Date(),
};

console.log(UserSchema.parse(user));
95 changes: 95 additions & 0 deletions packages/treeshake/test-v4mini-large-schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
// Test: Zod Mini with large schema (+ English locale for fair comparison)
import * as z from "zod/mini";
import { en } from "zod/v4/locales";

// Configure English (same as V4 auto-does)
z.config(en());

// Large schema to exercise the library (using Mini's limited API)
const AddressSchema = z.object({
street: z.string(),
city: z.string(),
state: z.string(),
zip: z.string(),
country: z.string(),
});

const ProfileSchema = z.object({
firstName: z.string(),
lastName: z.string(),
bio: z.string(),
avatar: z.string(),
birthDate: z.date(),
address: AddressSchema,
});

const SettingsSchema = z.object({
notifications: z.boolean(),
theme: z.enum(["light", "dark", "auto"]),
language: z.string(),
timezone: z.string(),
});

const UserSchema = z.object({
id: z.string(),
email: z.string(),
username: z.string(),
age: z.number(),
role: z.enum(["admin", "user", "moderator"]),
profile: ProfileSchema,
settings: SettingsSchema,
metadata: z.record(z.string(), z.unknown()),
tags: z.array(z.string()),
createdAt: z.date(),
updatedAt: z.date(),
});

const CompanySchema = z.object({
name: z.string(),
employees: z.array(UserSchema),
revenue: z.number(),
founded: z.date(),
});

// Use the schemas
const user = {
id: "123e4567-e89b-12d3-a456-426614174000",
email: "test@example.com",
username: "testuser",
age: 25,
role: "user" as const,
profile: {
firstName: "John",
lastName: "Doe",
bio: "Software developer",
avatar: "https://example.com/avatar.jpg",
birthDate: new Date("1999-01-01"),
address: {
street: "123 Main St",
city: "San Francisco",
state: "CA",
zip: "94102",
country: "US",
},
},
settings: {
notifications: true,
theme: "dark" as const,
language: "en",
timezone: "America/Los_Angeles",
},
metadata: { source: "signup", referrer: "google" },
tags: ["developer", "typescript"],
createdAt: new Date(),
updatedAt: new Date(),
};

const company = {
name: "Acme Corp",
employees: [user],
revenue: 1000000,
founded: new Date("2020-01-01"),
};

console.log(UserSchema.parse(user));
console.log(CompanySchema.parse(company));
Loading