- React Context
- TypeScript
- CSS Custom Properties
- i18n
Multi-Language Typography Engine
Typography that doesn't just translate text — it reconfigures the entire reading surface for each language's structural requirements.
Problem
Standard i18n implementations swap text strings and stop there. The actual problem is deeper: Arabic and Hebrew require RTL layout direction, CJK scripts require fundamentally different font stacks and line-height adjustments, and some languages have UI strings long enough to break fixed-width navigation elements. A typography engine that only handles string replacement produces a site that technically supports eleven languages but visually reads like it was designed for one.
Approach
Language switching is implemented as a coordinated state update that modifies the document's dir attribute, the html element's lang, the body's font-family via a scoped CSS class, and all UI strings simultaneously — from a single context dispatch.
- localStorage persistence: the selected language is stored in localStorage and read on initial hydration to prevent flash-of-wrong-direction on return visits. The SSR default is 'en' with LTR; the client immediately reconciles on mount.
- CJK font stack: Japanese and Mandarin apply a separate --font-body token resolving to system CJK fonts. Line height increases from 1.6 to 1.8 for CJK to accommodate taller character bounding boxes.
- RTL cascade: Arabic and Hebrew set dir='rtl' on the html element, which reverses flex direction across the entire layout. Directional values are written using CSS logical properties (padding-inline-start, not padding-left) throughout.
- Dynamic dir management: The LanguageSwitcher imperatively updates document.dir on selection. This is intentionally imperative rather than declarative to avoid an intermediate render with the wrong direction.
Technical Detail
The font switching mechanism relies on data-lang attributes cascaded from the html element rather than JavaScript-injected inline styles. Each language has a CSS rule matching [data-lang='ja'] body { font-family: var(--font-cjk); line-height: 1.8; } — which means the font change happens in a single style recalculation pass rather than through multiple JS DOM mutations. The LanguageSwitcher component itself is dynamically imported with ssr:false, preventing hydration mismatches from the localStorage read.
Outcome
Eleven languages rendering correctly with appropriate typography, reading direction, and UI strings — loaded from a single shared stylesheet with no per-language CSS bundles.
- 11 languages: Latin, RTL (Arabic, Hebrew), CJK (Japanese, Mandarin), Cyrillic, Nordic
- Single CSS bundle with data-attribute language scoping
- No layout shift on language switch
- localStorage persistence eliminates repeat-visit FOUC