Repository: colinhacks/zod
Pull request #4196 (2025-04-16 17:23:23)

feat(lang): Added initial Turkish locale
Added initial Turkish locale.

ozgurozalp: thank you @mertssmnoglu , I've formatted with biome and fixed it
ozgurozalp: Thanks also for the unit test @mertssmnoglu wrote.


mesutcany: As a Turk, I can confirm that it is a very successful translation. @colinhacks 
colinhacks: Thank you!

Patch:
diff --git a/packages/core/src/locales.ts b/packages/core/src/locales.ts
index c7f4744c..c79b36cb 100644
--- a/packages/core/src/locales.ts
+++ b/packages/core/src/locales.ts
@@ -3,6 +3,7 @@ import az from "./locales/az.js";
 import cs from "./locales/cs.js";
 import en from "./locales/en.js";
 import es from "./locales/es.js";
+import tr from "./locales/tr.js";
 import id from "./locales/id.js";
 import it from "./locales/it.js";
 import vi from "./locales/vi.js";
@@ -16,4 +17,4 @@ import fr from "./locales/fr.js";
 import ja from "./locales/ja.js";
 import pt from "./locales/pt.js";
 
-export { ar, az, cs, es, en, fi, he, hu, id, it, pt, ja, fr, pl, ua, vi, zhCN };
+export { ar, az, cs, es, en, fi, he, hu, id, it, pt, tr, ja, fr, pl, ua, vi, zhCN };
diff --git a/packages/core/src/locales/tr.ts b/packages/core/src/locales/tr.ts
new file mode 100644
index 00000000..cadb0e56
--- /dev/null
+++ b/packages/core/src/locales/tr.ts
@@ -0,0 +1,121 @@
+import type { $ZodStringFormats } from "../checks.js";
+import type * as errors from "../errors.js";
+import * as util from "../util.js";
+
+const Sizable: Record<string, { unit: string; verb: string }> = {
+  string: { unit: "karakter", verb: "olmalı" },
+  file: { unit: "bayt", verb: "olmalı" },
+  array: { unit: "öğe", verb: "olmalı" },
+  set: { unit: "öğe", verb: "olmalı" },
+};
+
+function getSizing(origin: string): { unit: string; verb: string } | null {
+  return Sizable[origin] ?? null;
+}
+
+export const parsedType = (data: any): string => {
+  const t = typeof data;
+
+  switch (t) {
+    case "number": {
+      return Number.isNaN(data) ? "NaN" : "number";
+    }
+    case "object": {
+      if (Array.isArray(data)) {
+        return "array";
+      }
+      if (data === null) {
+        return "null";
+      }
+
+      if (Object.getPrototypeOf(data) !== Object.prototype && data.constructor) {
+        return data.constructor.name;
+      }
+    }
+  }
+  return t;
+};
+
+const Nouns: {
+  [k in $ZodStringFormats | (string & {})]?: string;
+} = {
+  regex: "girdi",
+  email: "e-posta adresi",
+  url: "URL",
+  emoji: "emoji",
+  uuid: "UUID",
+  uuidv4: "UUIDv4",
+  uuidv6: "UUIDv6",
+  nanoid: "nanoid",
+  guid: "GUID",
+  cuid: "cuid",
+  cuid2: "cuid2",
+  ulid: "ULID",
+  xid: "XID",
+  ksuid: "KSUID",
+  datetime: "ISO tarih ve saat",
+  date: "ISO tarih",
+  time: "ISO saat",
+  duration: "ISO süre",
+  ipv4: "IPv4 adresi",
+  ipv6: "IPv6 adresi",
+  cidrv4: "IPv4 aralığı",
+  cidrv6: "IPv6 aralığı",
+  base64: "base64 ile şifrelenmiş metin",
+  base64url: "base64url ile şifrelenmiş metin",
+  json_string: "JSON dizesi",
+  e164: "E.164 sayısı",
+  jwt: "JWT",
+  template_literal: "Şablon dizesi",
+};
+
+const error: errors.$ZodErrorMap = (issue) => {
+  switch (issue.code) {
+    case "invalid_type":
+      return `Geçersiz değer: beklenen ${issue.expected}, alınan ${parsedType(issue.input)}`;
+    case "invalid_value":
+      if (issue.values.length === 1) return `Geçersiz değer: beklenen ${util.stringifyPrimitive(issue.values[0])}`;
+      return `Geçersiz seçenek: aşağıdakilerden biri olmalı: ${util.joinValues(issue.values, "|")}`;
+    case "too_big": {
+      const adj = issue.inclusive ? "<=" : "<";
+      const sizing = getSizing(issue.origin);
+      if (sizing)
+        return `Çok büyük: beklenen ${issue.origin ?? "değer"} ${adj}${issue.maximum.toString()} ${sizing.unit ?? "öğe"}`;
+      return `Çok büyük: beklenen ${issue.origin ?? "değer"} ${adj}${issue.maximum.toString()}`;
+    }
+    case "too_small": {
+      const adj = issue.inclusive ? ">=" : ">";
+      const sizing = getSizing(issue.origin);
+      if (sizing) return `Çok küçük: beklenen ${issue.origin} ${adj}${issue.minimum.toString()} ${sizing.unit}`;
+      return `Çok küçük: beklenen ${issue.origin} ${adj}${issue.minimum.toString()}`;
+    }
+    case "invalid_format": {
+      const _issue = issue as errors.$ZodStringFormatIssues;
+      if (_issue.format === "starts_with") return `Geçersiz metin: "${_issue.prefix}" ile başlamalı`;
+      if (_issue.format === "ends_with") return `Geçersiz metin: "${_issue.suffix}" ile bitmeli`;
+      if (_issue.format === "includes") return `Geçersiz metin: "${_issue.includes}" içermeli`;
+      if (_issue.format === "regex") return `Geçersiz metin: ${_issue.pattern} desenine uymalı`;
+      return `Geçersiz ${Nouns[_issue.format] ?? issue.format}`;
+    }
+    case "not_multiple_of":
+      return `Geçersiz sayı: ${issue.divisor} ile tam bölünebilmeli`;
+    case "unrecognized_keys":
+      return `Tanınmayan anahtar${issue.keys.length > 1 ? "lar" : ""}: ${util.joinValues(issue.keys, ", ")}`;
+    case "invalid_key":
+      return `${issue.origin} içinde geçersiz anahtar`;
+    case "invalid_union":
+      return "Geçersiz değer";
+    case "invalid_element":
+      return `${issue.origin} içinde geçersiz değer`;
+    default:
+      return `Geçersiz değer`;
+  }
+};
+
+export { error };
+
+export default function (): { localeError: errors.$ZodErrorMap } {
+  return {
+    localeError: error,
+  };
+}
diff --git a/packages/docs/content/error-customization.mdx b/packages/docs/content/error-customization.mdx
index fe545000..2e0cd2fe 100644
--- a/packages/docs/content/error-customization.mdx
+++ b/packages/docs/content/error-customization.mdx
@@ -358,6 +358,7 @@ The following locales are available:
 - `ar` — Arabic
 - `az` — Azerbaijani
 - `en` — English
+- `tr` — Türkçe
 - `it` — Italian
 - `cs` - Czech
 - `vi` — Tiếng Việt
@@ -402,5 +403,3 @@ Below is a quick reference for determining error precedence: if multiple error c
 
   ```ts
   z.config(z.locales.en());
-  ```
-  
\ No newline at end of file


Test patch:
diff --git a/packages/core/tests/locales/tr.test.ts b/packages/core/tests/locales/tr.test.ts
new file mode 100644
index 00000000..053e1205
--- /dev/null
+++ b/packages/core/tests/locales/tr.test.ts
@@ -0,0 +1,69 @@
+import { expect, test } from "vitest";
+import * as z from "zod";
+import { parsedType } from "../../src/locales/tr.js";
+
+test("parsedType", () => {
+  expect(parsedType("string")).toBe("string");
+  expect(parsedType(1)).toBe("number");
+  expect(parsedType(true)).toBe("boolean");
+  expect(parsedType(null)).toBe("null");
+  expect(parsedType(undefined)).toBe("undefined");
+  expect(parsedType([])).toBe("array");
+  expect(parsedType({})).toBe("object");
+  expect(parsedType(new Date())).toBe("Date");
+  expect(parsedType(new Map())).toBe("Map");
+  expect(parsedType(new Set())).toBe("Set");
+  expect(parsedType(new Error())).toBe("Error");
+
+  const nullPrototype = Object.create(null);
+  expect(parsedType(nullPrototype)).toBe("object");
+
+  const doubleNullPrototype = Object.create(Object.create(null));
+  expect(parsedType(doubleNullPrototype)).toBe("object");
+
+  expect(parsedType(Number.NaN)).toBe("NaN");
+});
+
+test("locales - tr", () => {
+  z.config(z.locales.tr());
+
+  const invalidType = z.number().safeParse("a");
+  expect(invalidType.error!.issues[0].code).toBe("invalid_type");
+  expect(invalidType.error!.issues[0].message).toBe("Geçersiz değer: beklenen number, alınan string");
+
+  const invalidType2 = z.string().safeParse(1);
+  expect(invalidType2.error!.issues[0].code).toBe("invalid_type");
+  expect(invalidType2.error!.issues[0].message).toBe("Geçersiz değer: beklenen string, alınan number");
+
+  const invalidValue = z.enum(["a", "b"]).safeParse(1);
+  expect(invalidValue.error!.issues[0].code).toBe("invalid_value");
+  expect(invalidValue.error!.issues[0].message).toBe('Geçersiz seçenek: aşağıdakilerden biri olmalı: "a"|"b"');
+
+  const tooBig = z.number().max(10).safeParse(15);
+  expect(tooBig.error!.issues[0].code).toBe("too_big");
+  expect(tooBig.error!.issues[0].message).toBe("Çok büyük: beklenen number <=10");
+
+  const tooSmall = z.number().min(10).safeParse(5);
+  expect(tooSmall.error!.issues[0].code).toBe("too_small");
+  expect(tooSmall.error!.issues[0].message).toBe("Çok küçük: beklenen number >=10");
+
+  const invalidFormatRegex = z.string().regex(/abcd/).safeParse("invalid-string");
+  expect(invalidFormatRegex.error!.issues[0].code).toBe("invalid_format");
+  expect(invalidFormatRegex.error!.issues[0].message).toBe("Geçersiz metin: /abcd/ desenine uymalı");
+
+  const invalidFormatStartsWith = z.string().startsWith("abcd").safeParse("invalid-string");
+  expect(invalidFormatStartsWith.error!.issues[0].code).toBe("invalid_format");
+  expect(invalidFormatStartsWith.error!.issues[0].message).toBe('Geçersiz metin: "abcd" ile başlamalı');
+
+  const notMultipleOf = z.number().multipleOf(3).safeParse(10);
+  expect(notMultipleOf.error!.issues[0].code).toBe("not_multiple_of");
+  expect(notMultipleOf.error!.issues[0].message).toBe("Geçersiz sayı: 3 ile tam bölünebilmeli");
+
+  const unrecognizedKeys = z.object({ a: z.string(), b: z.number() }).strict().safeParse({ a: "a", b: 1, c: 2 });
+  expect(unrecognizedKeys.error!.issues[0].code).toBe("unrecognized_keys");
+  expect(unrecognizedKeys.error!.issues[0].message).toBe('Tanınmayan anahtar: "c"');
+
+  const invalidUnion = z.union([z.string(), z.number()]).safeParse(true);
+  expect(invalidUnion.error!.issues[0].code).toBe("invalid_union");
+  expect(invalidUnion.error!.issues[0].message).toBe("Geçersiz değer");
+});