Multi-Language Typography Engine
Typography that doesn't just translate text — it reconfigures the entire reading surface for each language's structural requirements.
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.
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.
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 'Hiragino Kaku Gothic ProN', 'Yu Gothic', 'Noto Sans CJK JP', system-ui. 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. Padding, margin, and border radius values that are directional (start/end, not left/right) are written using logical properties throughout the CSS.
- Dynamic dir attribute management:the LanguageSwitcher component reads the current language's metadata (including isRTL) and imperatively updates document.dir on selection. This is intentionally imperative rather than declarative to avoid an intermediate render with the wrong direction.
Demo
Switch between scripts to see font stack, reading direction, and typographic adjustments applied live.
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. The translations table is a flat TypeScript record rather than nested JSON files, which makes dead-code elimination simpler and keeps the bundle size predictable.
The font change happens in a single style recalculation pass rather than through multiple JS DOM mutations.
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.
- 11languages — Latin, RTL, CJK, Cyrillic, Nordic
- 1CSS bundle, scoped by data attribute
- 0layout shift on language switch
- localStorage persistence eliminates repeat-visit FOUC