Skip to content
3 changes: 2 additions & 1 deletion packages/core/src/locales.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import az from "./locales/az.js";
import en from "./locales/en.js";
import es from "./locales/es.js";
import fi from "./locales/fi.js";

export { az, es, en };
export { az, es, en, fi };
197 changes: 197 additions & 0 deletions packages/core/src/locales/fi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
import type { $ZodStringFormats } from "../checks.js";
import type * as errors from "../errors.js";
import type { $ZodTypeDef } from "../schemas.js";
import * as util from "../util.js";

const Sizable: Record<string, { unit: string; subject: string }> = {
string: { unit: "merkkiä", subject: "merkkijonon" },
file: { unit: "tavua", subject: "tiedoston" },
array: { unit: "alkiota", subject: "listan" },
set: { unit: "alkiota", subject: "joukon" },
number: { unit: "", subject: "luvun" },
bigint: { unit: "", subject: "suuren kokonaisluvun" },
int: { unit: "", subject: "kokonaisluvun" },
date: { unit: "", subject: "päivämäärän" },
};

function getSizing(origin: string): { unit: string; subject: string } | null {
return Sizable[origin] ?? null;
}

const TypeNames: { [k in $ZodTypeDef["type"] | (string & {})]?: string } = {
string: "merkkijono",
number: "luku",
boolean: "totuusarvo",
bigint: "suuri kokonaisluku",
symbol: "symboli",
null: "tyhjä",
undefined: "määrittelemätön",
date: "päivämäärä",
object: "objekti",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kind of hate this term, especially if these are displayed to non-technical users.
But I am not entirely sure where to draw the line. Similar to map comment below, I think in this case "tietue" might also work...

file: "tiedosto",
array: "lista",
map: "hakemisto",
Copy link

@Snurppa Snurppa Apr 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMO better would be "tietue"/"tietueessa"

avain hakemistossa

vrt

avain tietueessa

https://fi.wikipedia.org/wiki/Tietue

"hakemisto" ottaa mielestäni liikaa kantaa implementoivaan tietorakenteeseen (Python tulee mieleen).
Voisin myös kuvitella että tavallisille käyttäjille "hakemisto" tuo mieleen tiedostjärjestelmän kansiorakenteen tms. Ehkä... 😸

set: "joukko",
nan: "epäluku",
promise: "lupaus",
};

function getTypeName(type: string): string {
return TypeNames[type] ?? type;
}

export const parsedType = (data: any): string => {
const t = typeof data;

switch (t) {
case "number": {
return Number.isNaN(data) ? "epäluku" : "luku";
}
case "bigint": {
return Number.isNaN(data) ? "epäluku" : "suuri kokonaisluku";
}
case "boolean": {
return "totuusarvo";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe use a pattern like this...?

Suggested change
return "totuusarvo";
return TypeNames["boolean"];

With the current pattern here, if any of these needs terms tweaking later in the future you would need to remember to update same string into two places.

edit: or you seem to have some sort of getter for this already defined in getTypeName

}
case "symbol": {
return "symboli";
}
case "function": {
return "funktio";
}
case "string": {
return "merkkijono";
}
case "undefined": {
return "määrittelemätön";
}
case "object": {
if (Array.isArray(data)) {
return "lista";
}
if (data === null) {
return "tyhjä";
}
if (data instanceof Date) {
return "päivämäärä";
}
if (data instanceof Map) {
return "hakemisto";
}
if (data instanceof Set) {
return "joukko";
}
if (data instanceof File) {
return "tiedosto";
}
if (data instanceof Promise) {
return "lupaus";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haha, this is pretty tricky.
I might prefer this to be "Promise" actually... Idk.

}

if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {
return data.constructor.name;
}

return "objekti";
}
default: {
return t;
}
}
};

const Nouns: {
[k in $ZodStringFormats | (string & {})]?: string;
} = {
regex: "regex-lauseke",
email: "sähköpostiosoite",
url: "URL-osoite",
emoji: "emoji",
uuid: "UUID",
uuidv4: "UUIDv4",
uuidv6: "UUIDv6",
nanoid: "nanoid",
guid: "GUID",
cuid: "cuid",
cuid2: "cuid2",
ulid: "ULID",
xid: "XID",
ksuid: "KSUID",
datetime: "ISO-aikaleima",
date: "ISO-päivämäärä",
time: "ISO-aika",
duration: "ISO-kesto",
ipv4: "IPv4-osoite",
ipv6: "IPv6-osoite",
cidrv4: "IPv4-alue",
cidrv6: "IPv6-alue",
base64: "base64-koodattu merkkijono",
base64url: "base64url-koodattu merkkijono",
json_string: "JSON-merkkijono",
e164: "E.164-luku",
jwt: "JWT",
template_literal: "templaattimerkkijono",
};

const InOrigin: { [k in string & {}]?: string } = {
record: "tietueessa",
map: "hakemistossa",
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As above, I feel temptation to translate map to "tietueessa" – "hakemistossa" just instantly gives me idea about a file system hierarchy.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Viitaten vähän samaan tuon datatyyppien poisheivaamisen kanssa, niin tässäkin voisi melkeen yleistää nuo invalid_element ja invalid_key -tilanteet niin, että niissä sanotaan aina jotain tyyliin:

Virheellinen arvo joukossa

ja

Virheellinen avain tietueessa

Sillä itse datatyypillä ei varmaan tuossa kontekstissa ole kuitenkaan ihan hirveän oleellista tehtävää

set: "joukossa",
};

const error: errors.$ZodErrorMap = (issue) => {
switch (issue.code) {
case "invalid_type":
return `Virheellinen tyyppi: täytyy olla ${getTypeName(issue.expected)}, oli ${parsedType(issue.input)}`;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pallottelen vaihtoehtoisella sanamuodolla

Suggested change
return `Virheellinen tyyppi: täytyy olla ${getTypeName(issue.expected)}, oli ${parsedType(issue.input)}`;
return `Virheellinen tyyppi: odotettiin ${getTypeName(issue.expected)}, oli ${parsedType(issue.input)}`;

Joka tapauksessa tämä "täytyy olla" IMO huomattavasti parempi kuin alkuperäinen "pitäisi olla" 👍

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There seems to also be some older fi.json in
aiji42//zod-i18n library which has same error translated as:

"Odotettiin arvon olevan {{expected}}, saatiin {{received}}"

But yeah, not sure, food for thought.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tbh harkitsen vahvasti sitä, että heivais kuitenkin nää datatyyppien käännökset kokonaan pois tästä kohtaa, kun näitä JS datatyyppejä on niin mahdotonta saada mielekkäästi ja tyhjentävästi hoidettua. Voisi vaan olla esim tuo:

"Virheellinen tyyppi: odotettiin {{issue.expected}}, saatiin {{parsedType(issue.input)}}"

jossa tuo parsedType vaan palauttaa saman kuin tuo englanninkielinen versio.

case "invalid_value":
if (issue.values.length === 1)
return `Virheellinen syöte: täytyy olla ${util.stringifyPrimitive(issue.values[0])}`;
return `Virheellinen valinta: täytyy olla yksi seuraavista: ${util.joinValues(issue.values, "|")}`;
case "too_big": {
const adj = issue.inclusive ? "<=" : "<";
const sizing = getSizing(issue.origin);
if (sizing) {
return `Liian suuri: ${sizing.subject} täytyy olla ${adj}${issue.maximum.toString()} ${sizing.unit}`.trim();
}
return `Liian suuri: ${issue.origin ?? "arvon"} täytyy olla ${adj}${issue.maximum.toString()}`;
}
case "too_small": {
const adj = issue.inclusive ? ">=" : ">";
const sizing = getSizing(issue.origin);
if (sizing) {
return `Liian pieni: ${sizing.subject} täytyy olla ${adj}${issue.minimum.toString()} ${sizing.unit}`.trim();
}
return `Liian pieni: ${issue.origin ?? "arvon"} täytyy olla ${adj}${issue.minimum.toString()}`;
}
case "invalid_format": {
const _issue = issue as errors.$ZodStringFormatIssues;
if (_issue.format === "starts_with") return `Virheellinen merkkijono: alussa täytyy olla "${_issue.prefix}"`;
if (_issue.format === "ends_with") return `Virheellinen merkkijono: lopussa täytyy olla "${_issue.suffix}"`;
if (_issue.format === "includes") return `Virheellinen merkkijono: täytyy sisältää "${_issue.includes}"`;
if (_issue.format === "regex") {
return `Virheellinen merkkijono: täytyy vastata regex-lauseketta ${_issue.pattern}`;
}
return `Virheellinen ${Nouns[_issue.format] ?? issue.format}`;
}
case "not_multiple_of":
return `Virheellinen luku: täytyy olla luvun ${issue.divisor} monikerta`;
case "unrecognized_keys":
return `${issue.keys.length > 1 ? "Tuntemattomat avaimet" : "Tuntematon avain"}: ${util.joinValues(issue.keys, ", ")}`;
case "invalid_key":
return `Virheellinen avain ${InOrigin[issue.origin] ?? issue.origin}`;
case "invalid_union":
return "Virheellinen yhdiste";
case "invalid_element":
return `Virheellinen arvo ${InOrigin[issue.origin] ?? issue.origin}`;
default:
return `Virheellinen syöte`;
}
};

export { error };

export default function (): { localeError: errors.$ZodErrorMap } {
return {
localeError: error,
};
}
11 changes: 6 additions & 5 deletions packages/docs/content/error-customization.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import { Tabs, Tab } from 'fumadocs-ui/components/tabs';

In Zod, validation errors are surfaced as instances of the `z.core.$ZodError` class.

> The `zod` package uses a subclass of this called `ZodError` that implements some additional convenience methods.
> The `zod` package uses a subclass of this called `ZodError` that implements some additional convenience methods.

Instances of `$ZodError` contain an `.issues` property containing a human-readable `message` and additional structured information about each encountered validation issue.
Instances of `$ZodError` contain an `.issues` property containing a human-readable `message` and additional structured information about each encountered validation issue.


<Tabs groupId="lib" items={["zod", "@zod/mini"]}>
Expand Down Expand Up @@ -81,7 +81,7 @@ z.string({ error: "Bad!"}).parse(12);
// expected: 'string',
// code: 'invalid_type',
// path: [],
// message: 'Bad!' <-- 👀 custom error message
// message: 'Bad!' <-- 👀 custom error message
// }
// ]
// }
Expand Down Expand Up @@ -238,7 +238,7 @@ result.error.issues;
// [{ message: "highest priority", ... }]
```

The `iss` object is a [discriminated union](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions) of all possible issue types. Use the `code` property to discriminate between them.
The `iss` object is a [discriminated union](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions) of all possible issue types. Use the `code` property to discriminate between them.

> For a breakdown of all Zod issue codes, see the [`@zod/core`](/packages/core#issue-types) documentation.

Expand Down Expand Up @@ -269,7 +269,7 @@ z.config({
});
```

Global error messages have *lower precedence* than schema-level or per-parse error messages.
Global error messages have *lower precedence* than schema-level or per-parse error messages.

The `iss` object is a [discriminated union](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions) of all possible issue types. Use the `code` property to discriminate between them.

Expand Down Expand Up @@ -340,3 +340,4 @@ The following locales are available:

- `az` — Azerbaijani
- `en` — English
- `fi` — Finnish