From 28c682500ca84bba214c1b7d60868fac5ea33346 Mon Sep 17 00:00:00 2001 From: c0mrade Date: Tue, 26 May 2026 15:15:26 +0300 Subject: [PATCH] fix(i18n,a11y): sync + dir with i18n language MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit index.html ships and that attribute never changed at runtime, regardless of the user's selected language. Consequences: - screen readers pronounced en/zh/fa content using Russian phonics - browsers offered to translate already-translated content - the Telegram login button and an OAuth widget read documentElement.lang to set their own locale and got the wrong one LanguageSwitcher had a half-fix that only touched dir for fa, and only locally — i18n changes from anywhere else (initial detect, Telegram adopt) were not covered. Move both lang and dir updates into a single languageChanged subscriber inside i18n.ts, run once for the initial detected language, and drop the now-redundant dir-setting in LanguageSwitcher. Layout direction flips automatically for the full RTL set (fa + future ar/he/ur). --- src/components/LanguageSwitcher.tsx | 7 ++----- src/i18n.ts | 19 +++++++++++++++++++ 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/components/LanguageSwitcher.tsx b/src/components/LanguageSwitcher.tsx index 981a8cd..218b9f6 100644 --- a/src/components/LanguageSwitcher.tsx +++ b/src/components/LanguageSwitcher.tsx @@ -34,15 +34,12 @@ export default function LanguageSwitcher() { }, []); const changeLanguage = (code: string) => { + // i18n.ts subscribes to languageChanged and syncs + dir + // centrally — no need to set documentElement.dir here. i18n.changeLanguage(code); - document.documentElement.dir = code === 'fa' ? 'rtl' : 'ltr'; setIsOpen(false); }; - useEffect(() => { - document.documentElement.dir = i18n.language === 'fa' ? 'rtl' : 'ltr'; - }, [i18n.language]); - if (availableLanguages.length <= 1) { return null; } diff --git a/src/i18n.ts b/src/i18n.ts index 78a8a22..caaef33 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -57,10 +57,29 @@ const detectedLng = i18n.language?.split('-')[0] || FALLBACK_LNG; const langsToLoad = [FALLBACK_LNG, ...(detectedLng !== FALLBACK_LNG ? [detectedLng] : [])]; Promise.all(langsToLoad.map(loadLanguage)); +// Keep + dir in sync with i18n so screen readers pronounce +// content correctly, browsers don't offer to translate it, and RTL +// languages (fa) flip layout direction. index.html ships with lang="ru" +// for the first paint; runtime updates take over from there. +const RTL_LANGS = new Set(['fa', 'ar', 'he', 'ur']); +function syncHtmlLang(lng: string): void { + const code = lng.split('-')[0]; + if (typeof document === 'undefined') return; + if (document.documentElement.lang !== code) { + document.documentElement.lang = code; + } + const dir = RTL_LANGS.has(code) ? 'rtl' : 'ltr'; + if (document.documentElement.dir !== dir) { + document.documentElement.dir = dir; + } +} +syncHtmlLang(detectedLng); + // Lazy-load on language change i18n.on('languageChanged', (lng: string) => { const code = lng.split('-')[0]; loadLanguage(code); + syncHtmlLang(code); }); /**