From cecba5f8d795cf17807a8a321c2e03bb89c5e2cb Mon Sep 17 00:00:00 2001 From: Jason Desrosiers Date: Sat, 3 Oct 2026 15:40:27 -0700 Subject: [PATCH] Only use Fluent bidi isolation for RTL locales Fluent wraps interpolated values in U+2068/U+2069 isolation marks by default. These are only needed for right-to-left text and otherwise add invisible characters to messages. Each translation now declares its text direction, and isolation is enabled only for "rtl" translations. Co-Authored-By: Claude Opus 5.5 --- src/hyperjump-json-schema.test.js | 6 +++--- src/localization.js | 8 ++++---- src/localization.test.js | 18 +++++++++++++++++- src/translations/index.js | 11 +++++++++-- 4 files changed, 33 insertions(+), 10 deletions(-) diff --git a/src/hyperjump-json-schema.test.js b/src/hyperjump-json-schema.test.js index 7ac8f4c..e7b79e0 100644 --- a/src/hyperjump-json-schema.test.js +++ b/src/hyperjump-json-schema.test.js @@ -155,9 +155,9 @@ const isCompatible = (compatibility, versionUnderTest) => { /** @type (messageId: string, messageParams: MessageParams) => string */ const getMessage = (function () { - const ftl = translations["en-US"]; - const resource = new FluentResource(ftl); - const bundle = new FluentBundle("en-US"); + const translation = translations["en-US"]; + const resource = new FluentResource(translation.ftl); + const bundle = new FluentBundle("en-US", { useIsolating: translation.direction === "rtl" }); bundle.addResource(resource); const disjunction = new Intl.ListFormat("en-US", { type: "disjunction" }); diff --git a/src/localization.js b/src/localization.js index f6abc3c..1672371 100644 --- a/src/localization.js +++ b/src/localization.js @@ -24,12 +24,12 @@ export class Localization { /** @type (locale: string) => Localization */ static forLocale(locale) { if (!localizationCache.has(locale)) { - const ftl = translations[locale]; - if (!ftl) { + const translation = translations[locale]; + if (!translation) { throw Error(`The ${locale} locale is not supported.`); } - const resource = new FluentResource(ftl); - const bundle = new FluentBundle(locale); + const resource = new FluentResource(translation.ftl); + const bundle = new FluentBundle(locale, { useIsolating: translation.direction === "rtl" }); bundle.addResource(resource); localizationCache.set(locale, new Localization(locale, bundle)); } diff --git a/src/localization.test.js b/src/localization.test.js index 56bce3d..82900cc 100644 --- a/src/localization.test.js +++ b/src/localization.test.js @@ -4,13 +4,19 @@ import { Localization } from "./localization.js"; describe("Localization", () => { const fixtureLocale = "fx-TR"; + const ltrFixtureLocale = "fx-LT"; + const rtlFixtureLocale = "fx-RT"; beforeAll(() => { - translations[fixtureLocale] = `test = unsupported locale`; + translations[fixtureLocale] = { ftl: `test = unsupported locale`, direction: "ltr" }; + translations[ltrFixtureLocale] = { ftl: `type-message = Expected a {$expectedTypes}`, direction: "ltr" }; + translations[rtlFixtureLocale] = { ftl: `type-message = Expected a {$expectedTypes}`, direction: "rtl" }; }); afterAll(() => { delete translations[fixtureLocale]; + delete translations[ltrFixtureLocale]; + delete translations[rtlFixtureLocale]; }); test("unsupported locale", () => { @@ -21,4 +27,14 @@ describe("Localization", () => { const localization = Localization.forLocale(fixtureLocale); expect(() => localization.getBooleanSchemaErrorMessage()).to.throw(Error); }); + + test("ltr locale doesn't isolate placeables", () => { + const localization = Localization.forLocale(ltrFixtureLocale); + expect(localization.getTypeErrorMessage(["string"])).to.equal("Expected a string"); + }); + + test("rtl locale isolates placeables", () => { + const localization = Localization.forLocale(rtlFixtureLocale); + expect(localization.getTypeErrorMessage(["string"])).to.equal("Expected a ⁨string⁩"); + }); }); diff --git a/src/translations/index.js b/src/translations/index.js index ce6443e..889942b 100644 --- a/src/translations/index.js +++ b/src/translations/index.js @@ -1,6 +1,13 @@ import enUS from "./en-US.js"; -/** @type Record */ +/** + * @typedef {{ + * ftl: string; + * direction: "ltr" | "rtl"; + * }} Translation + */ + +/** @type Record */ export const translations = { - "en-US": enUS + "en-US": { ftl: enUS, direction: "ltr" } };