Skip to content
Merged
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
61 changes: 49 additions & 12 deletions packages/zod/src/v20170710/lang/_internal.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,57 @@
let lang: string | undefined;
import * as z from "zod/mini";
import type * as core from "zod/v4/core";
import type * as locales from "zod/v4/locales";
import { en } from "zod/v4/locales";

export type Locale = "en";
/** The names of the locales built into Zod, as exported from `zod/locales` (e.g. `en`, `ja`, `frCA`) */
type ZodLocale = keyof typeof locales;

/** The subset of Zod's built-in locales that this package has custom messages for */
type Locale = Extract<ZodLocale, "en">;

type LocaleFactory = () => { localeError: core.$ZodErrorMap };

const supportedLocales: readonly (readonly [Locale, LocaleFactory])[] = [["en", en]];

/*
N.b. Zod doesn't record which locale was passed to `z.config()`; it only keeps the locale's error map. To figure out
which locale is active, we feed this issue to the configured error map and compare its message against the message
each of our supported locales gives for the same issue.
*/
const probeIssue: core.$ZodRawIssue = { code: "invalid_type", expected: "string", input: 0 };

let fingerprints: ReadonlyMap<string | undefined, Locale> | undefined;
const detectedLocales = new WeakMap<core.$ZodErrorMap, Locale | undefined>();

function getMessage(errorMap: core.$ZodErrorMap): string | undefined {
const result = errorMap(probeIssue);
return typeof result === "string" ? result : result?.message;
}

function getFingerprints(): ReadonlyMap<string | undefined, Locale> {
fingerprints ??= new Map(supportedLocales.map(([locale, factory]) => [getMessage(factory().localeError), locale]));
return fingerprints;
}

/**
* Set the language for this package's custom messages
* Get the locale Zod is currently configured with, if this package has custom messages for it
*
* @param value The RFC-5646 tag to set the preferred locale to
* Zod Classic configures the `en` locale by default, while Zod Mini has no locale configured until something like
* `z.config(z.locales.en())` is run. Detection is done lazily, i.e. when an issue is being finalized, so `z.config()`
* may be called before or after this package is imported.
*
* @returns The configured locale, or `undefined` if no locale is configured or it isn't supported by this package
*/
export function setLang(value: string | undefined): void {
lang = value;
}

// @__NO_SIDE_EFFECTS__
export function getLocale(): Locale | undefined {
if (lang === undefined || lang === "en" || lang.startsWith("en-")) {
return "en";
function getLocale(): Locale | undefined {
const { localeError } = z.config();
if (localeError === undefined) {
return undefined;
}
if (!detectedLocales.has(localeError)) {
detectedLocales.set(localeError, getFingerprints().get(getMessage(localeError)));
}
return undefined;
return detectedLocales.get(localeError);
}

export { getLocale, type Locale };
1 change: 0 additions & 1 deletion packages/zod/src/v20170710/lang/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export { default as dateUnion } from "./date-union.ts";
export { default as reviewPayloadUnion } from "./review-payload-union.ts";
export { default as subjectTupleDuplicate } from "./subject-tuple-duplicate.ts";
export { setLang } from "./_internal.ts";
2 changes: 0 additions & 2 deletions packages/zod/tests/v20170710/base.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ import { describe, expect } from "vitest";
import { en } from "zod/locales";

import * as WaniKani from "../../src/v20170710/index.js";
import { setLang } from "../../src/v20170710/lang/_internal.ts";
import testFor from "./fixtures.js";

z.config(en());
setLang("en");

describe("ApiRevision", () => {
testFor("Valid WaniKani API Revision", ({ apiRevision }) => {
Expand Down
132 changes: 132 additions & 0 deletions packages/zod/tests/v20170710/lang.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import * as classic from "zod";
import * as z from "zod/v4/core";
import { describe, expect } from "vitest";
import { en, ja } from "zod/locales";

import * as WaniKani from "../../src/v20170710/index.js";
import { getLocale } from "../../src/v20170710/lang/_internal.ts";
import testFor from "./fixtures.js";

const messages = {
dateUnion: "Expected either a valid ISO-8601 timestamp string or a JavaScript Date",
reviewPayloadUnion:
"Review Payload must have one and only one of either an assignment_id or subject_id positive integer",
subjectTupleDuplicate: "Duplicate Subject Type detected in Subject Tuple",
};

const badReviewPayload = {
review: { assignment_id: 1, subject_id: undefined, incorrect_meaning_answers: 0, incorrect_reading_answers: 0 },
};

function getMessage(schema: z.$ZodType, value: unknown): string | undefined {
return z.safeParse(schema, value).error?.issues[0]?.message;
}

function useZodClassicDefaultLocale(): void {
z.config({ localeError: undefined });
// Zod Classic configures its default locale when the first schema is constructed
classic.string();
}

describe("getLocale", () => {
testFor("Returns undefined when no locale is configured", () => {
z.config({ localeError: undefined });
expect(getLocale()).toBeUndefined();
});
testFor("Returns en when Zod is configured with the en locale", () => {
z.config(en());
expect(getLocale()).toBe("en");
});
testFor("Returns en when Zod Classic sets its default locale", () => {
useZodClassicDefaultLocale();
expect(getLocale()).toBe("en");
});
testFor("Returns undefined when Zod is configured with an unsupported locale", () => {
z.config(ja());
expect(getLocale()).toBeUndefined();
});
testFor("Returns undefined for a custom locale error map", () => {
z.config({ localeError: () => "Custom locale message" });
expect(getLocale()).toBeUndefined();
});
testFor("Returns undefined for a custom locale error map with no message", () => {
z.config({ localeError: () => null });
expect(getLocale()).toBeUndefined();
});
testFor("Returns en for a locale error map giving en messages as objects", () => {
const { localeError } = en();
z.config({
localeError: (issue) => {
const message = localeError(issue);
return typeof message === "string" ? { message } : message;
},
});
expect(getLocale()).toBe("en");
});
testFor("Follows changes to the configured locale", () => {
const enLocale = en();
z.config(enLocale);
expect(getLocale()).toBe("en");
z.config(ja());
expect(getLocale()).toBeUndefined();
z.config(enLocale);
expect(getLocale()).toBe("en");
z.config({ localeError: undefined });
expect(getLocale()).toBeUndefined();
});
});

describe("Custom messages", () => {
testFor(
"Are used when Zod is configured with the en locale",
({ collectionParamsWithBadDatableStringUnion, repeatedSubjectTuple }) => {
z.config(en());
expect(getMessage(WaniKani.CollectionParameters, collectionParamsWithBadDatableStringUnion)).toBe(
messages.dateUnion,
);
expect(getMessage(WaniKani.ReviewPayload, badReviewPayload)).toBe(messages.reviewPayloadUnion);
expect(getMessage(WaniKani.SubjectTuple, repeatedSubjectTuple)).toBe(messages.subjectTupleDuplicate);
},
);
testFor(
"Are used when Zod Classic sets its default locale",
({ collectionParamsWithBadDatableStringUnion, repeatedSubjectTuple }) => {
useZodClassicDefaultLocale();
expect(getMessage(WaniKani.CollectionParameters, collectionParamsWithBadDatableStringUnion)).toBe(
messages.dateUnion,
);
expect(getMessage(WaniKani.ReviewPayload, badReviewPayload)).toBe(messages.reviewPayloadUnion);
expect(getMessage(WaniKani.SubjectTuple, repeatedSubjectTuple)).toBe(messages.subjectTupleDuplicate);
},
);
testFor(
"Are not used when no locale is configured",
({ collectionParamsWithBadDatableStringUnion, repeatedSubjectTuple }) => {
z.config({ localeError: undefined });
expect(getMessage(WaniKani.CollectionParameters, collectionParamsWithBadDatableStringUnion)).toBe(
"Invalid input",
);
expect(getMessage(WaniKani.ReviewPayload, badReviewPayload)).toBe("Invalid input");
expect(getMessage(WaniKani.SubjectTuple, repeatedSubjectTuple)).toBe("Invalid input");
},
);
testFor(
"Defer to Zod for an unsupported locale",
({ collectionParamsWithBadDatableStringUnion, repeatedSubjectTuple }) => {
z.config({ localeError: () => "Custom locale message" });
expect(getMessage(WaniKani.CollectionParameters, collectionParamsWithBadDatableStringUnion)).toBe(
"Custom locale message",
);
expect(getMessage(WaniKani.ReviewPayload, badReviewPayload)).toBe("Custom locale message");
expect(getMessage(WaniKani.SubjectTuple, repeatedSubjectTuple)).toBe("Custom locale message");
},
);
testFor("Follow the locale configured after the schemas were created", ({ repeatedSubjectTuple }) => {
z.config({ localeError: undefined });
expect(getMessage(WaniKani.SubjectTuple, repeatedSubjectTuple)).toBe("Invalid input");
z.config(en());
expect(getMessage(WaniKani.SubjectTuple, repeatedSubjectTuple)).toBe(messages.subjectTupleDuplicate);
z.config(ja());
expect(getMessage(WaniKani.SubjectTuple, repeatedSubjectTuple)).not.toBe(messages.subjectTupleDuplicate);
});
});
4 changes: 4 additions & 0 deletions packages/zod/tests/v20170710/reviews.test.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import * as z from "zod/v4/core";
import { describe, expect } from "vitest";
import { en } from "zod/locales";

import * as WaniKani from "../../src/v20170710/index.js";
import testFor from "./fixtures.js";

z.config(en());

describe("Review", () => {
testFor("Review from WaniKani API Docs", ({ review }) => {
expect(() => WaniKani.Review.parse(review)).not.toThrow();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ import { describe, expect } from "vitest";
import { en } from "zod/locales";

import * as WaniKani from "../../src/v20170710/index.js";
import { setLang } from "../../src/v20170710/lang/_internal.ts";
import testFor from "./fixtures.js";

z.config(en());
setLang("en");

describe("SpacedRepetitionSystemStageNumber", () => {
testFor(`Invalid SRS Stage Number: ${WaniKani.MIN_SRS_STAGE - 1}`, () => {
Expand Down
2 changes: 0 additions & 2 deletions packages/zod/tests/v20170710/study-materials.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ import { describe, expect } from "vitest";
import { en } from "zod/locales";

import * as WaniKani from "../../src/v20170710/index.js";
import { setLang } from "../../src/v20170710/lang/_internal.ts";
import testFor from "./fixtures.js";

z.config(en());
setLang("en");

describe("StudyMaterial", () => {
testFor("Real StudyMaterial", ({ studyMaterial }) => {
Expand Down
2 changes: 0 additions & 2 deletions packages/zod/tests/v20170710/subjects.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ import { describe, expect } from "vitest";
import { en } from "zod/locales";

import * as WaniKani from "../../src/v20170710/index.js";
import { setLang } from "../../src/v20170710/lang/_internal.ts";
import testFor from "./fixtures.js";

z.config(en());
setLang("en");

describe("SubjectType", () => {
testFor("Valid Subject Types", ({ subjectTypes }) => {
Expand Down
2 changes: 0 additions & 2 deletions packages/zod/tests/v20170710/user.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,9 @@ import { describe, expect } from "vitest";
import { en } from "zod/locales";

import * as WaniKani from "../../src/v20170710/index.js";
import { setLang } from "../../src/v20170710/lang/_internal.ts";
import testFor from "./fixtures.js";

z.config(en());
setLang("en");

describe("LessonBatchSizeNumber", () => {
testFor(`Invalid Lesson Batch Size: ${WaniKani.MIN_LESSON_BATCH_SIZE - 1}`, () => {
Expand Down
2 changes: 1 addition & 1 deletion vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export default defineConfig({
exclude: [
"packages/*/src/index.ts",
"packages/*/src/v20170710/index.ts",
"packages/*/src/v20170710/lang/**",
"packages/valibot/src/v20170710/lang/**",
"**/tests/**",
"**/node_modules/**",
...coverageConfigDefaults.exclude,
Expand Down
Loading