Repository: colinhacks/zod
Pull request #4199 (2025-04-16 17:57:02)

feat(locale): add belarusian language
Added Belarusian locale

coderabbitai[bot]: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto reviews are disabled on base/target branches other than the default branch.
> 
> 
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai -->
<!-- tips_start -->

---

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=colinhacks/zod&utm_content=4199):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Generate unit testing code for this file.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai generate unit testing code for this file.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and generate unit testing code.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
colinhacks: @coderabbitai review
coderabbitai[bot]: <!-- This is an auto-generated reply by CodeRabbit -->
<details>
<summary>:white_check_mark: Actions performed</summary>

Review triggered.

> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

</details>
colinhacks: Consolidating work on Belarusian here 👍 cc @wiedymi
wiedymi: @colinhacks use this PR for Belarusian language. It covers more language rules I didn't added in my initial PR. Close my PR. 

@VladSez nice work 💪

Patch:
diff --git a/packages/core/src/locales.ts b/packages/core/src/locales.ts
index 21ff0c85..33f021cd 100644
--- a/packages/core/src/locales.ts
+++ b/packages/core/src/locales.ts
@@ -1,5 +1,6 @@
 import ar from "./locales/ar.js";
 import az from "./locales/az.js";
+import be from "./locales/be.js";
 import cs from "./locales/cs.js";
 import en from "./locales/en.js";
 import es from "./locales/es.js";
@@ -18,4 +19,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, fr, he, hu, id, it, ja, pl, pt, ru, tr, ua, vi, zhCN };
+export { ar, az, be, cs, es, en, fi, fr, he, hu, id, it, ja, pl, pt, ru, tr, ua, vi, zhCN };
diff --git a/packages/core/src/locales/be.ts b/packages/core/src/locales/be.ts
new file mode 100644
index 00000000..7ce57141
--- /dev/null
+++ b/packages/core/src/locales/be.ts
@@ -0,0 +1,185 @@
+import type { $ZodStringFormats } from "../checks.js";
+import type * as errors from "../errors.js";
+import * as util from "../util.js";
+
+function getBelarusianPlural(count: number, one: string, few: string, many: string): string {
+  const absCount = Math.abs(count);
+  const lastDigit = absCount % 10;
+  const lastTwoDigits = absCount % 100;
+
+  if (lastTwoDigits >= 11 && lastTwoDigits <= 19) {
+    return many;
+  }
+
+  if (lastDigit === 1) {
+    return one;
+  }
+
+  if (lastDigit >= 2 && lastDigit <= 4) {
+    return few;
+  }
+
+  return many;
+}
+
+interface BelarusianSizable {
+  unit: {
+    one: string;
+    few: string;
+    many: string;
+  };
+  verb: string;
+}
+
+const Sizable: Record<string, BelarusianSizable> = {
+  string: {
+    unit: {
+      one: "сімвал",
+      few: "сімвалы",
+      many: "сімвалаў",
+    },
+    verb: "мець",
+  },
+  array: {
+    unit: {
+      one: "элемент",
+      few: "элементы",
+      many: "элементаў",
+    },
+    verb: "мець",
+  },
+  set: {
+    unit: {
+      one: "элемент",
+      few: "элементы",
+      many: "элементаў",
+    },
+    verb: "мець",
+  },
+  file: {
+    unit: {
+      one: "байт",
+      few: "байты",
+      many: "байтаў",
+    },
+    verb: "мець",
+  },
+};
+
+function getSizing(origin: string): BelarusianSizable | null {
+  return Sizable[origin] ?? null;
+}
+
+export const parsedType = (data: any): string => {
+  const t = typeof data;
+
+  switch (t) {
+    case "number": {
+      return Number.isNaN(data) ? "NaN" : "лік";
+    }
+    case "object": {
+      if (Array.isArray(data)) {
+        return "масіў";
+      }
+      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: "увод",
+  email: "email адрас",
+  url: "URL",
+  emoji: "эмодзі",
+  uuid: "UUID",
+  uuidv4: "UUIDv4",
+  uuidv6: "UUIDv6",
+  nanoid: "nanoid",
+  guid: "GUID",
+  cuid: "cuid",
+  cuid2: "cuid2",
+  ulid: "ULID",
+  xid: "XID",
+  ksuid: "KSUID",
+  datetime: "ISO дата і час",
+  date: "ISO дата",
+  time: "ISO час",
+  duration: "ISO працягласць",
+  ipv4: "IPv4 адрас",
+  ipv6: "IPv6 адрас",
+  cidrv4: "IPv4 дыяпазон",
+  cidrv6: "IPv6 дыяпазон",
+  base64: "радок у фармаце base64",
+  base64url: "радок у фармаце base64url",
+  json_string: "JSON радок",
+  e164: "нумар E.164",
+  jwt: "JWT",
+  template_literal: "увод",
+};
+
+const error: errors.$ZodErrorMap = (issue) => {
+  switch (issue.code) {
+    case "invalid_type":
+      return `Няправільны ўвод: чакаўся ${issue.expected}, атрымана ${parsedType(issue.input)}`;
+    case "invalid_value":
+      if (issue.values.length === 1) return `Няправільны ўвод: чакалася ${util.stringifyPrimitive(issue.values[0])}`;
+      return `Няправільны варыянт: чакаўся адзін з ${util.joinValues(issue.values, "|")}`;
+    case "too_big": {
+      const adj = issue.inclusive ? "<=" : "<";
+      const sizing = getSizing(issue.origin);
+      if (sizing) {
+        const maxValue = Number(issue.maximum);
+        const unit = getBelarusianPlural(maxValue, sizing.unit.one, sizing.unit.few, sizing.unit.many);
+        return `Занадта вялікі: чакалася, што ${issue.origin ?? "значэнне"} павінна ${sizing.verb} ${adj}${issue.maximum.toString()} ${unit}`;
+      }
+      return `Занадта вялікі: чакалася, што ${issue.origin ?? "значэнне"} павінна быць ${adj}${issue.maximum.toString()}`;
+    }
+    case "too_small": {
+      const adj = issue.inclusive ? ">=" : ">";
+      const sizing = getSizing(issue.origin);
+      if (sizing) {
+        const minValue = Number(issue.minimum);
+        const unit = getBelarusianPlural(minValue, sizing.unit.one, sizing.unit.few, sizing.unit.many);
+        return `Занадта малы: чакалася, што ${issue.origin} павінна ${sizing.verb} ${adj}${issue.minimum.toString()} ${unit}`;
+      }
+      return `Занадта малы: чакалася, што ${issue.origin} павінна быць ${adj}${issue.minimum.toString()}`;
+    }
+    case "invalid_format": {
+      const _issue = issue as errors.$ZodStringFormatIssues;
+      if (_issue.format === "starts_with") return `Няправільны радок: павінен пачынацца з "${_issue.prefix}"`;
+      if (_issue.format === "ends_with") return `Няправільны радок: павінен заканчвацца на "${_issue.suffix}"`;
+      if (_issue.format === "includes") return `Няправільны радок: павінен змяшчаць "${_issue.includes}"`;
+      if (_issue.format === "regex") return `Няправільны радок: павінен адпавядаць шаблону ${_issue.pattern}`;
+      return `Няправільны ${Nouns[_issue.format] ?? issue.format}`;
+    }
+    case "not_multiple_of":
+      return `Няправільны лік: павінен быць кратным ${issue.divisor}`;
+    case "unrecognized_keys":
+      return `Нераспазнаны ${issue.keys.length > 1 ? "ключы" : "ключ"}: ${util.joinValues(issue.keys, ", ")}`;
+    case "invalid_key":
+      return `Няправільны ключ у ${issue.origin}`;
+    case "invalid_union":
+      return "Няправільны ўвод";
+    case "invalid_element":
+      return `Няправільнае значэнне ў ${issue.origin}`;
+    default:
+      return `Няправільны ўвод`;
+  }
+};
+
+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 51b07247..82aca639 100644
--- a/packages/docs/content/error-customization.mdx
+++ b/packages/docs/content/error-customization.mdx
@@ -357,6 +357,7 @@ The following locales are available:
 
 - `ar` — Arabic
 - `az` — Azerbaijani
+- `be` — Belarusian
 - `en` — English
 - `es` — Spanish
 - `ru` — Russian


Test patch:
diff --git a/packages/core/tests/locales/be.test.ts b/packages/core/tests/locales/be.test.ts
new file mode 100644
index 00000000..ebcd461a
--- /dev/null
+++ b/packages/core/tests/locales/be.test.ts
@@ -0,0 +1,124 @@
+import { describe, expect, it } from "vitest";
+import be from "../../src/locales/be.js";
+
+describe("Belarusian localization", () => {
+  const { localeError } = be();
+
+  describe("pluralization rules", () => {
+    for (const { type, cases } of TEST_CASES) {
+      describe(`${type} pluralization`, () => {
+        for (const { count, expected } of cases) {
+          it(`correctly pluralizes ${count} ${type}`, () => {
+            const error = localeError({
+              code: "too_small",
+              minimum: count,
+              type: "number",
+              inclusive: true,
+              path: [],
+              origin: type,
+              input: count - 1,
+            });
+            expect(error).toContain(expected);
+          });
+        }
+      });
+    }
+
+    it("handles negative numbers correctly", () => {
+      const error = localeError({
+        code: "too_small",
+        minimum: -2,
+        type: "number",
+        inclusive: true,
+        path: [],
+        origin: "array",
+        input: -3,
+      });
+      expect(error).toContain("-2 элементы");
+    });
+
+    it("handles zero correctly", () => {
+      const error = localeError({
+        code: "too_small",
+        minimum: 0,
+        type: "number",
+        inclusive: true,
+        path: [],
+        origin: "array",
+        input: -1,
+      });
+      expect(error).toContain("0 элементаў");
+    });
+
+    it("handles bigint values correctly", () => {
+      const error = localeError({
+        code: "too_small",
+        minimum: BigInt(21),
+        type: "number",
+        inclusive: true,
+        path: [],
+        origin: "array",
+        input: BigInt(20),
+      });
+      expect(error).toContain("21 элемент");
+    });
+  });
+});
+
+const TEST_CASES = [
+  {
+    type: "array",
+    cases: [
+      { count: 1, expected: "1 элемент" },
+      { count: 2, expected: "2 элементы" },
+      { count: 5, expected: "5 элементаў" },
+      { count: 11, expected: "11 элементаў" },
+      { count: 21, expected: "21 элемент" },
+      { count: 22, expected: "22 элементы" },
+      { count: 25, expected: "25 элементаў" },
+      { count: 101, expected: "101 элемент" },
+      { count: 111, expected: "111 элементаў" },
+    ],
+  },
+  {
+    type: "set",
+    cases: [
+      { count: 1, expected: "1 элемент" },
+      { count: 2, expected: "2 элементы" },
+      { count: 5, expected: "5 элементаў" },
+      { count: 11, expected: "11 элементаў" },
+      { count: 21, expected: "21 элемент" },
+      { count: 22, expected: "22 элементы" },
+      { count: 25, expected: "25 элементаў" },
+      { count: 101, expected: "101 элемент" },
+      { count: 111, expected: "111 элементаў" },
+    ],
+  },
+  {
+    type: "string",
+    cases: [
+      { count: 1, expected: "1 сімвал" },
+      { count: 2, expected: "2 сімвалы" },
+      { count: 5, expected: "5 сімвалаў" },
+      { count: 11, expected: "11 сімвалаў" },
+      { count: 21, expected: "21 сімвал" },
+      { count: 22, expected: "22 сімвалы" },
+      { count: 25, expected: "25 сімвалаў" },
+    ],
+  },
+  {
+    type: "file",
+    cases: [
+      { count: 0, expected: "0 байтаў" },
+      { count: 1, expected: "1 байт" },
+      { count: 2, expected: "2 байты" },
+      { count: 5, expected: "5 байтаў" },
+      { count: 11, expected: "11 байтаў" },
+      { count: 21, expected: "21 байт" },
+      { count: 22, expected: "22 байты" },
+      { count: 25, expected: "25 байтаў" },
+      { count: 101, expected: "101 байт" },
+      { count: 110, expected: "110 байтаў" },
+    ],
+  },
+] as const;