Aimpress_site/specs/001-multi-language/plan.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

6.4 KiB

Implementation Plan: Multi-Language Support

Branch: 001-multi-language | Date: 2026-03-09 | Spec: spec.md Input: Feature specification from /specs/001-multi-language/spec.md

Summary

Add English/Ukrainian multi-language support to the AImpress React SPA. A custom i18n solution using React Context + typed JSON dictionaries will power the existing (non-functional) language toggle in the Header. All ~80+ hardcoded UI strings will be extracted into translation files, with localStorage persistence and chatbot backend language hints.

Technical Context

Language/Version: TypeScript 5.x, React 19 Primary Dependencies: None added (custom i18n solution — no library needed for 2 languages + ~200 strings) Storage: localStorage for language preference Testing: Manual verification per component (no test framework in project) Target Platform: Web browser (SPA) Project Type: Web application (React SPA with Vite) Performance Goals: Language switch under 1 second, no layout shift Constraints: No URL-based routing for languages, blog content stays English, analytics events stay English Scale/Scope: ~200 translatable strings across ~25 components, 2 languages (en, uk)

Constitution Check

Constitution is unconfigured (template only) — no gates to check.

Project Structure

Documentation (this feature)

specs/001-multi-language/
├── plan.md              # This file
├── research.md          # Phase 0 output
├── data-model.md        # Phase 1 output
├── quickstart.md        # Phase 1 output
└── tasks.md             # Phase 2 output (created by /speckit.tasks)

Source Code (new files)

src/
  i18n/
    types.ts              # Translations interface (all keys typed)
    en.ts                 # English translation object
    uk.ts                 # Ukrainian translation object
    LanguageContext.tsx    # LanguageProvider + useTranslation hook
    index.ts              # Barrel export

Source Code (modified files, by phase)

Phase 1 — Foundation + Header Toggle:

  • src/main.tsx — wrap <App> with <LanguageProvider>
  • src/components/Header.tsx — wire toggle to context, replace nav item names with t() calls

Phase 2 — Homepage Components (top to bottom):

  • src/components/Hero.tsx
  • src/components/Benefits.tsx
  • src/components/Banner1.tsx
  • src/components/RealResults.tsx
  • src/components/Timeline.tsx
  • src/components/Banner2.tsx
  • src/components/ComparisonTable.tsx
  • src/components/BlogSection.tsx (date locale fix)
  • src/components/ResourcesSection.tsx
  • src/components/ContactSection.tsx + src/components/ContactForm.tsx
  • src/components/Footer.tsx
  • src/components/CookieConsent.tsx

Phase 3 — Subpages + Chat + Forms:

  • src/pages/AboutPage.tsx (~50 strings)
  • src/pages/ServicesPage.tsx
  • src/pages/PricingPage.tsx
  • src/components/ChatBubble.tsx, ChatWindow.tsx, ChatLeadForm.tsx, ChatInput.tsx
  • src/components/QuoteForm.tsx
  • src/pages/BlogPage.tsx, BlogPostPage.tsx (UI chrome only)

Phase 4 — SEO + Chatbot Backend:

  • SEO component: <html lang={lang}> + translated titles/descriptions
  • src/hooks/useChat.ts — pass language in request body
  • chatbot-api/models.py — add language field to ChatRequest
  • chatbot-api/main.py — prepend language hint when language == "uk"

Structure Decision: Custom i18n module at src/i18n/ with React Context. No external library. Provider wraps the entire app at main.tsx.

Technical Approach

Why Custom Instead of react-i18next

  • Only 2 languages, ~200 strings, no pluralization complexity
  • Zero added bundle size beyond translation JSON
  • Full TypeScript type safety on keys (typos cause compile errors)
  • react-i18next would add ~3 dependencies for features we don't need
  • Simple t('key') API with identical developer experience

LanguageContext Design

// LanguageContext.tsx — ~60 lines
interface LanguageContextValue {
  lang: 'en' | 'uk';
  setLang: (lang: 'en' | 'uk') => void;
  t: (key: TranslationKey) => string;
}
  • State initialized from localStorage.getItem('aimpress_lang') || 'en'
  • setLang updates state + localStorage + document.documentElement.lang
  • t(key) looks up current language dict, falls back to English dict
  • useTranslation() hook returns { t, lang, setLang }

Header Toggle Wiring

The existing Header has a visual toggle (lines 112-142) with local state. Changes:

  1. Remove local currentLang state — derive from context
  2. Map: lang === 'en'"Eng", lang === 'uk'"Ukr"
  3. handleLangSelect calls setLang('en') or setLang('uk')
  4. navItems array moves inside component body to use t() calls

Array-Based Data Pattern

Components like Timeline, Benefits, ComparisonTable define data as module-scope arrays. These must move inside the component body (or become functions accepting t) since hooks can only be called inside components.

Chatbot Backend Changes

  1. ChatRequest model gets language: str = "en" field
  2. When language == "uk", prepend synthetic message pair to guide Claude:
    • User: [System: Visitor UI is Ukrainian. Default to Ukrainian.]
    • Assistant: Зрозуміло. Я буду відповідати українською.
  3. Frontend useChat hook passes lang from context in request body

SEO Approach

  • Use react-helmet-async (already installed) to set <html lang={lang}>
  • Each page passes t('seo.pageName.title') and t('seo.pageName.description') to <SEO> component
  • OpenGraph/Twitter tags inherit from same translated props

Known Risks

Risk Impact Mitigation
String extraction completeness Missing strings show English (safe but inconsistent) Grep for quoted strings in JSX after extraction
JSX in translations (e.g. <br>) ~5 cases where plain string lookup doesn't work Use CSS for line breaks or tJsx() variant returning ReactNode
Ukrainian text ~15-20% longer Layout overflow on fixed-width elements Review all buttons/nav for flexible sizing
Privacy Policy / Terms of Use Long legal text, may need legal review Stay English-only initially, translate later
Blog date locale hardcoded as 'en-US' Dates won't match language Map lang to locale: enen-GB, ukuk-UA

Complexity Tracking

No constitution violations to justify.