Aimpress_site/specs/001-multi-language/research.md
Vadym Samoilenko 6e932d76e4 Add multi-language support (EN/UK) across entire site
Custom i18n system with typed translation dictionaries (~570 keys),
LanguageProvider context, and useTranslation hook. All 31 components
and pages wired with t() calls. Chatbot backend passes language hint
to Claude for Ukrainian responses. Language preference persists via
localStorage. SEO meta tags and html lang attribute update dynamically.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 13:32:04 +00:00

3.3 KiB

Research: Multi-Language Support

Decision 1: i18n Library Choice

Decision: Custom React Context + typed JSON dictionaries (no external library)

Rationale:

  • Only 2 languages (en, uk) with ~200 strings — well below the complexity threshold where libraries add value
  • Full TypeScript type safety on translation keys (compile-time error for typos)
  • Zero bundle size overhead
  • react-i18next would add 3 dependencies (i18next, react-i18next, i18next-browser-languagedetector) for features not needed: namespace lazy-loading, plural rules engine, ICU message format
  • Developer experience is identical: const { t } = useTranslation()t('key')

Alternatives considered:

  • react-i18next: Industry standard but overkill for 2 languages + no pluralization needs
  • react-intl (FormatJS): Even heavier, designed for complex ICU message formatting
  • typesafe-i18n: Lighter but still an unnecessary dependency for this scale

Decision 2: Translation File Structure

Decision: Single TypeScript file per language with flat dot-separated keys

Rationale:

  • TypeScript files (not JSON) enable satisfies Translations type checking
  • Flat keys ('header.nav.home') are greppable and avoid nested access boilerplate
  • Single file per language keeps translations easy to review and compare side-by-side
  • ~200 keys fit comfortably in a single file without organization issues

Alternatives considered:

  • JSON files: Lose TypeScript type checking
  • Namespace-based splitting (per component): Unnecessary fragmentation for ~200 keys
  • Nested objects: Require helper functions to access deeply, harder to grep

Decision 3: Language Persistence

Decision: localStorage with key aimpress_lang

Rationale:

  • Simplest approach for client-side only language state
  • Persists across sessions without cookies or server-side state
  • No GDPR/privacy concerns (functional preference, not tracking)

Alternatives considered:

  • Cookie-based: Adds cookie consent complexity for a functional preference
  • URL-based (/en/, /uk/): Ruled out in spec — would require router changes and redirect logic

Decision 4: Chatbot Language Passing

Decision: Add language field to API request body + synthetic message pair for Ukrainian

Rationale:

  • The chatbot backend (FastAPI) already has a system prompt that says "Respond in the same language the visitor uses"
  • Explicit language parameter is more reliable than relying on message language detection
  • Synthetic message pair (user hint + assistant acknowledgment) guides Claude without modifying the system prompt

Alternatives considered:

  • Modify system prompt dynamically: Would require changing the knowledge.py architecture
  • Query parameter: Less clean than request body field
  • Separate endpoint per language: Over-engineered

Decision 5: Handling JSX in Translations

Decision: Avoid JSX in translations; use CSS for line breaks, split strings where needed

Rationale:

  • Only ~5 cases involve <br> in text (Benefits card titles)
  • CSS white-space: pre-line or \n in strings handles most cases
  • Keeps translation files as pure string dictionaries

Alternatives considered:

  • tJsx(key) returning ReactNode: Adds complexity to the i18n system for 5 edge cases
  • dangerouslySetInnerHTML: Security risk, avoid