From b814f7cad6cc5888961efb99ec71d885c819a6f2 Mon Sep 17 00:00:00 2001 From: Arpit Kuriyal Date: Tue, 5 Aug 2025 21:34:54 +0530 Subject: [PATCH 1/2] completed localisation for all keywords --- src/index.js | 64 +++---- src/keywordErrorMessage.test.js | 123 ++++++++++--- src/localization.js | 168 +++++++++++++++++- src/normalizeOutputFormat/normalizeOutput.js | 31 ++++ .../normalizeOutput.test.js | 12 +- src/translations/en-US.ftl | 26 ++- 6 files changed, 357 insertions(+), 67 deletions(-) diff --git a/src/index.js b/src/index.js index 0b9fa51..a2f74d6 100644 --- a/src/index.js +++ b/src/index.js @@ -111,7 +111,7 @@ const errorHandlers = [ if (!normalizedErrors["https://json-schema.org/keyword/minLength"][schemaLocation]) { const keyword = await getSchema(schemaLocation); errors.push({ - message: `The instance should be at least ${Schema.value(keyword)} characters`, + message: localization.getMinLengthErrorMessage(Schema.value(keyword)), instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); @@ -131,7 +131,7 @@ const errorHandlers = [ if (!normalizedErrors["https://json-schema.org/keyword/maxLength"][schemaLocation]) { const keyword = await getSchema(schemaLocation); errors.push({ - message: `The instance should be atmost ${Schema.value(keyword)} characters long.`, + message: localization.getMaxLengthErrorMessage(Schema.value(keyword)), instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); @@ -171,7 +171,7 @@ const errorHandlers = [ if (!normalizedErrors["https://json-schema.org/keyword/maximum"][schemaLocation]) { const keyword = await getSchema(schemaLocation); errors.push({ - message: `The instance should be less than or equal to ${Schema.value(keyword)}.`, + message: localization.getMaximumErrorMessage(Schema.value(keyword)), instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); @@ -191,7 +191,7 @@ const errorHandlers = [ if (!normalizedErrors["https://json-schema.org/keyword/minimum"][schemaLocation]) { const keyword = await getSchema(schemaLocation); errors.push({ - message: `The instance should be greater than or equal to ${Schema.value(keyword)}.`, + message: localization.getMinimumErrorMessage(Schema.value(keyword)), instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); @@ -211,7 +211,7 @@ const errorHandlers = [ if (!normalizedErrors["https://json-schema.org/keyword/exclusiveMinimum"][schemaLocation]) { const keyword = await getSchema(schemaLocation); errors.push({ - message: `The instance should be greater than ${Schema.value(keyword)}.`, + message: localization.getExclusiveMinimumErrorMessage(Schema.value(keyword)), instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); @@ -231,7 +231,7 @@ const errorHandlers = [ if (!normalizedErrors["https://json-schema.org/keyword/exclusiveMaximum"][schemaLocation]) { const keyword = await getSchema(schemaLocation); errors.push({ - message: `The instance should be less than ${Schema.value(keyword)}.`, + message: localization.getExclusiveMaximumErrorMessage(Schema.value(keyword)), instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); @@ -256,7 +256,7 @@ const errorHandlers = [ required.delete(propertyName); } errors.push({ - message: `"${Instance.uri(instance)}" is missing required property(s): ${[...required].join(", ")}.`, + message: localization.getRequiredErrorMessage(Instance.uri(instance), [...required]), instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); @@ -276,7 +276,7 @@ const errorHandlers = [ if (!normalizedErrors["https://json-schema.org/keyword/multipleOf"][schemaLocation]) { const keyword = await getSchema(schemaLocation); errors.push({ - message: `The instance should be of multiple of ${Schema.value(keyword)}.`, + message: localization.getMultipleOfErrorMessage(Schema.value(keyword)), instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); @@ -296,7 +296,7 @@ const errorHandlers = [ if (!normalizedErrors["https://json-schema.org/keyword/maxProperties"][schemaLocation]) { const keyword = await getSchema(schemaLocation); errors.push({ - message: `The instance should have maximum ${Schema.value(keyword)} properties.`, + message: localization.getMaxPropertiesErrorMessage(Schema.value(keyword)), instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); @@ -316,7 +316,7 @@ const errorHandlers = [ if (!normalizedErrors["https://json-schema.org/keyword/minProperties"][schemaLocation]) { const keyword = await getSchema(schemaLocation); errors.push({ - message: `The instance should have minimum ${Schema.value(keyword)} properties.`, + message: localization.getMinPropertiesErrorMessage(Schema.value(keyword)), instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); @@ -336,7 +336,7 @@ const errorHandlers = [ if (!normalizedErrors["https://json-schema.org/keyword/const"][schemaLocation]) { const keyword = await getSchema(schemaLocation); errors.push({ - message: `The instance should be equal to ${Schema.value(keyword)}.`, + message: localization.getConstErrorMessage(Schema.value(keyword)), instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); @@ -366,23 +366,25 @@ const errorHandlers = [ weight: leven(value, currentValue) })) .sort((a, b) => a.weight - b.weight)[0]; - - let suggestion = ""; + let message; if ( allowedValues.length === 1 || (bestMatch && bestMatch.weight < bestMatch.value.length) ) { - suggestion = ` Did you mean "${bestMatch.value}"?`; - errors.push({ - message: `Unexpected value "${currentValue}". ${suggestion}`, - instanceLocation: Instance.uri(instance), - schemaLocation: schemaLocation + message = localization.getEnumErrorMessage({ + variant: "suggestion", + instanceValue: currentValue, + suggestion: bestMatch.value + }); + } else { + message = localization.getEnumErrorMessage({ + variant: "fallback", + instanceValue: currentValue, + allowedValues: allowedValues }); - continue; } - errors.push({ - message: `Unexpected value "${currentValue}". Expected one of: ${allowedValues.join(",")}.`, + message, instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); @@ -402,7 +404,7 @@ const errorHandlers = [ if (!normalizedErrors["https://json-schema.org/keyword/maxItems"][schemaLocation]) { const keyword = await getSchema(schemaLocation); errors.push({ - message: `The instance should contain maximum ${Schema.value(keyword)} items in the array.`, + message: localization.getMaxItemsErrorMessage(Schema.value(keyword)), instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); @@ -422,7 +424,7 @@ const errorHandlers = [ if (!normalizedErrors["https://json-schema.org/keyword/minItems"][schemaLocation]) { const keyword = await getSchema(schemaLocation); errors.push({ - message: `The instance should contain minimum ${Schema.value(keyword)} items in the array.`, + message: localization.getMinItemsErrorMessage(Schema.value(keyword)), instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); @@ -442,7 +444,7 @@ const errorHandlers = [ for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/uniqueItems"]) { if (!normalizedErrors["https://json-schema.org/keyword/uniqueItems"][schemaLocation]) { errors.push({ - message: `The instance should have unique items in the array.`, + message: localization.getUniqueItemsErrorMessage(), instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); @@ -462,7 +464,7 @@ const errorHandlers = [ if (!normalizedErrors["https://json-schema.org/keyword/format"][schemaLocation]) { const keyword = await getSchema(schemaLocation); errors.push({ - message: `The instance should match the format: ${Schema.value(keyword)}.`, + message: localization.getFormatErrorMessage(Schema.value(keyword)), instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); @@ -482,7 +484,7 @@ const errorHandlers = [ if (!normalizedErrors["https://json-schema.org/keyword/pattern"][schemaLocation]) { const keyword = await getSchema(schemaLocation); errors.push({ - message: `The instance should match the pattern: ${Schema.value(keyword)}.`, + message: localization.getPatternErrorMessage(Schema.value(keyword)), instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); @@ -499,7 +501,7 @@ const errorHandlers = [ if (normalizedErrors["https://json-schema.org/keyword/contains"]) { for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/contains"]) { errors.push({ - message: `A required value is missing from the list`, + message: localization.getContainsErrorMessage(), instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); @@ -522,7 +524,7 @@ const errorHandlers = [ if (normalizedErrors["https://json-schema.org/keyword/not"]) { for (const schemaLocation in normalizedErrors["https://json-schema.org/keyword/not"]) { errors.push({ - message: `The instance is not allowed to be used in this schema.`, + message: localization.getNotErrorMessage(), instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); @@ -539,9 +541,9 @@ const errorHandlers = [ if (normalizedErrors["https://json-schema.org/validation"]) { for (const schemaLocation in normalizedErrors["https://json-schema.org/validation"]) { if (!normalizedErrors["https://json-schema.org/validation"][schemaLocation] && schemaLocation.endsWith("/additionalProperties")) { - const notAllowedValue = Instance.uri(instance).split("/").pop(); + const notAllowedValue = /** @type string */(Instance.uri(instance).split("/").pop()); errors.push({ - message: `The property "${notAllowedValue}" is not allowed.`, + message: localization.getAdditionalPropertiesErrorMessage(notAllowedValue), instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); @@ -567,7 +569,7 @@ const errorHandlers = [ if (missing.length > 0) { errors.push({ - message: `Property "${propertyName}" requires property(s): ${missing.join(", ")}.`, + message: localization.getDependentRequiredErrorMessage(propertyName, [...missing]), instanceLocation: Instance.uri(instance), schemaLocation: schemaLocation }); diff --git a/src/keywordErrorMessage.test.js b/src/keywordErrorMessage.test.js index 066c564..71af1c9 100644 --- a/src/keywordErrorMessage.test.js +++ b/src/keywordErrorMessage.test.js @@ -39,7 +39,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/minLength", instanceLocation: "#", - message: "The instance should be at least 3 characters" + message: localization.getMinLengthErrorMessage(3) } ]); }); @@ -67,7 +67,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/maxLength", instanceLocation: "#", - message: "The instance should be atmost 3 characters long." + message: localization.getMaxLengthErrorMessage(3) } ]); }); @@ -123,7 +123,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/maximum", instanceLocation: "#", - message: `The instance should be less than or equal to 10.` + message: localization.getMaximumErrorMessage(10) } ]); }); @@ -151,7 +151,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/minimum", instanceLocation: "#", - message: `The instance should be greater than or equal to 10.` + message: localization.getMinimumErrorMessage(10) } ]); }); @@ -179,7 +179,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/exclusiveMaximum", instanceLocation: "#", - message: `The instance should be less than 10.` + message: localization.getExclusiveMaximumErrorMessage(10) } ]); }); @@ -207,7 +207,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/exclusiveMinimum", instanceLocation: "#", - message: `The instance should be greater than 10.` + message: localization.getExclusiveMinimumErrorMessage(10) } ]); }); @@ -236,7 +236,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/required", instanceLocation: "#", - message: `"#" is missing required property(s): baz.` + message: localization.getRequiredErrorMessage("#", ["baz"]) } ]); }); @@ -265,7 +265,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/multipleOf", instanceLocation: "#", - message: `The instance should be of multiple of 5.` + message: localization.getMultipleOfErrorMessage(5) } ]); }); @@ -294,7 +294,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/maxProperties", instanceLocation: "#", - message: `The instance should have maximum 2 properties.` + message: localization.getMaxPropertiesErrorMessage(2) } ]); }); @@ -323,7 +323,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/minProperties", instanceLocation: "#", - message: `The instance should have minimum 2 properties.` + message: localization.getMinPropertiesErrorMessage(2) } ]); }); @@ -352,7 +352,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/const", instanceLocation: "#", - message: `The instance should be equal to 2.` + message: localization.getConstErrorMessage(2) } ]); }); @@ -381,7 +381,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/enum", instanceLocation: "#", - message: `Unexpected value "rwd". Did you mean "red"?` + message: localization.getEnumErrorMessage({ variant: "suggestion", instanceValue: "rwd", suggestion: "red" }) }]); }); @@ -408,7 +408,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/maxItems", instanceLocation: "#", - message: `The instance should contain maximum 3 items in the array.` + message: localization.getMaxItemsErrorMessage(3) } ]); }); @@ -436,7 +436,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/minItems", instanceLocation: "#", - message: `The instance should contain minimum 3 items in the array.` + message: localization.getMinItemsErrorMessage(3) } ]); }); @@ -464,7 +464,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/uniqueItems", instanceLocation: "#", - message: `The instance should have unique items in the array.` + message: localization.getUniqueItemsErrorMessage() } ]); }); @@ -491,7 +491,7 @@ describe("Error messages", async () => { { schemaLocation: "https://example.com/main#/format", instanceLocation: "#", - message: "The instance should match the format: email." + message: localization.getFormatErrorMessage("email") } ]); }); @@ -518,7 +518,7 @@ describe("Error messages", async () => { { schemaLocation: "https://example.com/main#/pattern", instanceLocation: "#", - message: "The instance should match the pattern: ^[a-z]+$." + message: localization.getPatternErrorMessage("^[a-z]+$") } ]); }); @@ -678,7 +678,7 @@ describe("Error messages", async () => { { schemaLocation: `https://example.com/main#/anyOf/0/minLength`, instanceLocation: "#", - message: "The instance should be at least 5 characters" + message: localization.getMinLengthErrorMessage(5) } ]); }); @@ -739,7 +739,7 @@ describe("Error messages", async () => { { schemaLocation: "https://example.com/main#/anyOf/1/properties/ID/pattern", instanceLocation: "#/ID", - message: "The instance should match the pattern: ^[0-9\\-]+$." + message: localization.getPatternErrorMessage("^[0-9\\-]+$") } ]); }); @@ -882,7 +882,7 @@ describe("Error messages", async () => { { schemaLocation: "https://example.com/main#/$defs/numberSchema/minimum", instanceLocation: "#/foo", - message: "The instance should be greater than or equal to 10." + message: localization.getMinimumErrorMessage(10) } ]); }); @@ -929,7 +929,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([ { instanceLocation: "#", - message: "A required value is missing from the list", + message: localization.getContainsErrorMessage(), schemaLocation: "https://example.com/main#/contains" }, { @@ -939,12 +939,12 @@ describe("Error messages", async () => { }, { instanceLocation: "#/1", - message: "The instance should be of multiple of 2.", + message: localization.getMultipleOfErrorMessage(2), schemaLocation: "https://example.com/main#/contains/multipleOf" }, { instanceLocation: "#/2", - message: "The instance should be of multiple of 2.", + message: localization.getMultipleOfErrorMessage(2), schemaLocation: "https://example.com/main#/contains/multipleOf" } ]); @@ -972,7 +972,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([ { instanceLocation: "#", - message: `The instance should be greater than or equal to 0.`, + message: localization.getMinimumErrorMessage(0), schemaLocation: "https://example.com/main#/then/minimum" } ]); @@ -1000,7 +1000,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([ { instanceLocation: "#", - message: `The instance should be greater than or equal to 0.`, + message: localization.getMinimumErrorMessage(0), schemaLocation: "https://example.com/main#/else/minimum" } ]); @@ -1085,7 +1085,7 @@ describe("Error messages", async () => { expect(result.errors).to.eql([ { instanceLocation: "#*/Foo", - message: "The instance should match the pattern: ^[a-z]*$.", + message: localization.getPatternErrorMessage("^[a-z]*$"), schemaLocation: "https://example.com/main#/propertyNames/pattern" } ]); @@ -1130,12 +1130,12 @@ describe("Error messages", async () => { expect(result.errors).to.eql([ { instanceLocation: "#/unknown_property", - message: `The property "unknown_property" is not allowed.`, + message: localization.getAdditionalPropertiesErrorMessage("unknown_property"), schemaLocation: "https://example.com/main#/additionalProperties" }, { instanceLocation: "#/unknown_property1", - message: `The property "unknown_property1" is not allowed.`, + message: localization.getAdditionalPropertiesErrorMessage("unknown_property1"), schemaLocation: "https://example.com/main#/additionalProperties" } ]); @@ -1167,9 +1167,74 @@ describe("Error messages", async () => { expect(result.errors).to.eql([ { instanceLocation: "#", - message: `Property "foo" requires property(s): baz.`, + message: localization.getDependentRequiredErrorMessage("foo", ["baz"]), schemaLocation: "https://example.com/main#/dependentRequired" } ]); }); + + test("should fail when an unevaluated item has the wrong type", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + prefixItems: [{ type: "number" }], + unevaluatedItems: { type: "string" } + }, schemaUri); + + const instance = [1, "two", false]; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/unevaluatedItems/type", + instanceLocation: "#/2" + } + ] + }; + + const result = await betterJsonSchemaErrors(instance, output, schemaUri); + expect(result.errors).to.eql([ + { + schemaLocation: "https://example.com/main#/unevaluatedItems/type", + instanceLocation: "#/2", + message: localization.getTypeErrorMessage(["string"], "boolean") + } + ]); + }); + + test("should fail when an unevaluated property has the wrong type", async () => { + registerSchema({ + $schema: "https://json-schema.org/draft/2020-12/schema", + properties: { + known: { type: "string" } + }, + unevaluatedProperties: { type: "number" } + }, schemaUri); + + const instance = { + known: "a string", + unknown: "this should have been a number" + }; + + /** @type OutputFormat */ + const output = { + valid: false, + errors: [ + { + absoluteKeywordLocation: "https://example.com/main#/unevaluatedProperties/type", + instanceLocation: "#/unknown" + } + ] + }; + + const result = await betterJsonSchemaErrors(instance, output, schemaUri); + expect(result.errors).to.eql([ + { + schemaLocation: "https://example.com/main#/unevaluatedProperties/type", + instanceLocation: "#/unknown", + message: localization.getTypeErrorMessage("number", "string") + } + ]); + }); }); diff --git a/src/localization.js b/src/localization.js index 5e41f89..dde1d1f 100644 --- a/src/localization.js +++ b/src/localization.js @@ -2,7 +2,8 @@ import { readFile } from "node:fs/promises"; import { FluentBundle, FluentResource } from "@fluent/bundle"; /** - * @import { Message, Pattern } from "@fluent/bundle/esm/ast.d.ts" + * @import { Pattern} from "@fluent/bundle/esm/ast.d.ts" + * @import { FluentVariable, Message } from "@fluent/bundle" */ export class Localization { /** @@ -42,4 +43,169 @@ export class Localization { actual: JSON.stringify(actualType) }); } + + /** @type (limit: number) => string */ + getMinLengthErrorMessage(limit) { + const message =/** @type Message */ (this.bundle.getMessage("min-length-error")); + return this.bundle.formatPattern(/** @type Pattern */ (message.value), { limit }); + } + + /** @type (limit: number) => string */ + getMaxLengthErrorMessage(limit) { + const message =/** @type Message */ (this.bundle.getMessage("max-length-error")); + return this.bundle.formatPattern(/** @type Pattern */ (message.value), { limit }); + } + + /** @type (limit: number) => string */ + getMaximumErrorMessage(limit) { + const message =/** @type Message */ (this.bundle.getMessage("maximum-error")); + return this.bundle.formatPattern(/** @type Pattern */ (message.value), { limit }); + } + + /** @type (limit: number) => string */ + getMinimumErrorMessage(limit) { + const message =/** @type Message */ (this.bundle.getMessage("minimum-error")); + return this.bundle.formatPattern(/** @type Pattern */ (message.value), { limit }); + } + + /** @type (limit: number) => string */ + getExclusiveMinimumErrorMessage(limit) { + const message =/** @type Message */ (this.bundle.getMessage("exclusive-minimum-error")); + return this.bundle.formatPattern(/** @type Pattern */ (message.value), { limit }); + } + + /** @type (limit: number) => string */ + getExclusiveMaximumErrorMessage(limit) { + const message =/** @type Message */ (this.bundle.getMessage("exclusive-maximum-error")); + return this.bundle.formatPattern(/** @type Pattern */ (message.value), { limit }); + } + + /** @type (instanceLocation: string, missingProperties: string | string[]) => string */ + getRequiredErrorMessage(instanceLocation, missingProperties) { + const requiredList = new Intl.ListFormat(this.locale, { type: "conjunction" }).format(missingProperties); + const message =/** @type Message */ (this.bundle.getMessage("required-error")); + return this.bundle.formatPattern(/** @type Pattern */ (message.value), { + instanceLocation, + missingProperties: requiredList + }); + } + + /** @type (divisor: number) => string */ + getMultipleOfErrorMessage(divisor) { + const message =/** @type Message */ (this.bundle.getMessage("multiple-of-error")); + return this.bundle.formatPattern(/** @type Pattern */ (message.value), { divisor }); + } + + /** @type (limit: number) => string */ + getMaxPropertiesErrorMessage(limit) { + const message =/** @type Message */ (this.bundle.getMessage("max-properties-error")); + return this.bundle.formatPattern(/** @type Pattern */ (message.value), { limit }); + } + + /** @type (limit: number) => string */ + getMinPropertiesErrorMessage(limit) { + const message =/** @type Message */ (this.bundle.getMessage("min-properties-error")); + return this.bundle.formatPattern(/** @type Pattern */ (message.value), { limit }); + } + + /** @type (expectedValue: FluentVariable) => string */ + getConstErrorMessage(expectedValue) { + const message =/** @type Message */ (this.bundle.getMessage("const-error")); // type doubt here + return this.bundle.formatPattern(/** @type Pattern */ (message.value), { expectedValue }); + } + + /** @type (limit: number) => string */ + getMaxItemsErrorMessage(limit) { + const message =/** @type Message */ (this.bundle.getMessage("max-items-error")); + return this.bundle.formatPattern(/** @type Pattern */ (message.value), { limit }); + } + + /** @type (limit: number) => string */ + getMinItemsErrorMessage(limit) { + const message =/** @type Message */ (this.bundle.getMessage("min-items-error")); + return this.bundle.formatPattern(/** @type Pattern */ (message.value), { limit }); + } + + /** @type () => string */ + getUniqueItemsErrorMessage() { + const message =/** @type Message */ (this.bundle.getMessage("unique-items-error")); + return this.bundle.formatPattern(/** @type Pattern */ (message.value)); + } + + /** @type (format: string) => string */ + getFormatErrorMessage(format) { + const message =/** @type Message */ (this.bundle.getMessage("format-error")); + return this.bundle.formatPattern(/** @type Pattern */ (message.value), { format }); + } + + /** @type (pattern: string) => string */ + getPatternErrorMessage(pattern) { + const message =/** @type Message */ (this.bundle.getMessage("pattern-error")); + return this.bundle.formatPattern(/** @type Pattern */ (message.value), { pattern }); + } + + /** @type () => string */ + getContainsErrorMessage() { + const message =/** @type Message */ (this.bundle.getMessage("contains-error")); + return this.bundle.formatPattern(/** @type Pattern */ (message.value)); + } + + /** @type () => string */ + getNotErrorMessage() { + const message =/** @type Message */ (this.bundle.getMessage("not-error")); + return this.bundle.formatPattern(/** @type Pattern */ (message.value)); + } + + /** @type (propertyName: string) => string */ + getAdditionalPropertiesErrorMessage(propertyName) { + const message =/** @type Message */ (this.bundle.getMessage("additional-properties-error")); + return this.bundle.formatPattern(/** @type Pattern */ (message.value), { propertyName }); + } + + /** @type (property: string, missingDependents: string | string[]) => string */ + getDependentRequiredErrorMessage(property, missingDependents) { + const dependentsList = new Intl.ListFormat(this.locale, { type: "conjunction" }).format(missingDependents); + const message =/** @type Message */ (this.bundle.getMessage("dependent-required-error")); + return this.bundle.formatPattern(/** @type Pattern */ (message.value), { + property, + missingDependents: dependentsList + }); + } + + /** + * @typedef {Object} EnumSuggestionArgs + * @property {"suggestion"} variant + * @property {string} instanceValue + * @property {string} suggestion + */ + + /** + * @typedef {Object} EnumFallbackArgs + * @property {"fallback"} variant + * @property {string} instanceValue + * @property {string[]} allowedValues + */ + + /** + * @param {EnumSuggestionArgs | EnumFallbackArgs} args + * @returns {string} + */ + getEnumErrorMessage(args) { + const message = /** @type Message */ (this.bundle.getMessage("enum-error")); + if (args.variant === "fallback") { + const quotedValues = args.allowedValues.map((value) => JSON.stringify(value)); + const formattedList = new Intl.ListFormat(this.locale, { type: "disjunction" }).format(quotedValues); + return this.bundle.formatPattern(/** @type Pattern */ (message.value), { + variant: "fallback", + instanceValue: `"${args.instanceValue}"`, + allowedValues: formattedList + }); + } else { + return this.bundle.formatPattern(/** @type Pattern */ (message.value), { + variant: "suggestion", + instanceValue: `"${args.instanceValue}"`, + suggestion: args.suggestion + }); + } + } } diff --git a/src/normalizeOutputFormat/normalizeOutput.js b/src/normalizeOutputFormat/normalizeOutput.js index 7724659..07cf3d6 100644 --- a/src/normalizeOutputFormat/normalizeOutput.js +++ b/src/normalizeOutputFormat/normalizeOutput.js @@ -300,6 +300,37 @@ keywordHandlers["https://json-schema.org/keyword/additionalProperties"] = { simpleApplicator: true }; +keywordHandlers["https://json-schema.org/keyword/unevaluatedItems"] = { + evaluate(/** @type string[] */ [, unevaluatedItemsSchemaLocation], ast, instance, errorIndex) { + /** @type NormalizedOutput[] */ + const outputs = []; + if (Instance.typeOf(instance) !== "array") { + return outputs; + } + for (const itemNode of Instance.iter(instance)) { + outputs.push(evaluateSchema(unevaluatedItemsSchemaLocation, ast, itemNode, errorIndex)); + } + return outputs; + }, + simpleApplicator: true +}; + +keywordHandlers["https://json-schema.org/keyword/unevaluatedProperties"] = { + evaluate(/** @type string[] */ [, unevaluatedPropertiesSchemaLocation], ast, instance, errorIndex) { + /** @type NormalizedOutput[] */ + const outputs = []; + if (Instance.typeOf(instance) !== "object") { + return outputs; + } + + for (const [, propertyValue] of Instance.entries(instance)) { + outputs.push(evaluateSchema(unevaluatedPropertiesSchemaLocation, ast, propertyValue, errorIndex)); + } + return outputs; + }, + simpleApplicator: true +}; + keywordHandlers["https://json-schema.org/keyword/definitions"] = { appliesTo() { return false; diff --git a/src/normalizeOutputFormat/normalizeOutput.test.js b/src/normalizeOutputFormat/normalizeOutput.test.js index c8c1cb4..36648e0 100644 --- a/src/normalizeOutputFormat/normalizeOutput.test.js +++ b/src/normalizeOutputFormat/normalizeOutput.test.js @@ -2,11 +2,13 @@ import { afterEach, describe, expect, test } from "vitest"; import { normalizedErrorOuput } from "./normalizeOutput.js"; import { betterJsonSchemaErrors } from "../index.js"; import { registerSchema, unregisterSchema } from "@hyperjump/json-schema/draft-2020-12"; +import { Localization } from "../localization.js"; /** * @import { OutputFormat} from "../index.d.ts" */ -describe("Error Output Normalization", () => { +describe("Error Output Normalization", async () => { + const localization = await Localization.forLocale("en-US"); const schemaUri = "https://example.com/main"; const schemaUri1 = "https://example.com/polygon"; @@ -38,7 +40,7 @@ describe("Error Output Normalization", () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/minLength", instanceLocation: "#", - message: "The instance should be at least 3 characters" + message: localization.getMinLengthErrorMessage(3) } ]); }); @@ -66,7 +68,7 @@ describe("Error Output Normalization", () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/minLength", instanceLocation: "#", - message: "The instance should be at least 3 characters" + message: localization.getMinLengthErrorMessage(3) }]); }); @@ -93,7 +95,7 @@ describe("Error Output Normalization", () => { expect(result.errors).to.eql([{ schemaLocation: "https://example.com/main#/minLength", instanceLocation: "#", - message: "The instance should be at least 3 characters" + message: localization.getMinLengthErrorMessage(3) }]); }); @@ -367,7 +369,7 @@ describe("Error Output Normalization", () => { { schemaLocation: "https://example.com/main#/$defs/lengthDefinition/minLength", instanceLocation: "#/foo", - message: "The instance should be at least 3 characters" + message: localization.getMinLengthErrorMessage(3) } ]); }); diff --git a/src/translations/en-US.ftl b/src/translations/en-US.ftl index e25acf3..7fbdcac 100644 --- a/src/translations/en-US.ftl +++ b/src/translations/en-US.ftl @@ -1 +1,25 @@ -type-error = The instance should be of type {$expected} but found {$actual}. \ No newline at end of file +type-error = The instance should be of type {$expected} but found {$actual}. +min-length-error = The instance should be atleast {$limit} characters. +max-length-error = The instance should be atmost {$limit} characters long. +maximum-error = The instance should be less than or equal to {$limit}. +minimum-error = The instance should be greater than or equal to {$limit}. +exclusive-maximum-error = The instance should be less than {$limit}. +exclusive-minimum-error = The instance should be greater than {$limit}. +required-error = "{$instanceLocation}" is missing required property(s): {$missingProperties}. +multiple-of-error = The instance should be a multiple of {$divisor}. +max-properties-error = The instance should have a maximum of {$limit} properties. +min-properties-error = The instance should have a minimum of {$limit} properties. +const-error = The instance should be equal to {$expectedValue}. +max-items-error = The instance should contain a maximum of {$limit} items in the array. +min-items-error = The instance should contain a minimum of {$limit} items in the array. +unique-items-error = The instance should have unique items in the array. +format-error = The instance should match the format: {$format}. +pattern-error = The instance should match the pattern: {$pattern}. +contains-error = A required value is missing from the list. +not-error = The instance is not allowed to be used in this schema. +additional-properties-error = The property "{$propertyName}" is not allowed. +dependent-required-error = Property "{$property}" requires property(s): {$missingDependents}. +enum-error = { $variant -> + [suggestion] Unexpected value {$instanceValue}. Did you mean {$suggestion}? + *[fallback] Unexpected value {$instanceValue}. Expected one of: {$allowedValues}. +} \ No newline at end of file From 061d229dfbc7405b724fed645bb481e5e6236d3e Mon Sep 17 00:00:00 2001 From: Arpit Kuriyal Date: Tue, 5 Aug 2025 23:13:18 +0530 Subject: [PATCH 2/2] remove unevaluatedKeywords --- src/keywordErrorMessage.test.js | 65 -------------------- src/normalizeOutputFormat/normalizeOutput.js | 31 ---------- 2 files changed, 96 deletions(-) diff --git a/src/keywordErrorMessage.test.js b/src/keywordErrorMessage.test.js index 71af1c9..7d2e280 100644 --- a/src/keywordErrorMessage.test.js +++ b/src/keywordErrorMessage.test.js @@ -1172,69 +1172,4 @@ describe("Error messages", async () => { } ]); }); - - test("should fail when an unevaluated item has the wrong type", async () => { - registerSchema({ - $schema: "https://json-schema.org/draft/2020-12/schema", - prefixItems: [{ type: "number" }], - unevaluatedItems: { type: "string" } - }, schemaUri); - - const instance = [1, "two", false]; - - /** @type OutputFormat */ - const output = { - valid: false, - errors: [ - { - absoluteKeywordLocation: "https://example.com/main#/unevaluatedItems/type", - instanceLocation: "#/2" - } - ] - }; - - const result = await betterJsonSchemaErrors(instance, output, schemaUri); - expect(result.errors).to.eql([ - { - schemaLocation: "https://example.com/main#/unevaluatedItems/type", - instanceLocation: "#/2", - message: localization.getTypeErrorMessage(["string"], "boolean") - } - ]); - }); - - test("should fail when an unevaluated property has the wrong type", async () => { - registerSchema({ - $schema: "https://json-schema.org/draft/2020-12/schema", - properties: { - known: { type: "string" } - }, - unevaluatedProperties: { type: "number" } - }, schemaUri); - - const instance = { - known: "a string", - unknown: "this should have been a number" - }; - - /** @type OutputFormat */ - const output = { - valid: false, - errors: [ - { - absoluteKeywordLocation: "https://example.com/main#/unevaluatedProperties/type", - instanceLocation: "#/unknown" - } - ] - }; - - const result = await betterJsonSchemaErrors(instance, output, schemaUri); - expect(result.errors).to.eql([ - { - schemaLocation: "https://example.com/main#/unevaluatedProperties/type", - instanceLocation: "#/unknown", - message: localization.getTypeErrorMessage("number", "string") - } - ]); - }); }); diff --git a/src/normalizeOutputFormat/normalizeOutput.js b/src/normalizeOutputFormat/normalizeOutput.js index 07cf3d6..7724659 100644 --- a/src/normalizeOutputFormat/normalizeOutput.js +++ b/src/normalizeOutputFormat/normalizeOutput.js @@ -300,37 +300,6 @@ keywordHandlers["https://json-schema.org/keyword/additionalProperties"] = { simpleApplicator: true }; -keywordHandlers["https://json-schema.org/keyword/unevaluatedItems"] = { - evaluate(/** @type string[] */ [, unevaluatedItemsSchemaLocation], ast, instance, errorIndex) { - /** @type NormalizedOutput[] */ - const outputs = []; - if (Instance.typeOf(instance) !== "array") { - return outputs; - } - for (const itemNode of Instance.iter(instance)) { - outputs.push(evaluateSchema(unevaluatedItemsSchemaLocation, ast, itemNode, errorIndex)); - } - return outputs; - }, - simpleApplicator: true -}; - -keywordHandlers["https://json-schema.org/keyword/unevaluatedProperties"] = { - evaluate(/** @type string[] */ [, unevaluatedPropertiesSchemaLocation], ast, instance, errorIndex) { - /** @type NormalizedOutput[] */ - const outputs = []; - if (Instance.typeOf(instance) !== "object") { - return outputs; - } - - for (const [, propertyValue] of Instance.entries(instance)) { - outputs.push(evaluateSchema(unevaluatedPropertiesSchemaLocation, ast, propertyValue, errorIndex)); - } - return outputs; - }, - simpleApplicator: true -}; - keywordHandlers["https://json-schema.org/keyword/definitions"] = { appliesTo() { return false;