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>
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 witht()calls
Phase 2 — Homepage Components (top to bottom):
src/components/Hero.tsxsrc/components/Benefits.tsxsrc/components/Banner1.tsxsrc/components/RealResults.tsxsrc/components/Timeline.tsxsrc/components/Banner2.tsxsrc/components/ComparisonTable.tsxsrc/components/BlogSection.tsx(date locale fix)src/components/ResourcesSection.tsxsrc/components/ContactSection.tsx+src/components/ContactForm.tsxsrc/components/Footer.tsxsrc/components/CookieConsent.tsx
Phase 3 — Subpages + Chat + Forms:
src/pages/AboutPage.tsx(~50 strings)src/pages/ServicesPage.tsxsrc/pages/PricingPage.tsxsrc/components/ChatBubble.tsx,ChatWindow.tsx,ChatLeadForm.tsx,ChatInput.tsxsrc/components/QuoteForm.tsxsrc/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— passlanguagein request bodychatbot-api/models.py— addlanguagefield toChatRequestchatbot-api/main.py— prepend language hint whenlanguage == "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-i18nextwould 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' setLangupdates state +localStorage+document.documentElement.langt(key)looks up current language dict, falls back to English dictuseTranslation()hook returns{ t, lang, setLang }
Header Toggle Wiring
The existing Header has a visual toggle (lines 112-142) with local state. Changes:
- Remove local
currentLangstate — derive from context - Map:
lang === 'en'→"Eng",lang === 'uk'→"Ukr" handleLangSelectcallssetLang('en')orsetLang('uk')navItemsarray moves inside component body to uset()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
ChatRequestmodel getslanguage: str = "en"field- When
language == "uk", prepend synthetic message pair to guide Claude:- User:
[System: Visitor UI is Ukrainian. Default to Ukrainian.] - Assistant:
Зрозуміло. Я буду відповідати українською.
- User:
- Frontend
useChathook passeslangfrom context in request body
SEO Approach
- Use
react-helmet-async(already installed) to set<html lang={lang}> - Each page passes
t('seo.pageName.title')andt('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: en → en-GB, uk → uk-UA |
Complexity Tracking
No constitution violations to justify.