index.html ships <html lang='ru'> 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).
Modal a11y:
- focus-trap + role=dialog/aria-modal/aria-labelledby on AdminRoles delete confirm
and AdminBanSystem user-detail modal (+ close aria-label)
Telegram theme/language sync (first run only, explicit choice always wins):
- getTelegramColorScheme() / getTelegramLanguageCode() helpers in useTelegramSDK
- useTheme initial state follows the Telegram client color scheme when no stored theme
- applyTelegramLanguage() adopts the Telegram client language when no stored choice,
called from main.tsx after SDK init (launch params unavailable before init)
Split 574KB monolithic locales chunk into per-language lazy chunks:
- ru: 211KB (loaded on startup as fallback)
- fa: 143KB, en: 121KB, zh: 101KB (loaded on demand)
Russian users save 63% bandwidth. Other languages loaded only when
user switches via LanguageSwitcher.