Skip to content

Commit 845a230

Browse files
authored
fix(locales): Add type name translations to Spanish locale (#5187)
- Add TypeNames dictionary mapping English to Spanish type names - Implement getTypeName helper function for translations - Update error messages to use translated type names - Add comprehensive tests for Spanish locale
1 parent 23a2d66 commit 845a230

File tree

2 files changed

+225
-10
lines changed

2 files changed

+225
-10
lines changed
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { expect, test } from "vitest";
2+
import { z } from "../../../../index.js";
3+
import es from "../../../locales/es.js";
4+
5+
test("Spanish locale - type name translations in too_small errors", () => {
6+
z.config(es());
7+
8+
// Test string type translation
9+
const stringSchema = z.string().min(5);
10+
const stringResult = stringSchema.safeParse("abc");
11+
expect(stringResult.success).toBe(false);
12+
if (!stringResult.success) {
13+
expect(stringResult.error.issues[0].message).toBe(
14+
"Demasiado pequeño: se esperaba que texto tuviera >=5 caracteres"
15+
);
16+
}
17+
18+
// Test number type translation
19+
const numberSchema = z.number().min(10);
20+
const numberResult = numberSchema.safeParse(5);
21+
expect(numberResult.success).toBe(false);
22+
if (!numberResult.success) {
23+
expect(numberResult.error.issues[0].message).toBe("Demasiado pequeño: se esperaba que número fuera >=10");
24+
}
25+
26+
// Test array type translation
27+
const arraySchema = z.array(z.string()).min(3);
28+
const arrayResult = arraySchema.safeParse(["a", "b"]);
29+
expect(arrayResult.success).toBe(false);
30+
if (!arrayResult.success) {
31+
expect(arrayResult.error.issues[0].message).toBe(
32+
"Demasiado pequeño: se esperaba que arreglo tuviera >=3 elementos"
33+
);
34+
}
35+
36+
// Test set type translation
37+
const setSchema = z.set(z.string()).min(2);
38+
const setResult = setSchema.safeParse(new Set(["a"]));
39+
expect(setResult.success).toBe(false);
40+
if (!setResult.success) {
41+
expect(setResult.error.issues[0].message).toBe("Demasiado pequeño: se esperaba que conjunto tuviera >=2 elementos");
42+
}
43+
});
44+
45+
test("Spanish locale - type name translations in too_big errors", () => {
46+
z.config(es());
47+
48+
// Test string type translation
49+
const stringSchema = z.string().max(3);
50+
const stringResult = stringSchema.safeParse("abcde");
51+
expect(stringResult.success).toBe(false);
52+
if (!stringResult.success) {
53+
expect(stringResult.error.issues[0].message).toBe("Demasiado grande: se esperaba que texto tuviera <=3 caracteres");
54+
}
55+
56+
// Test number type translation
57+
const numberSchema = z.number().max(10);
58+
const numberResult = numberSchema.safeParse(15);
59+
expect(numberResult.success).toBe(false);
60+
if (!numberResult.success) {
61+
expect(numberResult.error.issues[0].message).toBe("Demasiado grande: se esperaba que número fuera <=10");
62+
}
63+
64+
// Test array type translation
65+
const arraySchema = z.array(z.string()).max(2);
66+
const arrayResult = arraySchema.safeParse(["a", "b", "c"]);
67+
expect(arrayResult.success).toBe(false);
68+
if (!arrayResult.success) {
69+
expect(arrayResult.error.issues[0].message).toBe("Demasiado grande: se esperaba que arreglo tuviera <=2 elementos");
70+
}
71+
});
72+
73+
test("Spanish locale - type name translations in invalid_type errors", () => {
74+
z.config(es());
75+
76+
// Test string expected, number received
77+
const stringSchema = z.string();
78+
const stringResult = stringSchema.safeParse(123);
79+
expect(stringResult.success).toBe(false);
80+
if (!stringResult.success) {
81+
expect(stringResult.error.issues[0].message).toBe("Entrada inválida: se esperaba texto, recibido número");
82+
}
83+
84+
// Test number expected, string received
85+
const numberSchema = z.number();
86+
const numberResult = numberSchema.safeParse("abc");
87+
expect(numberResult.success).toBe(false);
88+
if (!numberResult.success) {
89+
expect(numberResult.error.issues[0].message).toBe("Entrada inválida: se esperaba número, recibido texto");
90+
}
91+
92+
// Test boolean expected, null received
93+
const booleanSchema = z.boolean();
94+
const booleanResult = booleanSchema.safeParse(null);
95+
expect(booleanResult.success).toBe(false);
96+
if (!booleanResult.success) {
97+
expect(booleanResult.error.issues[0].message).toBe("Entrada inválida: se esperaba booleano, recibido nulo");
98+
}
99+
100+
// Test array expected, object received
101+
const arraySchema = z.array(z.string());
102+
const arrayResult = arraySchema.safeParse({});
103+
expect(arrayResult.success).toBe(false);
104+
if (!arrayResult.success) {
105+
expect(arrayResult.error.issues[0].message).toBe("Entrada inválida: se esperaba arreglo, recibido objeto");
106+
}
107+
});
108+
109+
test("Spanish locale - fallback for unknown type names", () => {
110+
z.config(es());
111+
112+
// Test with a type that's not in the TypeNames dictionary
113+
// This will test the fallback behavior
114+
const dateSchema = z.date().min(new Date("2025-01-01"));
115+
const dateResult = dateSchema.safeParse(new Date("2024-01-01"));
116+
expect(dateResult.success).toBe(false);
117+
if (!dateResult.success) {
118+
// Should use "fecha" since we included it in TypeNames
119+
expect(dateResult.error.issues[0].message).toContain("fecha");
120+
}
121+
});
122+
123+
test("Spanish locale - other error cases", () => {
124+
z.config(es());
125+
126+
// Test invalid_element with tuple
127+
const tupleSchema = z.tuple([z.string(), z.number()]);
128+
const tupleResult = tupleSchema.safeParse(["abc", "not a number"]);
129+
expect(tupleResult.success).toBe(false);
130+
if (!tupleResult.success) {
131+
expect(tupleResult.error.issues[0].message).toContain("Entrada inválida");
132+
}
133+
134+
// Test invalid_value with enum
135+
const enumSchema = z.enum(["a", "b"]);
136+
const enumResult = enumSchema.safeParse("c");
137+
expect(enumResult.success).toBe(false);
138+
if (!enumResult.success) {
139+
expect(enumResult.error.issues[0].message).toBe('Opción inválida: se esperaba una de "a"|"b"');
140+
}
141+
142+
// Test not_multiple_of
143+
const multipleSchema = z.number().multipleOf(3);
144+
const multipleResult = multipleSchema.safeParse(10);
145+
expect(multipleResult.success).toBe(false);
146+
if (!multipleResult.success) {
147+
expect(multipleResult.error.issues[0].message).toBe("Número inválido: debe ser múltiplo de 3");
148+
}
149+
150+
// Test unrecognized_keys
151+
const strictSchema = z.object({ a: z.string() }).strict();
152+
const strictResult = strictSchema.safeParse({ a: "test", b: "extra" });
153+
expect(strictResult.success).toBe(false);
154+
if (!strictResult.success) {
155+
expect(strictResult.error.issues[0].message).toBe('Llave desconocida: "b"');
156+
}
157+
158+
// Test invalid_union
159+
const unionSchema = z.union([z.string(), z.number()]);
160+
const unionResult = unionSchema.safeParse(true);
161+
expect(unionResult.success).toBe(false);
162+
if (!unionResult.success) {
163+
expect(unionResult.error.issues[0].message).toBe("Entrada inválida");
164+
}
165+
166+
// Test invalid_format with regex
167+
const regexSchema = z.string().regex(/^[a-z]+$/);
168+
const regexResult = regexSchema.safeParse("ABC123");
169+
expect(regexResult.success).toBe(false);
170+
if (!regexResult.success) {
171+
expect(regexResult.error.issues[0].message).toBe("Cadena inválida: debe coincidir con el patrón /^[a-z]+$/");
172+
}
173+
174+
// Test invalid_format with startsWith
175+
const startsWithSchema = z.string().startsWith("hello");
176+
const startsWithResult = startsWithSchema.safeParse("world");
177+
expect(startsWithResult.success).toBe(false);
178+
if (!startsWithResult.success) {
179+
expect(startsWithResult.error.issues[0].message).toBe('Cadena inválida: debe comenzar con "hello"');
180+
}
181+
});

packages/zod/src/v4/locales/es.ts

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,27 +10,59 @@ const error: () => errors.$ZodErrorMap = () => {
1010
set: { unit: "elementos", verb: "tener" },
1111
};
1212

13+
const TypeNames: Record<string, string> = {
14+
string: "texto",
15+
number: "número",
16+
boolean: "booleano",
17+
array: "arreglo",
18+
object: "objeto",
19+
set: "conjunto",
20+
file: "archivo",
21+
date: "fecha",
22+
bigint: "número grande",
23+
symbol: "símbolo",
24+
undefined: "indefinido",
25+
null: "nulo",
26+
function: "función",
27+
map: "mapa",
28+
record: "registro",
29+
tuple: "tupla",
30+
enum: "enumeración",
31+
union: "unión",
32+
literal: "literal",
33+
promise: "promesa",
34+
void: "vacío",
35+
never: "nunca",
36+
unknown: "desconocido",
37+
any: "cualquiera",
38+
};
39+
1340
function getSizing(origin: string): { unit: string; verb: string } | null {
1441
return Sizable[origin] ?? null;
1542
}
1643

44+
function getTypeName(type: string): string {
45+
return TypeNames[type] ?? type;
46+
}
47+
1748
const parsedType = (data: any): string => {
1849
const t = typeof data;
1950

2051
switch (t) {
2152
case "number": {
22-
return Number.isNaN(data) ? "NaN" : "número";
53+
return Number.isNaN(data) ? "NaN" : "number";
2354
}
2455
case "object": {
2556
if (Array.isArray(data)) {
26-
return "arreglo";
57+
return "array";
2758
}
2859
if (data === null) {
29-
return "nulo";
60+
return "null";
3061
}
3162
if (Object.getPrototypeOf(data) !== Object.prototype) {
3263
return data.constructor.name;
3364
}
65+
return "object";
3466
}
3567
}
3668
return t;
@@ -72,7 +104,7 @@ const error: () => errors.$ZodErrorMap = () => {
72104
return (issue) => {
73105
switch (issue.code) {
74106
case "invalid_type":
75-
return `Entrada inválida: se esperaba ${issue.expected}, recibido ${parsedType(issue.input)}`;
107+
return `Entrada inválida: se esperaba ${getTypeName(issue.expected)}, recibido ${getTypeName(parsedType(issue.input))}`;
76108
// return `Entrada inválida: se esperaba ${issue.expected}, recibido ${util.getParsedType(issue.input)}`;
77109
case "invalid_value":
78110
if (issue.values.length === 1)
@@ -81,18 +113,20 @@ const error: () => errors.$ZodErrorMap = () => {
81113
case "too_big": {
82114
const adj = issue.inclusive ? "<=" : "<";
83115
const sizing = getSizing(issue.origin);
116+
const origin = getTypeName(issue.origin);
84117
if (sizing)
85-
return `Demasiado grande: se esperaba que ${issue.origin ?? "valor"} tuviera ${adj}${issue.maximum.toString()} ${sizing.unit ?? "elementos"}`;
86-
return `Demasiado grande: se esperaba que ${issue.origin ?? "valor"} fuera ${adj}${issue.maximum.toString()}`;
118+
return `Demasiado grande: se esperaba que ${origin ?? "valor"} tuviera ${adj}${issue.maximum.toString()} ${sizing.unit ?? "elementos"}`;
119+
return `Demasiado grande: se esperaba que ${origin ?? "valor"} fuera ${adj}${issue.maximum.toString()}`;
87120
}
88121
case "too_small": {
89122
const adj = issue.inclusive ? ">=" : ">";
90123
const sizing = getSizing(issue.origin);
124+
const origin = getTypeName(issue.origin);
91125
if (sizing) {
92-
return `Demasiado pequeño: se esperaba que ${issue.origin} tuviera ${adj}${issue.minimum.toString()} ${sizing.unit}`;
126+
return `Demasiado pequeño: se esperaba que ${origin} tuviera ${adj}${issue.minimum.toString()} ${sizing.unit}`;
93127
}
94128

95-
return `Demasiado pequeño: se esperaba que ${issue.origin} fuera ${adj}${issue.minimum.toString()}`;
129+
return `Demasiado pequeño: se esperaba que ${origin} fuera ${adj}${issue.minimum.toString()}`;
96130
}
97131
case "invalid_format": {
98132
const _issue = issue as errors.$ZodStringFormatIssues;
@@ -107,11 +141,11 @@ const error: () => errors.$ZodErrorMap = () => {
107141
case "unrecognized_keys":
108142
return `Llave${issue.keys.length > 1 ? "s" : ""} desconocida${issue.keys.length > 1 ? "s" : ""}: ${util.joinValues(issue.keys, ", ")}`;
109143
case "invalid_key":
110-
return `Llave inválida en ${issue.origin}`;
144+
return `Llave inválida en ${getTypeName(issue.origin)}`;
111145
case "invalid_union":
112146
return "Entrada inválida";
113147
case "invalid_element":
114-
return `Valor inválido en ${issue.origin}`;
148+
return `Valor inválido en ${getTypeName(issue.origin)}`;
115149
default:
116150
return `Entrada inválida`;
117151
}

0 commit comments

Comments
 (0)