From 5497661c0a203f2c939b59534fa5656e4a04eee9 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Sat, 25 Apr 2026 01:19:27 +0300 Subject: [PATCH 1/6] adding JS module instead reading ftl files for sync pipeline --- src/localization.js | 11 +++++--- src/localization.test.js | 20 ++++----------- src/translations.js | 55 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 19 deletions(-) create mode 100644 src/translations.js diff --git a/src/localization.js b/src/localization.js index e810d76..533d2b1 100644 --- a/src/localization.js +++ b/src/localization.js @@ -1,4 +1,4 @@ -import { readFile } from "node:fs/promises"; +import { translations } from "./translations.js"; import { FluentBundle, FluentResource } from "@fluent/bundle"; /** @@ -21,11 +21,14 @@ export class Localization { this.conjunction = new Intl.ListFormat(this.locale, { type: "conjunction" }); } - /** @type (locale: string) => Promise */ - static async forLocale(locale) { + /** @type (locale: string) => Localization */ + static forLocale(locale) { if (!localizationCache.has(locale)) { try { - const ftl = await readFile(`${import.meta.dirname}/translations/${locale}.ftl`, "utf-8"); + const ftl = translations[locale]; + if (!ftl) { + throw Error(`No translation found for the ${locale} locale.`); + } const resource = new FluentResource(ftl); const bundle = new FluentBundle(locale); bundle.addResource(resource); diff --git a/src/localization.test.js b/src/localization.test.js index 388e47c..c873dc6 100644 --- a/src/localization.test.js +++ b/src/localization.test.js @@ -1,25 +1,15 @@ -import { afterEach, beforeEach, describe, expect, test } from "vitest"; -import { rm, writeFile } from "node:fs/promises"; +import { describe, expect, test } from "vitest"; import { Localization } from "./localization.js"; describe("Localization", () => { const fixtureLocale = "fx-TR"; - beforeEach(async () => { - await writeFile(`src/translations/${fixtureLocale}.ftl`, "example = message"); + test("unsupported locale", () => { + expect(() => Localization.forLocale("xx-XX")).to.throw(Error); }); - afterEach(async () => { - await rm(`src/translations/${fixtureLocale}.ftl`); - }); - - test("unsupported locale", async () => { - const localization = Localization.forLocale("xx-XX"); - await expect(localization).rejects.to.throw(Error); - }); - - test("unsupported message", async () => { - const localization = await Localization.forLocale(fixtureLocale); + test("unsupported message", () => { + const localization = Localization.forLocale(fixtureLocale); expect(() => localization.getBooleanSchemaErrorMessage()).to.throw(Error); }); }); diff --git a/src/translations.js b/src/translations.js new file mode 100644 index 0000000..7a8cd97 --- /dev/null +++ b/src/translations.js @@ -0,0 +1,55 @@ +/** @type Record */ +export const translations = { + "en-US": ` +// Any type keywords +boolean-schema-message = A value is not allowed here +type-message = Expected a {$expectedTypes} +const-message = Expected exactly {$expected} +enum-message = Expected one of {$expected} +format-message = Expected a value matching the '{$format}' format +unknown-message = Validation failed for '{$keyword}' + +// Number keywords +exclusiveMaximum-message = Expected a number less than {$exclusiveMaximum} +exclusiveMinimum-message = Expected a number greater than {$exclusiveMinimum} +maximum-message = Expected a number less than or equal to {$maximum} +minimum-message = Expected a number greater than or equal to {$minimum} +multipleOf-message = Expected a number that is a multiple of {$multipleOf} + +// String keywords +maxLength-message = Expected a string with no more than {$maxLength} characters +minLength-message = Expected a string with at least {$minLength} characters +pattern-message = Expected a string matching the regular expression /{$pattern}/ + +// Array keywords +maxItems-message = Expected an array with no more than {$maxItems} items +minItems-message = Expected an array with at least {$minItems} items +contains-message = Expected an array that contains {$minContains -> + [1] at least one item matching + *[other] at least {$minContains} items matching +} the 'contains' schema +contains-range-message = Expected an array containing between {$minContains} and {$maxContains} items matching the 'contains' schema +contains-exact-message = Expected an array containing {$minContains -> + [1] exactly one item matching + *[other] exactly {$minContains} items matching +} the 'contains' schema +uniqueItems-message = Array items must be unique + +// Object keywords +maxProperties-message = Expected an object with no more than {$maxProperties} properties +minProperties-message = Expected an object with at least {$minProperties} properties +required-message = Missing required {$count -> + [one] property: {$required} + *[other] properties: {$required} +} + +// Applicators +anyOf-message = Expected the value to match at least one alternative +oneOf-message = Expected the value to match exactly one alternative, {$matchCount -> + [0] but none + *[other] but more than one +} matched +not-message = Expected a value that doesn't match the 'not' schema +`, + "fx-TR": `test = unsupported locale` +}; From b589b7c45573fedccda58b4b637a0493109fa988 Mon Sep 17 00:00:00 2001 From: Ahmed Date: Sat, 25 Apr 2026 01:21:50 +0300 Subject: [PATCH 2/6] fixing oxlint warnings --- src/json-schema-errors.js | 4 ++-- src/normalization.test.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/json-schema-errors.js b/src/json-schema-errors.js index da0bcd2..be74ab3 100644 --- a/src/json-schema-errors.js +++ b/src/json-schema-errors.js @@ -24,7 +24,7 @@ export const jsonSchemaErrors = async (errorOutput, schemaUri, instance, options errorIndex, plugins: [...ast.plugins] }); - const localization = await Localization.forLocale(options.locale ?? "en-US"); + const localization = Localization.forLocale(options.locale ?? "en-US"); return getErrors(normalizedErrors, rootInstance, localization, ast); }; @@ -256,7 +256,7 @@ export const validate = async (schemaUri, instance, options) => { /** @type (compiledSchema: CompiledSchema, instance: API.Json, options?: API.ValidationOptions) => Promise */ const evaluateCompiledSchema = async (compiledSchema, instance, options = {}) => { - const localization = await Localization.forLocale(options.locale ?? "en-US"); + const localization = Localization.forLocale(options.locale ?? "en-US"); const jsonNode = Instance.fromJs(instance); const outputPlugin = new JsonSchemaErrorsOutputPlugin(); const context = { diff --git a/src/normalization.test.js b/src/normalization.test.js index 1459330..e43e314 100644 --- a/src/normalization.test.js +++ b/src/normalization.test.js @@ -9,7 +9,7 @@ import { Localization } from "./localization.js"; describe("Normalization", async () => { const schemaUri = "https://example.com/main"; - const localization = await Localization.forLocale("en-US"); + const localization = Localization.forLocale("en-US"); afterEach(() => { unregisterSchema(schemaUri); From 8e7e08e9f60d58d5510d96c63f3cee5d0795cd5d Mon Sep 17 00:00:00 2001 From: Ahmed Date: Sat, 25 Apr 2026 01:24:58 +0300 Subject: [PATCH 3/6] removing ftl files and rely completely on JS module --- src/hyperjump-json-schema.test.js | 5 ++-- src/translations/en-US.ftl | 49 ------------------------------- 2 files changed, 3 insertions(+), 51 deletions(-) delete mode 100644 src/translations/en-US.ftl diff --git a/src/hyperjump-json-schema.test.js b/src/hyperjump-json-schema.test.js index 45cbd1c..47c8590 100644 --- a/src/hyperjump-json-schema.test.js +++ b/src/hyperjump-json-schema.test.js @@ -14,6 +14,7 @@ import "@hyperjump/json-schema/formats"; import { BASIC } from "@hyperjump/json-schema/experimental"; import { jsonSchemaErrors } from "../src/index.js"; import { FluentBundle, FluentResource } from "@fluent/bundle"; +import { translations } from "./translations.js"; /** * @import { SchemaObject } from "@hyperjump/json-schema" @@ -153,8 +154,8 @@ const isCompatible = (compatibility, versionUnderTest) => { }; /** @type (messageId: string, messageParams: MessageParams) => string */ -const getMessage = await (async function () { - const ftl = await readFile(`${import.meta.dirname}/translations/en-US.ftl`, "utf-8"); +const getMessage = (function () { + const ftl = translations["en-US"]; const resource = new FluentResource(ftl); const bundle = new FluentBundle("en-US"); bundle.addResource(resource); diff --git a/src/translations/en-US.ftl b/src/translations/en-US.ftl deleted file mode 100644 index 4999fbf..0000000 --- a/src/translations/en-US.ftl +++ /dev/null @@ -1,49 +0,0 @@ -// Any type keywords -boolean-schema-message = A value is not allowed here -type-message = Expected a {$expectedTypes} -const-message = Expected exactly {$expected} -enum-message = Expected one of {$expected} -format-message = Expected a value matching the '{$format}' format -unknown-message = Validation failed for '{$keyword}' - -// Number keywords -exclusiveMaximum-message = Expected a number less than {$exclusiveMaximum} -exclusiveMinimum-message = Expected a number greater than {$exclusiveMinimum} -maximum-message = Expected a number less than or equal to {$maximum} -minimum-message = Expected a number greater than or equal to {$minimum} -multipleOf-message = Expected a number that is a multiple of {$multipleOf} - -// String keywords -maxLength-message = Expected a string with no more than {$maxLength} characters -minLength-message = Expected a string with at least {$minLength} characters -pattern-message = Expected a string matching the regular expression /{$pattern}/ - -// Array keywords -maxItems-message = Expected an array with no more than {$maxItems} items -minItems-message = Expected an array with at least {$minItems} items -contains-message = Expected an array that contains {$minContains -> - [1] at least one item matching - *[other] at least {$minContains} items matching -} the 'contains' schema -contains-range-message = Expected an array containing between {$minContains} and {$maxContains} items matching the 'contains' schema -contains-exact-message = Expected an array containing {$minContains -> - [1] exactly one item matching - *[other] exactly {$minContains} items matching -} the 'contains' schema -uniqueItems-message = Array items must be unique - -// Object keywords -maxProperties-message = Expected an object with no more than {$maxProperties} properties -minProperties-message = Expected an object with at least {$minProperties} properties -required-message = Missing required {$count -> - [one] property: {$required} - *[other] properties: {$required} -} - -// Applicators -anyOf-message = Expected the value to match at least one alternative -oneOf-message = Expected the value to match exactly one alternative, {$matchCount -> - [0] but none - *[other] but more than one -} matched -not-message = Expected a value that doesn't match the 'not' schema From 840818ef3bcf1c7af288893db2646d9072c77b5d Mon Sep 17 00:00:00 2001 From: Ahmed Date: Mon, 27 Apr 2026 19:26:44 +0300 Subject: [PATCH 4/6] isolating locales & importing them for translations map --- src/translations.js | 57 ++++----------------------------------- src/translations/en-US.js | 51 +++++++++++++++++++++++++++++++++++ src/translations/fx-TR.js | 3 +++ 3 files changed, 59 insertions(+), 52 deletions(-) create mode 100644 src/translations/en-US.js create mode 100644 src/translations/fx-TR.js diff --git a/src/translations.js b/src/translations.js index 7a8cd97..7415711 100644 --- a/src/translations.js +++ b/src/translations.js @@ -1,55 +1,8 @@ +import { enUS } from "./translations/en-US.js"; +import { fxTR } from "./translations/fx-TR.js"; + /** @type Record */ export const translations = { - "en-US": ` -// Any type keywords -boolean-schema-message = A value is not allowed here -type-message = Expected a {$expectedTypes} -const-message = Expected exactly {$expected} -enum-message = Expected one of {$expected} -format-message = Expected a value matching the '{$format}' format -unknown-message = Validation failed for '{$keyword}' - -// Number keywords -exclusiveMaximum-message = Expected a number less than {$exclusiveMaximum} -exclusiveMinimum-message = Expected a number greater than {$exclusiveMinimum} -maximum-message = Expected a number less than or equal to {$maximum} -minimum-message = Expected a number greater than or equal to {$minimum} -multipleOf-message = Expected a number that is a multiple of {$multipleOf} - -// String keywords -maxLength-message = Expected a string with no more than {$maxLength} characters -minLength-message = Expected a string with at least {$minLength} characters -pattern-message = Expected a string matching the regular expression /{$pattern}/ - -// Array keywords -maxItems-message = Expected an array with no more than {$maxItems} items -minItems-message = Expected an array with at least {$minItems} items -contains-message = Expected an array that contains {$minContains -> - [1] at least one item matching - *[other] at least {$minContains} items matching -} the 'contains' schema -contains-range-message = Expected an array containing between {$minContains} and {$maxContains} items matching the 'contains' schema -contains-exact-message = Expected an array containing {$minContains -> - [1] exactly one item matching - *[other] exactly {$minContains} items matching -} the 'contains' schema -uniqueItems-message = Array items must be unique - -// Object keywords -maxProperties-message = Expected an object with no more than {$maxProperties} properties -minProperties-message = Expected an object with at least {$minProperties} properties -required-message = Missing required {$count -> - [one] property: {$required} - *[other] properties: {$required} -} - -// Applicators -anyOf-message = Expected the value to match at least one alternative -oneOf-message = Expected the value to match exactly one alternative, {$matchCount -> - [0] but none - *[other] but more than one -} matched -not-message = Expected a value that doesn't match the 'not' schema -`, - "fx-TR": `test = unsupported locale` + "en-US": enUS, + "fx-TR": fxTR }; diff --git a/src/translations/en-US.js b/src/translations/en-US.js new file mode 100644 index 0000000..4b00d9f --- /dev/null +++ b/src/translations/en-US.js @@ -0,0 +1,51 @@ +export const enUS = ` +// Any type keywords +boolean-schema-message = A value is not allowed here +type-message = Expected a {$expectedTypes} +const-message = Expected exactly {$expected} +enum-message = Expected one of {$expected} +format-message = Expected a value matching the '{$format}' format +unknown-message = Validation failed for '{$keyword}' + +// Number keywords +exclusiveMaximum-message = Expected a number less than {$exclusiveMaximum} +exclusiveMinimum-message = Expected a number greater than {$exclusiveMinimum} +maximum-message = Expected a number less than or equal to {$maximum} +minimum-message = Expected a number greater than or equal to {$minimum} +multipleOf-message = Expected a number that is a multiple of {$multipleOf} + +// String keywords +maxLength-message = Expected a string with no more than {$maxLength} characters +minLength-message = Expected a string with at least {$minLength} characters +pattern-message = Expected a string matching the regular expression /{$pattern}/ + +// Array keywords +maxItems-message = Expected an array with no more than {$maxItems} items +minItems-message = Expected an array with at least {$minItems} items +contains-message = Expected an array that contains {$minContains -> + [1] at least one item matching + *[other] at least {$minContains} items matching +} the 'contains' schema +contains-range-message = Expected an array containing between {$minContains} and {$maxContains} items matching the 'contains' schema +contains-exact-message = Expected an array containing {$minContains -> + [1] exactly one item matching + *[other] exactly {$minContains} items matching +} the 'contains' schema +uniqueItems-message = Array items must be unique + +// Object keywords +maxProperties-message = Expected an object with no more than {$maxProperties} properties +minProperties-message = Expected an object with at least {$minProperties} properties +required-message = Missing required {$count -> + [one] property: {$required} + *[other] properties: {$required} +} + +// Applicators +anyOf-message = Expected the value to match at least one alternative +oneOf-message = Expected the value to match exactly one alternative, {$matchCount -> + [0] but none + *[other] but more than one +} matched +not-message = Expected a value that doesn't match the 'not' schema +`; diff --git a/src/translations/fx-TR.js b/src/translations/fx-TR.js new file mode 100644 index 0000000..201a722 --- /dev/null +++ b/src/translations/fx-TR.js @@ -0,0 +1,3 @@ +export const fxTR = ` +test = unsupported locale +`; From 9f8019c0ee6babece33b0baf46c6158e8053da0b Mon Sep 17 00:00:00 2001 From: Ahmed Date: Wed, 29 Apr 2026 01:11:37 +0300 Subject: [PATCH 5/6] abstracting testing details --- src/localization.test.js | 11 ++++++++++- src/translations.js | 4 +--- src/translations/fx-TR.js | 3 --- 3 files changed, 11 insertions(+), 7 deletions(-) delete mode 100644 src/translations/fx-TR.js diff --git a/src/localization.test.js b/src/localization.test.js index c873dc6..50b1def 100644 --- a/src/localization.test.js +++ b/src/localization.test.js @@ -1,9 +1,18 @@ -import { describe, expect, test } from "vitest"; +import { beforeAll, afterAll, describe, expect, test } from "vitest"; +import { translations } from "./translations.js"; import { Localization } from "./localization.js"; describe("Localization", () => { const fixtureLocale = "fx-TR"; + beforeAll(() => { + translations[fixtureLocale] = `test = unsupported locale`; + }); + + afterAll(() => { + delete translations[fixtureLocale]; + }); + test("unsupported locale", () => { expect(() => Localization.forLocale("xx-XX")).to.throw(Error); }); diff --git a/src/translations.js b/src/translations.js index 7415711..c76cb9c 100644 --- a/src/translations.js +++ b/src/translations.js @@ -1,8 +1,6 @@ import { enUS } from "./translations/en-US.js"; -import { fxTR } from "./translations/fx-TR.js"; /** @type Record */ export const translations = { - "en-US": enUS, - "fx-TR": fxTR + "en-US": enUS }; diff --git a/src/translations/fx-TR.js b/src/translations/fx-TR.js deleted file mode 100644 index 201a722..0000000 --- a/src/translations/fx-TR.js +++ /dev/null @@ -1,3 +0,0 @@ -export const fxTR = ` -test = unsupported locale -`; From 3491b4d8c593cb3320ca60abda193e70db9762bf Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Wed, 29 Apr 2026 11:21:40 -0700 Subject: [PATCH 6/6] A few style tweaks --- src/hyperjump-json-schema.test.js | 2 +- src/index.d.ts | 4 ++-- src/json-schema-errors.js | 4 ++-- src/localization.js | 20 ++++++++------------ src/localization.test.js | 2 +- src/normalization.test.js | 2 +- src/translations.js | 6 ------ src/translations/en-US.js | 2 +- src/translations/index.js | 6 ++++++ 9 files changed, 22 insertions(+), 26 deletions(-) delete mode 100644 src/translations.js create mode 100644 src/translations/index.js diff --git a/src/hyperjump-json-schema.test.js b/src/hyperjump-json-schema.test.js index 47c8590..7ac8f4c 100644 --- a/src/hyperjump-json-schema.test.js +++ b/src/hyperjump-json-schema.test.js @@ -14,7 +14,7 @@ import "@hyperjump/json-schema/formats"; import { BASIC } from "@hyperjump/json-schema/experimental"; import { jsonSchemaErrors } from "../src/index.js"; import { FluentBundle, FluentResource } from "@fluent/bundle"; -import { translations } from "./translations.js"; +import { translations } from "./translations/index.js"; /** * @import { SchemaObject } from "@hyperjump/json-schema" diff --git a/src/index.d.ts b/src/index.d.ts index 9f3e106..2cb2687 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -172,10 +172,10 @@ export type ContainsRange = { export const validate: ( (schemaUri: string) => Promise ) & ( - (schemaUri: string, instance: Json, options?: ValidationOptions) => Promise + (schemaUri: string, instance: Json, options?: ValidationOptions) => ValidationResult ); -export type EvaluateInstance = (instance: Json, options?: ValidationOptions) => Promise; +export type EvaluateInstance = (instance: Json, options?: ValidationOptions) => ValidationResult; export type ValidationOptions = { /** diff --git a/src/json-schema-errors.js b/src/json-schema-errors.js index be74ab3..66ed333 100644 --- a/src/json-schema-errors.js +++ b/src/json-schema-errors.js @@ -254,8 +254,8 @@ export const validate = async (schemaUri, instance, options) => { } }; -/** @type (compiledSchema: CompiledSchema, instance: API.Json, options?: API.ValidationOptions) => Promise */ -const evaluateCompiledSchema = async (compiledSchema, instance, options = {}) => { +/** @type (compiledSchema: CompiledSchema, instance: API.Json, options?: API.ValidationOptions) => API.ValidationResult */ +const evaluateCompiledSchema = (compiledSchema, instance, options = {}) => { const localization = Localization.forLocale(options.locale ?? "en-US"); const jsonNode = Instance.fromJs(instance); const outputPlugin = new JsonSchemaErrorsOutputPlugin(); diff --git a/src/localization.js b/src/localization.js index 533d2b1..f6abc3c 100644 --- a/src/localization.js +++ b/src/localization.js @@ -1,4 +1,4 @@ -import { translations } from "./translations.js"; +import { translations } from "./translations/index.js"; import { FluentBundle, FluentResource } from "@fluent/bundle"; /** @@ -24,18 +24,14 @@ export class Localization { /** @type (locale: string) => Localization */ static forLocale(locale) { if (!localizationCache.has(locale)) { - try { - const ftl = translations[locale]; - if (!ftl) { - throw Error(`No translation found for the ${locale} locale.`); - } - const resource = new FluentResource(ftl); - const bundle = new FluentBundle(locale); - bundle.addResource(resource); - localizationCache.set(locale, new Localization(locale, bundle)); - } catch (error) { - throw Error(`The ${locale} locale is not supported.`, { cause: error }); + const ftl = translations[locale]; + if (!ftl) { + throw Error(`The ${locale} locale is not supported.`); } + const resource = new FluentResource(ftl); + const bundle = new FluentBundle(locale); + bundle.addResource(resource); + localizationCache.set(locale, new Localization(locale, bundle)); } return /** @type Localization */ (localizationCache.get(locale)); diff --git a/src/localization.test.js b/src/localization.test.js index 50b1def..56bce3d 100644 --- a/src/localization.test.js +++ b/src/localization.test.js @@ -1,5 +1,5 @@ import { beforeAll, afterAll, describe, expect, test } from "vitest"; -import { translations } from "./translations.js"; +import { translations } from "./translations/index.js"; import { Localization } from "./localization.js"; describe("Localization", () => { diff --git a/src/normalization.test.js b/src/normalization.test.js index e43e314..95131b2 100644 --- a/src/normalization.test.js +++ b/src/normalization.test.js @@ -7,7 +7,7 @@ import { Localization } from "./localization.js"; * @import { OutputFormat } from "./index.js"; */ -describe("Normalization", async () => { +describe("Normalization", () => { const schemaUri = "https://example.com/main"; const localization = Localization.forLocale("en-US"); diff --git a/src/translations.js b/src/translations.js deleted file mode 100644 index c76cb9c..0000000 --- a/src/translations.js +++ /dev/null @@ -1,6 +0,0 @@ -import { enUS } from "./translations/en-US.js"; - -/** @type Record */ -export const translations = { - "en-US": enUS -}; diff --git a/src/translations/en-US.js b/src/translations/en-US.js index 4b00d9f..731cc41 100644 --- a/src/translations/en-US.js +++ b/src/translations/en-US.js @@ -1,4 +1,4 @@ -export const enUS = ` +export default ` // Any type keywords boolean-schema-message = A value is not allowed here type-message = Expected a {$expectedTypes} diff --git a/src/translations/index.js b/src/translations/index.js new file mode 100644 index 0000000..ce6443e --- /dev/null +++ b/src/translations/index.js @@ -0,0 +1,6 @@ +import enUS from "./en-US.js"; + +/** @type Record */ +export const translations = { + "en-US": enUS +};