From 6e932d76e4f751bbe4a41d537d9c4fd41c7bb3e1 Mon Sep 17 00:00:00 2001 From: Vadym Samoilenko Date: Mon, 9 Mar 2026 13:32:04 +0000 Subject: [PATCH] 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 --- CLAUDE.md | 7 + chatbot-api/main.py | 7 + chatbot-api/models.py | 1 + .../checklists/requirements.md | 35 ++ specs/001-multi-language/data-model.md | 57 ++ specs/001-multi-language/plan.md | 151 +++++ specs/001-multi-language/quickstart.md | 36 ++ specs/001-multi-language/research.md | 72 +++ specs/001-multi-language/spec.md | 126 ++++ specs/001-multi-language/tasks.md | 211 +++++++ src/components/Banner1.tsx | 16 +- src/components/Banner2.tsx | 4 +- src/components/Benefits.tsx | 75 +-- src/components/BlogSection.tsx | 22 +- src/components/ChatBubble.tsx | 7 +- src/components/ChatInput.tsx | 6 +- src/components/ChatLeadForm.tsx | 28 +- src/components/ChatWidget.tsx | 4 +- src/components/ChatWindow.tsx | 16 +- src/components/ComparisonTable.tsx | 64 +- src/components/ContactForm.tsx | 26 +- src/components/ContactSection.tsx | 6 +- src/components/CookieConsent.tsx | 11 +- src/components/Footer.tsx | 8 +- src/components/Header.tsx | 69 ++- src/components/Hero.tsx | 12 +- src/components/QuoteForm.tsx | 57 +- src/components/RealResults.tsx | 43 +- src/components/ResourcesSection.tsx | 4 +- src/components/SEO.tsx | 65 +- src/components/Timeline.tsx | 130 ++-- src/hooks/useChat.ts | 5 +- src/i18n/LanguageContext.tsx | 58 ++ src/i18n/en.ts | 568 +++++++++++++++++ src/i18n/index.ts | 2 + src/i18n/types.ts | 570 ++++++++++++++++++ src/i18n/uk.ts | 568 +++++++++++++++++ src/main.tsx | 9 +- src/pages/AboutPage.tsx | 290 +++++---- src/pages/BlogPage.tsx | 30 +- src/pages/BlogPostPage.tsx | 28 +- src/pages/PricingPage.tsx | 270 ++++----- src/pages/ServicesPage.tsx | 502 +++++++-------- 43 files changed, 3390 insertions(+), 886 deletions(-) create mode 100644 specs/001-multi-language/checklists/requirements.md create mode 100644 specs/001-multi-language/data-model.md create mode 100644 specs/001-multi-language/plan.md create mode 100644 specs/001-multi-language/quickstart.md create mode 100644 specs/001-multi-language/research.md create mode 100644 specs/001-multi-language/spec.md create mode 100644 specs/001-multi-language/tasks.md create mode 100644 src/i18n/LanguageContext.tsx create mode 100644 src/i18n/en.ts create mode 100644 src/i18n/index.ts create mode 100644 src/i18n/types.ts create mode 100644 src/i18n/uk.ts diff --git a/CLAUDE.md b/CLAUDE.md index e95ee8e..59cd4c5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -40,3 +40,10 @@ React 19 + TypeScript single-page application built with Vite. **Font:** Self-hosted Inter (weights 300–900) loaded via `@font-face` in `src/index.css`, served from `public/font/Inter/`. **Base styles:** CSS reset in `src/index.css`. Dark background (`--dark-grey-100`), light text (`--light-grey-100`). + +## Active Technologies +- TypeScript 5.x, React 19 + None added (custom i18n solution — no library needed for 2 languages + ~200 strings) (001-multi-language) +- localStorage for language preference (001-multi-language) + +## Recent Changes +- 001-multi-language: Added TypeScript 5.x, React 19 + None added (custom i18n solution — no library needed for 2 languages + ~200 strings) diff --git a/chatbot-api/main.py b/chatbot-api/main.py index 2ef22d3..4d0ef00 100644 --- a/chatbot-api/main.py +++ b/chatbot-api/main.py @@ -133,6 +133,13 @@ async def chat(req: ChatRequest): lead_context += " IMPORTANT: Whenever the visitor mentions budget, timeline, requirements, job title, phone, city, or any useful detail — IMMEDIATELY call update_lead with the 'note' field to enrich their CRM profile.]" messages = [{"role": "user", "content": lead_context}, {"role": "assistant", "content": "Understood. I'll use update_lead to enrich their profile whenever they share new details."}] + messages + # Prepend language hint for non-English visitors + if req.language == "uk": + messages = [ + {"role": "user", "content": "[System: The visitor's interface language is Ukrainian. Default to Ukrainian in your responses unless they write in English.]"}, + {"role": "assistant", "content": "Зрозуміло. Я буду відповідати українською."}, + ] + messages + # Get AI response try: reply, lead_data = await get_ai_response(messages, session_meta) diff --git a/chatbot-api/models.py b/chatbot-api/models.py index a4e487e..db28bb6 100644 --- a/chatbot-api/models.py +++ b/chatbot-api/models.py @@ -11,6 +11,7 @@ class ChatRequest(BaseModel): session_id: str = Field(..., min_length=1, max_length=64) message: str = Field(..., min_length=1, max_length=500) page_context: str = Field(default="/", max_length=200) + language: str = Field(default="en", max_length=5) lead: ChatLead | None = None diff --git a/specs/001-multi-language/checklists/requirements.md b/specs/001-multi-language/checklists/requirements.md new file mode 100644 index 0000000..59c60c5 --- /dev/null +++ b/specs/001-multi-language/checklists/requirements.md @@ -0,0 +1,35 @@ +# Specification Quality Checklist: Multi-Language Support + +**Purpose**: Validate specification completeness and quality before proceeding to planning +**Created**: 2026-03-09 +**Feature**: [spec.md](../spec.md) + +## Content Quality + +- [x] No implementation details (languages, frameworks, APIs) +- [x] Focused on user value and business needs +- [x] Written for non-technical stakeholders +- [x] All mandatory sections completed + +## Requirement Completeness + +- [x] No [NEEDS CLARIFICATION] markers remain +- [x] Requirements are testable and unambiguous +- [x] Success criteria are measurable +- [x] Success criteria are technology-agnostic (no implementation details) +- [x] All acceptance scenarios are defined +- [x] Edge cases are identified +- [x] Scope is clearly bounded +- [x] Dependencies and assumptions identified + +## Feature Readiness + +- [x] All functional requirements have clear acceptance criteria +- [x] User scenarios cover primary flows +- [x] Feature meets measurable outcomes defined in Success Criteria +- [x] No implementation details leak into specification + +## Notes + +- All items pass validation +- Blog content: UI-only translation confirmed (articles stay English) diff --git a/specs/001-multi-language/data-model.md b/specs/001-multi-language/data-model.md new file mode 100644 index 0000000..e523523 --- /dev/null +++ b/specs/001-multi-language/data-model.md @@ -0,0 +1,57 @@ +# Data Model: Multi-Language Support + +## Entities + +### Language + +A supported language for the site UI. + +| Field | Type | Description | +|-------|------|-------------| +| code | `'en' \| 'uk'` | ISO 639-1 language code | +| displayLabel | `string` | Label shown in toggle ("Eng", "Ukr") | +| locale | `string` | Full locale for date formatting ("en-GB", "uk-UA") | + +**Supported languages** (hardcoded, no dynamic loading): +- English: `{ code: 'en', displayLabel: 'Eng', locale: 'en-GB' }` +- Ukrainian: `{ code: 'uk', displayLabel: 'Ukr', locale: 'uk-UA' }` + +### Translations + +A typed dictionary mapping translation keys to localized strings. + +| Field | Type | Description | +|-------|------|-------------| +| [key: TranslationKey] | `string` | Translated text for the given key | + +**TranslationKey** is a union type of all valid dot-separated keys (e.g., `'header.nav.home' | 'hero.title' | ...`). + +Both `en.ts` and `uk.ts` must satisfy the `Translations` type — TypeScript enforces that no keys are missing. + +### Language Preference (Client-Side) + +| Storage | Key | Type | Default | +|---------|-----|------|---------| +| localStorage | `aimpress_lang` | `'en' \| 'uk'` | `'en'` | + +Read on app initialization. Updated on language toggle. No server-side persistence. + +## State Transitions + +``` +First Visit → lang = 'en' (default) + ↓ +User clicks 'Ukr' → lang = 'uk', localStorage set + ↓ +User clicks 'Eng' → lang = 'en', localStorage set + ↓ +Return Visit → lang = localStorage value (or 'en' if cleared) +``` + +## Relationships + +- `LanguageContext` holds current `Language` and provides `t()` function +- `t(key)` resolves against current language's `Translations` dict, falls back to English +- `ChatRequest` body includes `language` code for backend AI responses +- Date formatting uses `Language.locale` via `Intl.DateTimeFormat` +- `` attribute set from `Language.code` diff --git a/specs/001-multi-language/plan.md b/specs/001-multi-language/plan.md new file mode 100644 index 0000000..259be23 --- /dev/null +++ b/specs/001-multi-language/plan.md @@ -0,0 +1,151 @@ +# Implementation Plan: Multi-Language Support + +**Branch**: `001-multi-language` | **Date**: 2026-03-09 | **Spec**: [spec.md](./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) + +```text +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) + +```text +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 `` with `` +- `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: `` + 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 + +```typescript +// 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 `` +- Each page passes `t('seo.pageName.title')` and `t('seo.pageName.description')` to `` 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. `
`) | ~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. diff --git a/specs/001-multi-language/quickstart.md b/specs/001-multi-language/quickstart.md new file mode 100644 index 0000000..8c95cb2 --- /dev/null +++ b/specs/001-multi-language/quickstart.md @@ -0,0 +1,36 @@ +# Quickstart: Multi-Language Support + +## Adding a New Translation + +1. Open `src/i18n/types.ts` and add the key to the `Translations` interface +2. Add the English value in `src/i18n/en.ts` +3. Add the Ukrainian value in `src/i18n/uk.ts` +4. TypeScript will error if any file is missing the new key + +## Using Translations in a Component + +```typescript +import { useTranslation } from '../i18n'; + +function MyComponent() { + const { t, lang } = useTranslation(); + return

{t('my.section.title')}

; +} +``` + +## Formatting Dates by Locale + +```typescript +const { lang } = useTranslation(); +const locale = lang === 'uk' ? 'uk-UA' : 'en-GB'; +const formatted = new Date(dateStr).toLocaleDateString(locale, { + year: 'numeric', month: 'long', day: 'numeric' +}); +``` + +## Adding a New Language (Future) + +1. Add the new code to the `Lang` type in `types.ts` +2. Create a new translation file (e.g., `de.ts`) satisfying `Translations` +3. Register it in `LanguageContext.tsx` translations map +4. Add the language option to the Header toggle diff --git a/specs/001-multi-language/research.md b/specs/001-multi-language/research.md new file mode 100644 index 0000000..02fa018 --- /dev/null +++ b/specs/001-multi-language/research.md @@ -0,0 +1,72 @@ +# 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 `
` 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 diff --git a/specs/001-multi-language/spec.md b/specs/001-multi-language/spec.md new file mode 100644 index 0000000..9e1790b --- /dev/null +++ b/specs/001-multi-language/spec.md @@ -0,0 +1,126 @@ +# Feature Specification: Multi-Language Support + +**Feature Branch**: `001-multi-language` +**Created**: 2026-03-09 +**Status**: Draft +**Input**: User description: "Implement multi-language support for the site" + +## User Scenarios & Testing *(mandatory)* + +### User Story 1 - Browse Site in Ukrainian (Priority: P1) + +A Ukrainian-speaking visitor arrives at the AImpress website. They see a language toggle in the header (which already exists visually but is non-functional) and switch from English to Ukrainian. All static page content — headings, descriptions, navigation labels, button text, form labels, and footer — immediately updates to Ukrainian. The visitor can browse the full site in their preferred language without any page reload. + +**Why this priority**: The language toggle already exists in the UI ("Eng"/"Ukr"), so users already expect this functionality. Delivering Ukrainian translations for static content is the foundational slice that enables the entire multi-language experience. + +**Independent Test**: Can be fully tested by clicking the language toggle in the header and verifying all visible text on every page section changes to Ukrainian. Delivers immediate value to Ukrainian-speaking visitors. + +**Acceptance Scenarios**: + +1. **Given** a visitor on the English site, **When** they click the language toggle to "Ukr", **Then** all static text on the current page displays in Ukrainian without a page reload. +2. **Given** a visitor browsing in Ukrainian, **When** they navigate to a different page section, **Then** the new section also displays in Ukrainian. +3. **Given** a visitor browsing in Ukrainian, **When** they switch back to "Eng", **Then** all text reverts to English. + +--- + +### User Story 2 - Submit Forms in Preferred Language (Priority: P2) + +A visitor browsing in Ukrainian wants to submit the contact form or interact with the chatbot lead form. All form labels, placeholders, validation messages, and success/error messages appear in Ukrainian. The submitted form data is stored as-is (in whichever language the user typed), but system-generated labels and messages display in the selected language. + +**Why this priority**: Forms are the primary conversion point. If a Ukrainian visitor sees a translated page but English-only forms, the experience feels incomplete and may reduce conversions. + +**Independent Test**: Can be tested by switching to Ukrainian, opening the contact form, verifying all labels/placeholders are in Ukrainian, submitting with invalid data to check validation messages, and submitting successfully to verify the success message is in Ukrainian. + +**Acceptance Scenarios**: + +1. **Given** a visitor browsing in Ukrainian, **When** they open the contact form, **Then** all form labels, placeholders, and the submit button display in Ukrainian. +2. **Given** a visitor submitting a form in Ukrainian, **When** validation fails, **Then** error messages appear in Ukrainian. +3. **Given** a visitor submitting a form in Ukrainian, **When** submission succeeds, **Then** the success confirmation message appears in Ukrainian. +4. **Given** a visitor using the chatbot lead form in Ukrainian, **When** they see the lead capture form, **Then** all labels, placeholders, and consent text display in Ukrainian. + +--- + +### User Story 3 - Language Preference Persistence (Priority: P3) + +A visitor selects Ukrainian as their language. When they return to the site later or refresh the page, the site remembers their preference and displays in Ukrainian automatically. New visitors see the site in a default language based on reasonable defaults (English as fallback). + +**Why this priority**: Without persistence, users must re-select their language on every visit, creating friction. This is important for repeat visitors but not as critical as the initial translation itself. + +**Independent Test**: Can be tested by selecting Ukrainian, closing the browser tab, reopening the site, and verifying the language is still set to Ukrainian. + +**Acceptance Scenarios**: + +1. **Given** a visitor who previously selected Ukrainian, **When** they return to the site, **Then** the site loads in Ukrainian automatically. +2. **Given** a first-time visitor, **When** they load the site, **Then** the site displays in English by default. +3. **Given** a visitor who clears their browser data, **When** they return to the site, **Then** the site displays in English (default). + +--- + +### User Story 4 - Chatbot Conversations in Preferred Language (Priority: P4) + +A visitor browsing in Ukrainian opens the AI chatbot. The chatbot greeting, system messages, and AI responses are in Ukrainian. The chatbot understands and responds in the language the visitor is using. + +**Why this priority**: The chatbot is a key engagement tool but involves backend AI integration. The static UI elements (greeting, status text, input placeholder) should match the selected language, and the AI should respond in the same language the user writes in. + +**Independent Test**: Can be tested by switching to Ukrainian, opening the chatbot, verifying the greeting and UI elements are in Ukrainian, and sending a message in Ukrainian to verify the AI responds in Ukrainian. + +**Acceptance Scenarios**: + +1. **Given** a visitor browsing in Ukrainian, **When** they open the chatbot, **Then** the greeting message, status text, and input placeholder display in Ukrainian. +2. **Given** a visitor chatting in Ukrainian, **When** they send a message in Ukrainian, **Then** the AI responds in Ukrainian. +3. **Given** a visitor who switches language mid-conversation, **When** they switch from Ukrainian to English, **Then** new chatbot UI elements update to English (existing messages remain as-sent). + +--- + +### Edge Cases + +- What happens when a translation is missing for a specific string? The system falls back to English for that string. +- What happens when a visitor's browser language is Ukrainian but they haven't explicitly chosen a language? The site defaults to English (explicit selection required via toggle). +- How does the system handle right-to-left languages? Out of scope for this feature — only English and Ukrainian are supported. +- What happens to the cookie consent banner when language changes? It updates to the selected language like all other static content. +- What happens to blog post content when language is switched? Blog article titles and body content remain in English. Only the surrounding blog UI (section title, "View All Posts" link, date formatting) translates to the selected language. +- What happens to date formatting when language changes? Dates display in the locale matching the selected language (e.g., "March 9, 2026" vs "9 березня 2026"). + +## Requirements *(mandatory)* + +### Functional Requirements + +- **FR-001**: System MUST provide a functional language toggle in the header that switches between English and Ukrainian. +- **FR-002**: System MUST translate all static UI text (navigation, headings, descriptions, button labels, footer text) to the selected language. +- **FR-003**: System MUST translate all form labels, placeholders, validation messages, and success/error messages to the selected language. +- **FR-004**: System MUST translate chatbot UI elements (greeting, status, placeholder, system messages) to the selected language. +- **FR-005**: System MUST persist the visitor's language preference across browser sessions. +- **FR-006**: System MUST default to English for first-time visitors. +- **FR-007**: System MUST fall back to English for any missing translation string. +- **FR-008**: System MUST update all visible text instantly when the language is toggled, without requiring a page reload. +- **FR-009**: System MUST format dates according to the selected language's locale conventions. +- **FR-010**: System MUST pass the selected language context to the chatbot so AI responses match the visitor's language. +- **FR-011**: System MUST translate the cookie consent banner text when language changes. +- **FR-012**: System MUST translate page titles, meta descriptions, and OpenGraph tags to the selected language for proper search engine indexing. + +### Key Entities + +- **Language**: A supported language with a code (e.g., "en", "uk"), display name, and flag/label for the toggle. +- **Translation**: A key-value mapping of translation keys to translated strings, organized by language. +- **Language Preference**: The visitor's stored language selection, persisted locally in the browser. + +## Success Criteria *(mandatory)* + +### Measurable Outcomes + +- **SC-001**: 100% of static UI text across all page sections displays correctly in both English and Ukrainian. +- **SC-002**: Visitors can switch languages in under 1 second with no visible page reload or layout shift. +- **SC-003**: Language preference persists across at least 5 consecutive return visits. +- **SC-004**: 100% of form labels, placeholders, and messages display in the selected language. +- **SC-005**: The chatbot greets users and displays UI elements in the selected language on first open. +- **SC-006**: Missing translations gracefully fall back to English with no visible errors or broken UI. + +## Assumptions + +- Only two languages are in scope: English (default) and Ukrainian. Additional languages may be added in the future but are not part of this feature. +- The existing language toggle UI in the Header component ("Eng"/"Ukr") will be reused and made functional. +- Blog post body content stays in English. Only the surrounding blog UI (section titles, navigation, dates) will be translated. +- The chatbot AI (Claude Sonnet) already supports Ukrainian — no backend model changes are needed to respond in Ukrainian. The system prompt may need a language hint. +- URL structure will not change for different languages (no `/en/` or `/uk/` path prefixes). Language is client-side state only. +- Analytics event names remain in English regardless of UI language. +- SEO meta tags and page titles should also be translated for proper indexing in Ukrainian search engines. diff --git a/specs/001-multi-language/tasks.md b/specs/001-multi-language/tasks.md new file mode 100644 index 0000000..a49a08b --- /dev/null +++ b/specs/001-multi-language/tasks.md @@ -0,0 +1,211 @@ +# Tasks: Multi-Language Support (EN/UK) + +**Input**: Design documents from `/specs/001-multi-language/` +**Prerequisites**: plan.md, spec.md, research.md, data-model.md, quickstart.md + +**Tests**: Not requested — no test tasks included. + +**Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story. + +## Format: `[ID] [P?] [Story] Description` + +- **[P]**: Can run in parallel (different files, no dependencies) +- **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3) +- Include exact file paths in descriptions + +## Phase 1: Setup (Shared Infrastructure) + +**Purpose**: Create the i18n module and translation files + +- [x] T001 Create `Translations` interface with all translation keys in `src/i18n/types.ts` +- [x] T002 Create English translations object (source of truth) in `src/i18n/en.ts` — extract all ~200 hardcoded strings from every component into dot-separated keys (e.g., `header.nav.home`, `hero.title`, `contact.form.fullName`). Must `satisfies Translations`. +- [x] T003 Create Ukrainian translations object in `src/i18n/uk.ts` — translate all keys from `en.ts` into Ukrainian. Must `satisfies Translations`. +- [x] T004 Create `LanguageProvider` context and `useTranslation` hook in `src/i18n/LanguageContext.tsx` — state initialized from `localStorage.getItem('aimpress_lang') || 'en'`, `setLang` updates state + localStorage + `document.documentElement.lang`, `t(key)` resolves against current language dict with English fallback. +- [x] T005 Create barrel export in `src/i18n/index.ts` + +--- + +## Phase 2: Foundational (Blocking Prerequisites) + +**Purpose**: Wire LanguageProvider into the app and connect the Header toggle + +**CRITICAL**: No user story work can begin until this phase is complete + +- [x] T006 Wrap `` with `` inside the existing `` in `src/main.tsx` +- [x] T007 Wire the existing Header language toggle to the i18n context in `src/components/Header.tsx` — remove local `currentLang` state, import `useTranslation`, map `setLang('en')`/`setLang('uk')` to toggle clicks, derive display label ("Eng"/"Ukr") from context `lang` value + +**Checkpoint**: Language toggle is functional — clicking Eng/Ukr updates `lang` in context and localStorage. No visible text changes yet. + +--- + +## Phase 3: User Story 1 — Browse Site in Ukrainian (Priority: P1) MVP + +**Goal**: All static homepage text displays in the selected language when the toggle is clicked. + +**Independent Test**: Click the language toggle in the header → verify all visible text on every homepage section changes to Ukrainian without page reload. Click back to English → verify all text reverts. + +### Implementation for User Story 1 + +- [x] T008 [US1] Replace hardcoded nav item names with `t()` calls in `src/components/Header.tsx` — move `navItems` array inside component body to use hook, translate login form strings +- [x] T009 [P] [US1] Replace hardcoded title and CTA button text with `t()` calls in `src/components/Hero.tsx` +- [x] T010 [P] [US1] Replace flip card titles/subtitles/back text, "Built for" section, and static card titles/descriptions with `t()` calls in `src/components/Benefits.tsx` — move data arrays inside component body +- [x] T011 [P] [US1] Replace questions array and CTA button text with `t()` calls in `src/components/Banner1.tsx` +- [x] T012 [P] [US1] Replace section title and result descriptions with `t()` calls in `src/components/RealResults.tsx` +- [x] T013 [P] [US1] Replace section title, step titles, durations, descriptions, and details with `t()` calls in `src/components/Timeline.tsx` — move timeline steps array inside component body +- [x] T014 [P] [US1] Replace questions and CTA button text with `t()` calls in `src/components/Banner2.tsx` +- [x] T015 [P] [US1] Replace section title, metric labels/values, alternative names, and footer text with `t()` calls in `src/components/ComparisonTable.tsx` — move comparison data inside component body +- [x] T016 [P] [US1] Replace "Recent Updates" title, "View All Posts" link text with `t()` calls, and update date locale from hardcoded `'en-US'` to `lang === 'uk' ? 'uk-UA' : 'en-GB'` in `src/components/BlogSection.tsx` +- [x] T017 [P] [US1] Replace section title and UI text with `t()` calls in `src/components/ResourcesSection.tsx` +- [x] T018 [P] [US1] Replace heading and subtitle with `t()` calls in `src/components/ContactSection.tsx` +- [x] T019 [P] [US1] Replace copyright text and link labels with `t()` calls in `src/components/Footer.tsx` +- [x] T020 [P] [US1] Replace banner text and button labels with `t()` calls in `src/components/CookieConsent.tsx` + +**Checkpoint**: All homepage static text switches between English and Ukrainian via the Header toggle. MVP deliverable. + +--- + +## Phase 4: User Story 2 — Submit Forms in Preferred Language (Priority: P2) + +**Goal**: Contact form, chatbot lead form — all labels, placeholders, validation messages, and success/error messages display in the selected language. + +**Independent Test**: Switch to Ukrainian → open contact form → verify all labels/placeholders are in Ukrainian → submit with invalid data → verify validation messages in Ukrainian → submit successfully → verify success message in Ukrainian. Repeat for chatbot lead form. + +### Implementation for User Story 2 + +- [x] T021 [P] [US2] Replace form title, labels ("Full Name", "Job Title / Role", "Work Email", etc.), placeholders, submit button text ("Submit a request" / "Sending..."), success message, and validation error messages with `t()` calls in `src/components/ContactForm.tsx` +- [x] T022 [P] [US2] Replace chatbot lead form labels ("Your name *", "Email *", "Company (optional)"), placeholders, consent text ("I agree to the processing..."), and button text with `t()` calls in `src/components/ChatLeadForm.tsx` +- [x] T023 [P] [US2] Replace quote form labels and placeholders with `t()` calls in `src/components/QuoteForm.tsx` + +**Checkpoint**: All forms display labels, placeholders, validation, and success messages in the selected language. + +--- + +## Phase 5: User Story 3 — Language Preference Persistence (Priority: P3) + +**Goal**: Selected language persists across browser sessions via localStorage. + +**Independent Test**: Select Ukrainian → close tab → reopen site → verify it loads in Ukrainian. Clear browser data → reopen → verify English default. + +### Implementation for User Story 3 + +- [x] T024 [US3] Verify and ensure `LanguageContext.tsx` reads from `localStorage` on initialization in `src/i18n/LanguageContext.tsx` — confirm `useState` initializer reads `localStorage.getItem('aimpress_lang')`, confirm `setLang` writes to localStorage, confirm `document.documentElement.lang` is set on mount +- [x] T025 [US3] Verify the Header toggle correctly reflects the persisted language on page load in `src/components/Header.tsx` — ensure the toggle display ("Eng"/"Ukr") matches the stored preference, not a hardcoded default + +**Checkpoint**: Language preference persists across page refreshes and browser sessions. + +--- + +## Phase 6: User Story 4 — Chatbot Conversations in Preferred Language (Priority: P4) + +**Goal**: Chatbot UI displays in the selected language and AI responds in the matching language. + +**Independent Test**: Switch to Ukrainian → open chatbot → verify greeting, status text ("Online"), and input placeholder are in Ukrainian → send a message → verify AI responds in Ukrainian. + +### Implementation for User Story 4 + +- [x] T026 [P] [US4] Replace greeting text with `t()` call in `src/components/ChatBubble.tsx` +- [x] T027 [P] [US4] Replace "Online" status text, welcome message with `t()` calls in `src/components/ChatWindow.tsx` +- [x] T028 [P] [US4] Replace "Type a message..." placeholder with `t()` call in `src/components/ChatInput.tsx` +- [x] T029 [US4] Pass `lang` from `useTranslation()` context to the `useChat` hook in `src/components/ChatWidget.tsx` — pass as parameter to `useChat` +- [x] T030 [US4] Add `language` field to the request body sent to `/api/chat` in `src/hooks/useChat.ts` — accept `lang` parameter, include `language: lang` in fetch body +- [x] T031 [US4] Add `language: str = "en"` field to `ChatRequest` model in `chatbot-api/models.py` +- [x] T032 [US4] Prepend language hint to messages when `req.language == "uk"` in `chatbot-api/main.py` — add synthetic user/assistant message pair before the conversation to guide Claude to respond in Ukrainian + +**Checkpoint**: Chatbot UI is fully translated and AI responds in the selected language. + +--- + +## Phase 7: Polish & Cross-Cutting Concerns + +**Purpose**: SEO, subpage translations, and final verification + +- [x] T033 [P] [US1] Replace all hardcoded text (hero, story, differentiators, values, founder bio, industries, CTA) with `t()` calls in `src/pages/AboutPage.tsx` — move data arrays (differentiators, values, industries) inside component body +- [x] T034 [P] [US1] Replace service titles, descriptions, includes list, and CTA with `t()` calls in `src/pages/ServicesPage.tsx` +- [x] T035 [P] [US1] Replace pricing labels, tier names, feature lists, and CTA with `t()` calls in `src/pages/PricingPage.tsx` +- [x] T036 [P] [US1] Replace page title and UI chrome with `t()` calls in `src/pages/BlogPage.tsx` — update date locale +- [x] T037 [P] [US1] Replace "Back to blog" link text with `t()` call and update date formatting locale in `src/pages/BlogPostPage.tsx` +- [x] T038 Add `` via Helmet and pass translated SEO title/description to each page's `` component — update SEO component to accept lang, add `seo.*` translation keys for all pages +- [x] T039 Visual review of all pages in Ukrainian — check for text overflow, broken layouts, or missing translations. Fix any CSS issues caused by longer Ukrainian text (buttons, nav items, cards). +- [x] T040 Run quickstart.md validation — verify the developer workflow (adding a new key, using `t()` in a component, date formatting) works as documented + +--- + +## Dependencies & Execution Order + +### Phase Dependencies + +- **Setup (Phase 1)**: No dependencies — can start immediately +- **Foundational (Phase 2)**: Depends on Phase 1 completion — BLOCKS all user stories +- **User Stories (Phases 3-6)**: All depend on Phase 2 completion + - US1 (Phase 3): No dependencies on other stories + - US2 (Phase 4): No dependencies on other stories (forms are separate components) + - US3 (Phase 5): Largely verification — persistence is built into Phase 2's LanguageContext + - US4 (Phase 6): No dependencies on other stories (chatbot is separate component tree) +- **Polish (Phase 7)**: Depends on Phase 2 completion (can run in parallel with user stories, but best after US1) + +### User Story Dependencies + +- **US1 (P1)**: After Phase 2 — no cross-story dependencies +- **US2 (P2)**: After Phase 2 — no cross-story dependencies +- **US3 (P3)**: After Phase 2 — verification only (persistence built into LanguageContext) +- **US4 (P4)**: After Phase 2 — no cross-story dependencies, but includes backend changes + +### Parallel Opportunities + +- T009–T020 (US1 homepage components): ALL parallelizable — different files, no dependencies +- T021–T023 (US2 forms): ALL parallelizable — different files +- T026–T028 (US4 chat UI): ALL parallelizable — different files +- T033–T037 (Polish subpages): ALL parallelizable — different files +- US1, US2, US4 can all proceed in parallel after Phase 2 + +--- + +## Parallel Example: User Story 1 + +```bash +# After Phase 2 is complete, launch all homepage component translations in parallel: +Task: "T009 — Replace text in Hero.tsx" +Task: "T010 — Replace text in Benefits.tsx" +Task: "T011 — Replace text in Banner1.tsx" +Task: "T012 — Replace text in RealResults.tsx" +Task: "T013 — Replace text in Timeline.tsx" +Task: "T014 — Replace text in Banner2.tsx" +Task: "T015 — Replace text in ComparisonTable.tsx" +Task: "T016 — Replace text in BlogSection.tsx" +Task: "T017 — Replace text in ResourcesSection.tsx" +Task: "T018 — Replace text in ContactSection.tsx" +Task: "T019 — Replace text in Footer.tsx" +Task: "T020 — Replace text in CookieConsent.tsx" +``` + +--- + +## Implementation Strategy + +### MVP First (User Story 1 Only) + +1. Complete Phase 1: Create i18n module + translation files (T001–T005) +2. Complete Phase 2: Wire provider + Header toggle (T006–T007) +3. Complete Phase 3: Translate all homepage components (T008–T020) +4. **STOP and VALIDATE**: Toggle language, verify all homepage text switches +5. Deploy/demo if ready + +### Incremental Delivery + +1. Phase 1 + 2 → Foundation ready +2. Add US1 (homepage) → Test → Deploy (MVP!) +3. Add US2 (forms) → Test → Deploy +4. Add US3 (persistence verification) → Test → Deploy +5. Add US4 (chatbot) → Test → Deploy +6. Polish (subpages + SEO) → Final deploy + +--- + +## Notes + +- [P] tasks = different files, no dependencies +- [Story] label maps task to specific user story for traceability +- Translation files (T002, T003) are the most labor-intensive tasks — ~200 keys each +- Ukrainian translations should be reviewed by a native speaker before final deploy +- Privacy Policy and Terms of Use pages remain English-only (legal review needed for translation) +- Analytics event names stay in English regardless of UI language diff --git a/src/components/Banner1.tsx b/src/components/Banner1.tsx index 2b04ed0..4e4fc07 100644 --- a/src/components/Banner1.tsx +++ b/src/components/Banner1.tsx @@ -3,16 +3,18 @@ import { motion } from 'framer-motion'; import Modal from './Modal'; import ContactForm from './ContactForm'; import './Banner1.css'; - -const questions = [ - 'How many employees in your company?', - 'How many hours per day on repetitive tasks?', - 'Which processes do you want to automate?', -]; +import { useTranslation } from '../i18n'; const Banner1: React.FC = () => { + const { t } = useTranslation(); const [isModalOpen, setIsModalOpen] = useState(false); + const questions = [ + t('banner1.q1'), + t('banner1.q2'), + t('banner1.q3'), + ]; + return (
@@ -52,7 +54,7 @@ const Banner1: React.FC = () => { viewport={{ once: true }} transition={{ duration: 0.4, delay: 0.6 }} > - Get Your Free Consultation + {t('banner1.cta')}
diff --git a/src/components/Banner2.tsx b/src/components/Banner2.tsx index 98aecee..56b5c21 100644 --- a/src/components/Banner2.tsx +++ b/src/components/Banner2.tsx @@ -3,8 +3,10 @@ import { motion } from 'framer-motion'; import Modal from './Modal'; import ContactForm from './ContactForm'; import './Banner2.css'; +import { useTranslation } from '../i18n'; const Banner2: React.FC = () => { + const { t } = useTranslation(); const [isModalOpen, setIsModalOpen] = useState(false); const [isHovered, setIsHovered] = useState(false); @@ -39,7 +41,7 @@ const Banner2: React.FC = () => { {/* Rotating text */} Get Your Free Consultation diff --git a/src/components/Benefits.tsx b/src/components/Benefits.tsx index 9f534ad..66882a0 100644 --- a/src/components/Benefits.tsx +++ b/src/components/Benefits.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { motion } from 'framer-motion'; import FlipCard from './FlipCard'; import './Benefits.css'; +import { useTranslation } from '../i18n'; const TRACKERS = Array.from({ length: 25 }, (_, i) => i + 1); @@ -19,37 +20,48 @@ const TiltCard: React.FC<{ children: React.ReactNode; className?: string }> = ({ }; const Benefits: React.FC = () => { + const { t } = useTranslation(); + + const flipCards = [ + { + frontImage: "/bg/cost scale.png", + frontTitle: t('benefits.card1.front'), + frontSubtitle: t('benefits.card1.subtitle'), + backTitle: t('benefits.card1.front'), + backSubtitle: t('benefits.card1.subtitle'), + backText: t('benefits.card1.back'), + }, + { + frontImage: "/bg/Accuracy.png", + frontTitle: t('benefits.card2.front'), + frontSubtitle: t('benefits.card2.subtitle'), + backTitle: t('benefits.card2.front'), + backSubtitle: t('benefits.card2.subtitle'), + backText: t('benefits.card2.back'), + }, + { + frontImage: "/bg/Availability.png", + frontTitle: t('benefits.card3.front'), + frontSubtitle: t('benefits.card3.subtitle'), + backTitle: t('benefits.card3.front'), + backSubtitle: t('benefits.card3.subtitle'), + backText: t('benefits.card3.back'), + }, + ]; + + const staticCards = [ + { icon: "/icons/chatbot icon.svg", title: t('benefits.static1.title'), desc: t('benefits.static1.desc') }, + { icon: "/icons/Business Process icon.svg", title: t('benefits.static2.title'), desc: t('benefits.static2.desc'), descClass: "small-text" }, + { icon: "/icons/Content Farms icon.svg", title: t('benefits.static3.title'), desc: t('benefits.static3.desc') }, + { icon: "/icons/Marketing Automation icon.svg", title: t('benefits.static4.title'), desc: t('benefits.static4.desc') }, + ]; + return (
{/* Flip Cards Grid */}
- {[ - { - frontImage: "/bg/cost scale.png", - frontTitle: "Cost & Scale", - frontSubtitle: "Elastic Growth, Zero Headcount", - backTitle: "Cost & Scale", - backSubtitle: "Elastic Growth, Zero Headcount", - backText: "Scale 10x without hiring. AI assistants handle peak loads automatically, keeping your operations elastic and efficient.", - }, - { - frontImage: "/bg/Accuracy.png", - frontTitle: "Accuracy", - frontSubtitle: "Machine Learning Precision (99.9%)", - backTitle: "Accuracy", - backSubtitle: "Machine Learning Precision (99.9%)", - backText: "Eliminate human error. Our ML-validated workflows guarantee data integrity across CRM, finance, and logistics systems.", - }, - { - frontImage: "/bg/Availability.png", - frontTitle: "Availability", - frontSubtitle: "True 24/7/365 Operations", - backTitle: "Availability", - backSubtitle: "True 24/7/365 Operations", - backText: "Your AI workforce never sleeps, takes breaks, or burns out. Serve global clients nonstop.", - }, - ].map((card, i) => ( + {flipCards.map((card, i) => ( {
-

Built for Local Entrepreneurs Like You

+

{t('benefits.builtTitle')}

- AImpress turns chaos into a system. We build automations that save up to 75% of your time and boost sales. + {t('benefits.builtDesc')}

- {[ - { icon: "/icons/chatbot icon.svg", title: <>Chatbots &
AI Assistants, desc: "→ instant answers 24/7" }, - { icon: "/icons/Business Process icon.svg", title: <>Business Process
Automation (n8n, Make.com), desc: "→ CRM, email, finance, inventory", descClass: "small-text" }, - { icon: "/icons/Content Farms icon.svg", title: <>Content Farms &
AI Copywriting, desc: "→ AI creates posts, articles, product descriptions" }, - { icon: "/icons/Marketing Automation icon.svg", title: <>Marketing Automation
(email, CRM, ads), desc: "→ campaigns, lead nurturing, ads on autopilot" }, - ].map((card, i) => ( + {staticCards.map((card, i) => ( { + const { t, lang } = useTranslation(); + + function formatDate(dateStr: string) { + const [y, m, d] = dateStr.split('-').map(Number); + return new Date(y, m - 1, d).toLocaleDateString(lang === 'uk' ? 'uk-UA' : 'en-GB', { + month: 'short', day: '2-digit', year: 'numeric', + }); + } const [posts, setPosts] = useState([]); useEffect(() => { @@ -26,7 +28,7 @@ const BlogSection: React.FC = () => { return (
-

Recent Updates

+

{t('blogSection.title')}

{posts.map((post, index) => ( @@ -50,7 +52,7 @@ const BlogSection: React.FC = () => { {formatDate(post.date)}

{post.title}

{post.excerpt}

- Read More → + {t('blogSection.readMore')}
@@ -58,7 +60,7 @@ const BlogSection: React.FC = () => {
- View All Posts → + {t('blogSection.viewAll')}
diff --git a/src/components/ChatBubble.tsx b/src/components/ChatBubble.tsx index c9e13b1..df610a1 100644 --- a/src/components/ChatBubble.tsx +++ b/src/components/ChatBubble.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { motion, AnimatePresence } from 'framer-motion'; +import { useTranslation } from '../i18n'; import './ChatBubble.css'; interface ChatBubbleProps { @@ -15,6 +16,8 @@ const ChatBubble: React.FC = ({ showGreeting, onDismissGreeting, }) => { + const { t } = useTranslation(); + if (isOpen) return null; return ( @@ -28,7 +31,7 @@ const ChatBubble: React.FC = ({ exit={{ opacity: 0, y: 8, scale: 0.9 }} transition={{ duration: 0.25 }} > -

Hi! How can I help you today?

+

{t('chat.greeting')}

); diff --git a/src/components/ChatWidget.tsx b/src/components/ChatWidget.tsx index 1e28900..7d7db6d 100644 --- a/src/components/ChatWidget.tsx +++ b/src/components/ChatWidget.tsx @@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback } from 'react'; import { AnimatePresence } from 'framer-motion'; import mixpanel from 'mixpanel-browser'; import { getConsent } from './CookieConsent'; +import { useTranslation } from '../i18n'; import { useChat } from '../hooks/useChat'; import ChatBubble from './ChatBubble'; import ChatWindow from './ChatWindow'; @@ -11,9 +12,10 @@ const GREETING_KEY = 'aimpress_chat_greeted'; const GREETING_DELAY = 30000; // 30 seconds const ChatWidget: React.FC = () => { + const { lang } = useTranslation(); const [isOpen, setIsOpen] = useState(false); const [showGreeting, setShowGreeting] = useState(false); - const { messages, loading, rateLimited, lead, sendMessage, setLead, clearChat, sessionId } = useChat(); + const { messages, loading, rateLimited, lead, sendMessage, setLead, clearChat, sessionId } = useChat(lang); const openedAtRef = React.useRef(0); // Auto-greeting after 30s (once per session) diff --git a/src/components/ChatWindow.tsx b/src/components/ChatWindow.tsx index 890190d..3625bdd 100644 --- a/src/components/ChatWindow.tsx +++ b/src/components/ChatWindow.tsx @@ -1,5 +1,6 @@ import React, { useEffect, useRef } from 'react'; import { motion } from 'framer-motion'; +import { useTranslation } from '../i18n'; import ChatMessage from './ChatMessage'; import ChatInput from './ChatInput'; import ChatLeadForm from './ChatLeadForm'; @@ -28,6 +29,7 @@ const ChatWindow: React.FC = ({ onClear, onLeadSubmit, }) => { + const { t } = useTranslation(); const messagesEndRef = useRef(null); useEffect(() => { @@ -50,7 +52,7 @@ const ChatWindow: React.FC = ({ @@ -111,7 +113,7 @@ const ComparisonTable: React.FC = () => { viewport={{ once: true, margin: "-60px" }} transition={{ duration: 0.5, delay: 0.15 }} > - The alternatives + {t('comparison.altHeading')} { transition={{ duration: 0.25 }} >
- Traditional Agency + {t('comparison.alt1')}
{metrics[activeMetric].label} {metrics[activeMetric].others[0]} @@ -134,7 +136,7 @@ const ComparisonTable: React.FC = () => {
- In-House Hire + {t('comparison.alt2')}
{metrics[activeMetric].label} {metrics[activeMetric].others[1]} @@ -146,9 +148,7 @@ const ComparisonTable: React.FC = () => { -

- Switch from legacy methods — save up to 70% on costs and 30+ hours per week. -

+

diff --git a/src/components/ContactForm.tsx b/src/components/ContactForm.tsx index 558f559..535da27 100644 --- a/src/components/ContactForm.tsx +++ b/src/components/ContactForm.tsx @@ -1,6 +1,7 @@ import React, { useState, type ChangeEvent, type FormEvent } from 'react'; import { motion } from 'framer-motion'; import mixpanel from 'mixpanel-browser'; +import { useTranslation } from '../i18n'; import './ContactForm.css'; interface FormData { @@ -17,6 +18,7 @@ interface ContactFormProps { } const ContactForm: React.FC = () => { + const { t } = useTranslation(); const [formData, setFormData] = useState({ fullName: '', workEmail: '', @@ -74,24 +76,24 @@ const ContactForm: React.FC = () => { return (
-

Get in touch:

+

{t('contactForm.title')}

{status === 'success' ? (
-

Thank you!

-

We have received your request and will contact you shortly.

- +

{t('contactForm.successTitle')}

+

{t('contactForm.successText')}

+
) : (
{[ - { label: 'Full Name', name: 'fullName', type: 'text', placeholder: 'John Doe' }, - { label: 'Job Title / Role', name: 'jobTitle', type: 'text', placeholder: 'Project Manager' }, - { label: 'Work Email', name: 'workEmail', type: 'email', placeholder: 'john@company.com' }, - { label: 'Automation Need', name: 'automationNeed', type: 'text', placeholder: 'Workflow optimization' }, - { label: 'Company Name', name: 'companyName', type: 'text', placeholder: 'Tech Solutions Inc.' }, - { label: 'Phone Number', name: 'phoneNumber', type: 'tel', placeholder: '+44...' }, + { label: t('contactForm.fullName'), name: 'fullName', type: 'text', placeholder: t('contactForm.fullNamePlaceholder') }, + { label: t('contactForm.jobTitle'), name: 'jobTitle', type: 'text', placeholder: t('contactForm.jobTitlePlaceholder') }, + { label: t('contactForm.email'), name: 'workEmail', type: 'email', placeholder: t('contactForm.emailPlaceholder') }, + { label: t('contactForm.need'), name: 'automationNeed', type: 'text', placeholder: t('contactForm.needPlaceholder') }, + { label: t('contactForm.company'), name: 'companyName', type: 'text', placeholder: t('contactForm.companyPlaceholder') }, + { label: t('contactForm.phone'), name: 'phoneNumber', type: 'tel', placeholder: t('contactForm.phonePlaceholder') }, ].map((field, i) => ( = () => { whileHover={{ scale: 1.03 }} whileTap={{ scale: 0.97 }} > - {status === 'submitting' ? 'Sending...' : 'Submit a request'} + {status === 'submitting' ? t('contactForm.sending') : t('contactForm.submit')} - {status === 'error' &&

Something went wrong. Please try again.

} + {status === 'error' &&

{t('contactForm.error')}

}
)} diff --git a/src/components/ContactSection.tsx b/src/components/ContactSection.tsx index a0c717c..6ce66ae 100644 --- a/src/components/ContactSection.tsx +++ b/src/components/ContactSection.tsx @@ -1,9 +1,11 @@ import React from 'react'; import { motion } from 'framer-motion'; +import { useTranslation } from '../i18n'; import ContactForm from './ContactForm'; import './ContactSection.css'; const ContactSection: React.FC = () => { + const { t } = useTranslation(); return (
@@ -15,9 +17,9 @@ const ContactSection: React.FC = () => { transition={{ duration: 0.5 }} >
-

Ready to Automate?

+

{t('contactSection.title')}

- Stop wasting time on routine. Start scaling with AI today. + {t('contactSection.subtitle')}

diff --git a/src/components/CookieConsent.tsx b/src/components/CookieConsent.tsx index 8faff30..a476711 100644 --- a/src/components/CookieConsent.tsx +++ b/src/components/CookieConsent.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; import { Link } from 'react-router-dom'; +import { useTranslation } from '../i18n'; import { initAnalytics } from '../analytics'; import './CookieConsent.css'; @@ -12,6 +13,7 @@ export function getConsent(): ConsentValue | null { } const CookieConsent = () => { + const { t } = useTranslation(); const [visible, setVisible] = useState(false); useEffect(() => { @@ -40,16 +42,15 @@ const CookieConsent = () => {

- We use cookies and similar technologies to analyse website traffic and improve your - experience. By clicking "Accept", you consent to the use of analytics cookies. - See our Privacy Policy for details. + {t('cookie.text')}{' '} + {t('cookie.privacyLink')}

diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 075e97c..9114bde 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,8 +1,10 @@ import React from 'react'; import { Link } from 'react-router-dom'; +import { useTranslation } from '../i18n'; import './Footer.css'; const Footer: React.FC = () => { + const { t } = useTranslation(); return (
@@ -29,11 +31,11 @@ const Footer: React.FC = () => {
- Privacy Policy + {t('footer.privacy')} | - Terms of Use + {t('footer.terms')}
-

© 2026 AImpress LTD. All rights reserved.

+

{t('footer.copyright')}

diff --git a/src/components/Header.tsx b/src/components/Header.tsx index bd88576..8a0c607 100644 --- a/src/components/Header.tsx +++ b/src/components/Header.tsx @@ -1,29 +1,36 @@ import React, { useState, useEffect } from 'react'; import { useNavigate, useLocation } from 'react-router-dom'; import { motion } from 'framer-motion'; +import { useTranslation } from '../i18n'; import Modal from './Modal'; import './Header.css'; -const navItems = [ - { name: 'Home', link: '/' }, - { name: 'About Us', link: '/about' }, - { name: 'Services', link: '/services' }, - { name: 'Pricing', link: '/pricing' }, - { name: 'Blog', link: '/blog' }, - { name: 'Contacts', link: '#contact' }, -]; - const Header: React.FC = () => { - const [activeTab, setActiveTab] = useState('Home'); + const { t, lang, setLang } = useTranslation(); + + const navItems = [ + { name: t('header.nav.home'), link: '/' }, + { name: t('header.nav.about'), link: '/about' }, + { name: t('header.nav.services'), link: '/services' }, + { name: t('header.nav.pricing'), link: '/pricing' }, + { name: t('header.nav.blog'), link: '/blog' }, + { name: t('header.nav.contacts'), link: '#contact' }, + ]; + + const [activeTab, setActiveTab] = useState(navItems[0].name); const [hoveredTab, setHoveredTab] = useState(null); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isLangOpen, setIsLangOpen] = useState(false); const [isLoginOpen, setIsLoginOpen] = useState(false); - const [currentLang, setCurrentLang] = useState('Eng'); const [isScrolled, setIsScrolled] = useState(false); const navigate = useNavigate(); const location = useLocation(); + // Update activeTab when language changes + useEffect(() => { + setActiveTab(navItems[0].name); + }, [lang]); + useEffect(() => { const onScroll = () => { setIsScrolled(window.scrollY > 50); @@ -38,8 +45,8 @@ const Header: React.FC = () => { setIsMobileMenuOpen(false); }, [location.pathname]); - const handleLangSelect = (lang: string) => { - setCurrentLang(lang); + const handleLangSelect = (selectedLang: 'en' | 'uk') => { + setLang(selectedLang); setIsLangOpen(false); }; @@ -63,6 +70,8 @@ const Header: React.FC = () => { setIsMobileMenuOpen(false); }; + const currentLangLabel = lang === 'en' ? t('header.lang.en') : t('header.lang.uk'); + return ( <>
@@ -82,7 +91,7 @@ const Header: React.FC = () => { const isActive = (hoveredTab || activeTab) === item.name; return (
  • setHoveredTab(item.name)} onClick={() => setActiveTab(item.name)} @@ -115,7 +124,7 @@ const Header: React.FC = () => { onClick={() => setIsLangOpen(!isLangOpen)} > Language - {currentLang} + {currentLangLabel}
  • {isLangOpen && ( @@ -126,21 +135,21 @@ const Header: React.FC = () => { exit={{ opacity: 0, y: 10 }} >
    handleLangSelect('Eng')} + className={`lang-option ${lang === 'en' ? 'active' : ''}`} + onClick={() => handleLangSelect('en')} > - Eng + {t('header.lang.en')}
    handleLangSelect('Ukr')} + className={`lang-option ${lang === 'uk' ? 'active' : ''}`} + onClick={() => handleLangSelect('uk')} > - Ukr + {t('header.lang.uk')}
    )}
    - +
    +

    - Don't have an account? Sign up + {t('header.loginModal.signupPrompt')} {t('header.loginModal.signupLink')}

    diff --git a/src/components/Hero.tsx b/src/components/Hero.tsx index 9b99248..a4ad5e1 100644 --- a/src/components/Hero.tsx +++ b/src/components/Hero.tsx @@ -3,8 +3,10 @@ import { motion, useTransform, useMotionValue } from 'framer-motion'; import './Hero.css'; import ContactForm from './ContactForm'; import Modal from './Modal'; +import { useTranslation } from '../i18n'; const Hero: React.FC = () => { + const { t } = useTranslation(); const [isModalOpen, setIsModalOpen] = useState(false); const x = useMotionValue(0); const y = useMotionValue(0); @@ -47,9 +49,9 @@ const Hero: React.FC = () => {
    - Build. - Automate. - Impress. + {t('hero.circle1')} + {t('hero.circle2')} + {t('hero.circle3')}
    @@ -81,10 +83,10 @@ const Hero: React.FC = () => {

    - Stop Hiring. Start Scaling. Deploy Your AI Digital Workforce. + {t('hero.title')}

    diff --git a/src/components/QuoteForm.tsx b/src/components/QuoteForm.tsx index eaabe01..6c2a7ac 100644 --- a/src/components/QuoteForm.tsx +++ b/src/components/QuoteForm.tsx @@ -1,6 +1,7 @@ import React, { useState, type ChangeEvent, type FormEvent } from 'react'; import { motion } from 'framer-motion'; import mixpanel from 'mixpanel-browser'; +import { useTranslation } from '../i18n'; import './QuoteForm.css'; interface QuoteData { @@ -13,23 +14,25 @@ interface QuoteData { projectDescription: string; } -const serviceOptions = [ - 'Workflow Automation Implementation', - 'System Integration & Synchronisation', - 'CRM Workflow Optimisation', - 'Marketing Automation Setup', - 'AI Integration & Enhancement', - 'Infrastructure Setup & Configuration', - 'Support Retainer', - 'Training & Workshop', - 'Other / Not sure yet', -]; - interface QuoteFormProps { onClose?: () => void; } const QuoteForm: React.FC = () => { + const { t } = useTranslation(); + + const serviceOptions = [ + t('quoteForm.service1'), + t('quoteForm.service2'), + t('quoteForm.service3'), + t('quoteForm.service4'), + t('quoteForm.service5'), + t('quoteForm.service6'), + t('quoteForm.service7'), + t('quoteForm.service8'), + t('quoteForm.service9'), + ]; + const [formData, setFormData] = useState({ fullName: '', workEmail: '', @@ -88,22 +91,22 @@ const QuoteForm: React.FC = () => { }; const inputFields = [ - { label: 'Full Name', name: 'fullName', type: 'text', placeholder: 'John Doe' }, - { label: 'Job Title / Role', name: 'jobTitle', type: 'text', placeholder: 'Project Manager' }, - { label: 'Work Email', name: 'workEmail', type: 'email', placeholder: 'john@company.com' }, - { label: 'Phone Number', name: 'phoneNumber', type: 'tel', placeholder: '+44...' }, - { label: 'Company Name', name: 'companyName', type: 'text', placeholder: 'Tech Solutions Ltd' }, + { label: t('quoteForm.fullName'), name: 'fullName', type: 'text', placeholder: t('quoteForm.fullNamePlaceholder') }, + { label: t('quoteForm.jobTitle'), name: 'jobTitle', type: 'text', placeholder: t('quoteForm.jobTitlePlaceholder') }, + { label: t('quoteForm.email'), name: 'workEmail', type: 'email', placeholder: t('quoteForm.emailPlaceholder') }, + { label: t('quoteForm.phone'), name: 'phoneNumber', type: 'tel', placeholder: t('quoteForm.phonePlaceholder') }, + { label: t('quoteForm.company'), name: 'companyName', type: 'text', placeholder: t('quoteForm.companyPlaceholder') }, ]; return (
    -

    Get Your Quote

    +

    {t('quoteForm.title')}

    {status === 'success' ? (
    -

    Thank you!

    -

    We've received your quote request. We'll review your requirements and get back to you within 24 hours with a detailed proposal.

    - +

    {t('quoteForm.successTitle')}

    +

    {t('quoteForm.successText')}

    +
    ) : (
    @@ -138,7 +141,7 @@ const QuoteForm: React.FC = () => { viewport={{ once: true }} transition={{ duration: 0.3, delay: 0.4 }} > - +