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>
3.3 KiB
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-i18nextwould 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 needsreact-intl(FormatJS): Even heavier, designed for complex ICU message formattingtypesafe-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 Translationstype 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-lineor\nin 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