From 6b5cf77e677a7032df3588efe6a1563a93393a06 Mon Sep 17 00:00:00 2001 From: Andrew Polk Date: Fri, 24 Jul 2026 11:08:42 -0700 Subject: [PATCH] fix: populate script isRtl in controller and svelte Completes feature parity for IScript.isRtl, which shipped in 0.4.0 for the React hook only. The language-chooser controller now stamps isRtl on the selected script at every selection path (normal selection, sole-script auto-selection, and customize-modal submit), so Svelte and other controller consumers receive the reading direction in the emitted orthography. The Svelte demo shows the script's RTL/LTR direction, matching the React DialogDemo. Also hardens isRTLScript to return false (instead of throwing) on a malformed or unknown script code. Co-Authored-By: Claude Opus 4.8 --- .../find-language/findLanguageInterfaces.ts | 14 ++++++--- .../src/view-models/language-chooser.ts | 18 ++++++++++-- .../test/language-chooser.spec.ts | 29 ++++++++++++++----- .../src/demos/BasicDemo.svelte | 10 ++++++- 4 files changed, 56 insertions(+), 15 deletions(-) diff --git a/components/language-chooser/common/find-language/findLanguageInterfaces.ts b/components/language-chooser/common/find-language/findLanguageInterfaces.ts index 1efe4736..cd745387 100644 --- a/components/language-chooser/common/find-language/findLanguageInterfaces.ts +++ b/components/language-chooser/common/find-language/findLanguageInterfaces.ts @@ -72,8 +72,14 @@ export interface IOrthography { // isRtl setting to match its IScript in every case, which can accomplish // with und-{script}. export function isRTLScript(scriptCode: string): boolean { - const locale = new Intl.Locale(`und-${scriptCode}`); - // getTextInfo is the standardized property; textInfo is the older name - const info = locale.getTextInfo?.() ?? (locale as any).textInfo; - return info?.direction === "rtl"; + try { + const locale = new Intl.Locale(`und-${scriptCode}`); + // getTextInfo is the standardized property; textInfo is the older name + const info = locale.getTextInfo?.() ?? (locale as any).textInfo; + return info?.direction === "rtl"; + } catch { + // An unrecognized/malformed script code makes Intl.Locale throw. Such a + // script has no known RTL direction, so treat it as not RTL. + return false; + } } diff --git a/components/language-chooser/common/language-chooser-controller/src/view-models/language-chooser.ts b/components/language-chooser/common/language-chooser-controller/src/view-models/language-chooser.ts index f1014d21..49cc55fa 100644 --- a/components/language-chooser/common/language-chooser-controller/src/view-models/language-chooser.ts +++ b/components/language-chooser/common/language-chooser-controller/src/view-models/language-chooser.ts @@ -9,6 +9,7 @@ import { type IRegion, type IScript, isManuallyEnteredTagLanguage, + isRTLScript, isUnlistedLanguage, isValidBcp47Tag, languageForManuallyEnteredTag, @@ -118,7 +119,7 @@ export function useLanguageChooserViewModel( if (selectedLang.scripts.length === 1) { // Automatically select a language's only script _setScriptList([]); - selectedScript.value = selectedLang.scripts[0]; + selectedScript.value = scriptWithReadingDirection(selectedLang.scripts[0]); } else { _setScriptList(selectedLang.scripts); } @@ -135,7 +136,9 @@ export function useLanguageChooserViewModel( function _onScriptSelected(index: number) { selectItem(index, listedScripts.value); - selectedScript.value = listedScripts.value[index].script; + selectedScript.value = scriptWithReadingDirection( + listedScripts.value[index].script + ); _onOrthographyChanged(); } @@ -271,7 +274,9 @@ export function useLanguageChooserViewModel( region?: IRegion; dialect?: string; }) { - selectedScript.requestUpdate(script); + selectedScript.requestUpdate( + script ? scriptWithReadingDirection(script) : script + ); customizations.requestUpdate({ region, dialect, @@ -308,6 +313,13 @@ export function useLanguageChooserViewModel( }; } +// Returns a copy of the script with its reading direction (isRtl) populated, +// so consumers receive the direction as part of the selected orthography. +// Mirrors the behavior of the React useLanguageChooser hook. +function scriptWithReadingDirection(script: IScript): IScript { + return { ...script, isRtl: isRTLScript(script.code) }; +} + function hasValidDisplayName(selection: IOrthography) { if (!selection.language) { return false; diff --git a/components/language-chooser/common/language-chooser-controller/test/language-chooser.spec.ts b/components/language-chooser/common/language-chooser-controller/test/language-chooser.spec.ts index e2a83d66..d4d448ce 100644 --- a/components/language-chooser/common/language-chooser-controller/test/language-chooser.spec.ts +++ b/components/language-chooser/common/language-chooser-controller/test/language-chooser.spec.ts @@ -336,9 +336,10 @@ describe("selected script", () => { test.viewModel.listedLanguages.value[0].isSelected.requestUpdate(true); test.viewModel.listedScripts.value[0].isSelected.requestUpdate(true); - expect(test.viewModel.selectedScript.value).toEqual( - NorthernUzbekLanguage.scripts[0] - ); + expect(test.viewModel.selectedScript.value).toEqual({ + ...NorthernUzbekLanguage.scripts[0], + isRtl: false, + }); }); it("should be undefined after script deselected", () => { @@ -366,9 +367,20 @@ describe("selected script", () => { test.viewModel.listedLanguages.value[0].isSelected.requestUpdate(true); - expect(test.viewModel.selectedScript.value).toEqual( - WaataLanguage.scripts[0] - ); + expect(test.viewModel.selectedScript.value).toEqual({ + ...WaataLanguage.scripts[0], + isRtl: false, + }); + }); + + it("should populate isRtl for a right-to-left script", () => { + const test = new TestHelper({ initialLanguages: [NorthernUzbekLanguage] }); + + test.viewModel.listedLanguages.value[0].isSelected.requestUpdate(true); + // NorthernUzbekLanguage.scripts[1] is Arabic, a right-to-left script + test.viewModel.listedScripts.value[1].isSelected.requestUpdate(true); + + expect(test.viewModel.selectedScript.value?.isRtl).toBe(true); }); }); @@ -626,7 +638,9 @@ describe("customize language modal", () => { scriptViewModel.isSelected.requestUpdate(true); t.viewModel.onCustomizeButtonClicked(); - expect(spy).toHaveBeenCalledWith({ script: scriptViewModel.script }); + expect(spy).toHaveBeenCalledWith({ + script: { ...scriptViewModel.script, isRtl: false }, + }); }); it("populates with dialect when custom dialect was selected", () => { @@ -706,6 +720,7 @@ describe("customize language modal", () => { expect(t.viewModel.selectedScript.value).toEqual({ code: "abc", name: "ABC Script", + isRtl: false, }); }); diff --git a/components/language-chooser/svelte/language-chooser-svelte-daisyui/src/demos/BasicDemo.svelte b/components/language-chooser/svelte/language-chooser-svelte-daisyui/src/demos/BasicDemo.svelte index c867a35e..b81c1a6d 100644 --- a/components/language-chooser/svelte/language-chooser-svelte-daisyui/src/demos/BasicDemo.svelte +++ b/components/language-chooser/svelte/language-chooser-svelte-daisyui/src/demos/BasicDemo.svelte @@ -70,7 +70,15 @@
Script
-
{orthography.script?.name || "-"}
+
+ {#if orthography.script} + {orthography.script.name} ({orthography.script.isRtl + ? "RTL" + : "LTR"}) + {:else} + - + {/if} +