diff --git a/docs/feature-checklists/06-multilingual-ui.md b/docs/feature-checklists/06-multilingual-ui.md index 4d079b2..5409fdd 100644 --- a/docs/feature-checklists/06-multilingual-ui.md +++ b/docs/feature-checklists/06-multilingual-ui.md @@ -6,15 +6,15 @@ Make the product feel more Sri Lanka-aware by adding localized UI copy and light ## Checklist -- [ ] Review candidate multilingual dictionaries and language-detection behavior -- [ ] Decide the minimum viable language surface for this app -- [ ] Add localized labels and helper copy for the main UI flows -- [ ] Keep language changes lightweight and avoid duplicating business logic -- [ ] Verify English remains the default fallback -- [ ] Add at least one focused test for language selection or copy mapping -- [ ] Run `npm run lint` -- [ ] Run `npm run build` -- [ ] Verify the flow in a live browser session +- [x] Review candidate multilingual dictionaries and language-detection behavior +- [x] Decide the minimum viable language surface for this app +- [x] Add localized labels and helper copy for the main UI flows +- [x] Keep language changes lightweight and avoid duplicating business logic +- [x] Verify English remains the default fallback +- [x] Add at least one focused test for language selection or copy mapping +- [x] Run `npm run lint` +- [x] Run `npm run build` +- [x] Verify the flow in a live browser session - [ ] Commit, push branch, and open draft PR ## Notes @@ -22,3 +22,10 @@ Make the product feel more Sri Lanka-aware by adding localized UI copy and light - Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/lib/i18n/index.js` - Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/lib/i18n/en.js` - Candidate reference: `tmp_compare/Kapruka-Ai-Shopping-Agent/lib/i18n/si.js` +- The implementation stays frontend-only: a tiny UI copy map plus a header toggle for `en`, `si`, and `ta`. +- Focused check: `node --test --experimental-strip-types frontend/src/lib/ui-copy.spec.ts` +- Live browser verification used a Playwright fallback because the in-app Browser tool was not callable in this thread. +- Browser checks confirmed: + - English is the default UI language + - selecting `si` updates the visible hero copy + - switching back to `en` restores the English fallback diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 9767982..e63a4c9 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -8,6 +8,7 @@ import GiftAdvisor from "@/components/GiftAdvisor"; import { useCart } from "@/hooks/useCart"; import { useChat } from "@/hooks/useChat"; import { createRecognition, isSpeechRecognitionSupported, isSpeechSynthesisSupported, speakText, stopSpeaking, type RecognitionLike } from "@/lib/speech"; +import { detectUiLanguage, getUiCopy, type UiLanguage } from "@/lib/ui-copy"; import { getBackendMeta, type BackendMeta, updateCheckoutInfo, updateBudget, type CheckoutInfoPayload } from "@/lib/api"; function generateSessionId() { @@ -56,23 +57,17 @@ function loadVoiceRepliesEnabled() { } } -const suggestions = [ - { label: "Restock groceries", prompt: "Help me restock weekly groceries on Kapruka" }, - { label: "Birthday cake", prompt: "Find birthday cakes on Kapruka under Rs. 10,000" }, - { label: "Send flowers", prompt: "I want to send flowers to someone special" }, - { label: "Party supplies", prompt: "Show party supplies for 10 people" }, -]; +const TRACK_ORDER_PROMPT: Record = { + en: "I want to track my order. Please ask me for the Kapruka order number if needed.", + si: "මට මගේ ඇණවුම ලුහුබඳින්න ඕන. අවශ්‍ය නම් Kapruka order number එක ඉල්ලන්න.", + ta: "என் order-ஐ track செய்ய வேண்டும். தேவைப்பட்டால் Kapruka order number கேளுங்கள்.", +}; -const TRACK_ORDER_PROMPT = "I want to track my order. Please ask me for the Kapruka order number if needed."; -const CATEGORY_PROMPT = "Show me Kapruka product categories"; - -const chatActions = [ - { label: "Gift advisor", kind: "advisor" as const }, - { label: "Birthday gifts", prompt: "Find 5 birthday gift ideas on Kapruka under Rs. 5,000" }, - { label: "Anniversary gifts", prompt: "Show anniversary gift ideas on Kapruka for my partner" }, - { label: "Track order", prompt: TRACK_ORDER_PROMPT }, - { label: "Show categories", prompt: CATEGORY_PROMPT }, -]; +const CATEGORY_PROMPT: Record = { + en: "Show me Kapruka product categories", + si: "Kapruka නිෂ්පාදන කාණ්ඩ පෙන්නන්න", + ta: "Kapruka product categories காட்டு", +}; function CartIcon({ className = "" }: { className?: string }) { return ( @@ -129,6 +124,7 @@ export default function Home() { const [budgetDraft, setBudgetDraft] = useState(""); const [budgetSaving, setBudgetSaving] = useState(false); const [inkPosition, setInkPosition] = useState({ x: "50%", y: "42%" }); + const [uiLanguage, setUiLanguage] = useState(() => detectUiLanguage(typeof navigator === "undefined" ? "" : navigator.language)); const [voiceInputSupported] = useState(isSpeechRecognitionSupported); const [voiceRepliesSupported] = useState(isSpeechSynthesisSupported); const [voiceRepliesEnabled, setVoiceRepliesEnabled] = useState(loadVoiceRepliesEnabled); @@ -355,6 +351,16 @@ export default function Home() { }, [submitCurrentMessage]); const cartCount = cart.items.reduce((sum, item) => sum + item.quantity, 0); + const uiCopy = getUiCopy(uiLanguage); + const trackOrderPrompt = TRACK_ORDER_PROMPT[uiLanguage]; + const categoryPrompt = CATEGORY_PROMPT[uiLanguage]; + const chatActions = [ + { label: uiCopy.giftAdvisor, kind: "advisor" as const }, + { label: uiCopy.birthdayGifts, prompt: uiCopy.suggestions[1].prompt }, + { label: uiCopy.anniversaryGifts, prompt: uiLanguage === "en" ? "Show anniversary gift ideas on Kapruka for my partner" : uiLanguage === "si" ? "මගේ partnerට anniversary gift ideas Kapruka එකෙන් පෙන්නන්න" : "என் partner-க்கு anniversary gift ideas Kapruka-வில் காட்டு" }, + { label: uiCopy.trackOrder, prompt: trackOrderPrompt }, + { label: uiCopy.showCategories, prompt: categoryPrompt }, + ]; const inkStyle = { "--ink-x": inkPosition.x, "--ink-y": inkPosition.y, @@ -369,7 +375,7 @@ export default function Home() { >
-

Cart

+

{uiCopy.cart}

{cartCount} {cartCount === 1 ? "item" : "items"}

@@ -445,10 +451,10 @@ export default function Home() {
- KaprukaAI + {uiCopy.appName.replace("AI", "")}AI
- Shopping assistant + {uiCopy.assistantLabel} {backendMeta && (
@@ -462,6 +468,22 @@ export default function Home() {
)} +
+ {(["en", "si", "ta"] as UiLanguage[]).map((language) => ( + + ))} +
{cartCount > 0 ? ( ) : null} - {suggestions.map((suggestion) => ( + {uiCopy.suggestions.map((suggestion) => ( ) : null}
@@ -676,7 +698,7 @@ export default function Home() { submitCurrentMessage(); } }} - placeholder="Search products, ask for recommendations..." + placeholder={uiCopy.inputPlaceholder} disabled={isStreaming} className="h-12 min-w-0 flex-1 rounded-xl border border-border bg-surface-2 px-5 text-sm text-ink outline-none transition placeholder:text-muted focus:border-ink focus:bg-surface focus:shadow-[0_0_0_3px_rgba(37,36,31,0.08)] disabled:opacity-40" /> diff --git a/frontend/src/lib/ui-copy.spec.ts b/frontend/src/lib/ui-copy.spec.ts new file mode 100644 index 0000000..4cc2dd8 --- /dev/null +++ b/frontend/src/lib/ui-copy.spec.ts @@ -0,0 +1,10 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +import { detectUiLanguage, getUiCopy } from "./ui-copy.ts"; + +test("detectUiLanguage falls back to English and maps Sinhala locales", () => { + assert.equal(detectUiLanguage("si-LK"), "si"); + assert.equal(detectUiLanguage("en-US"), "en"); + assert.equal(getUiCopy("ta").trackMyOrder.length > 0, true); +}); diff --git a/frontend/src/lib/ui-copy.ts b/frontend/src/lib/ui-copy.ts new file mode 100644 index 0000000..7f051e1 --- /dev/null +++ b/frontend/src/lib/ui-copy.ts @@ -0,0 +1,138 @@ +"use client"; + +export type UiLanguage = "en" | "si" | "ta"; + +type Suggestion = { + label: string; + prompt: string; +}; + +type UiCopy = { + appName: string; + assistantLabel: string; + heroTitle: string; + heroDescription: string; + heroExample: string; + capabilityGiftTitle: string; + capabilityGiftBody: string; + capabilityTrackingTitle: string; + capabilityTrackingBody: string; + capabilityCheckoutTitle: string; + capabilityCheckoutBody: string; + openGiftAdvisor: string; + trackMyOrder: string; + browseCategories: string; + openCheckout: string; + cart: string; + giftAdvisor: string; + birthdayGifts: string; + anniversaryGifts: string; + trackOrder: string; + showCategories: string; + inputPlaceholder: string; + suggestions: Suggestion[]; +}; + +const copy: Record = { + en: { + appName: "KaprukaAI", + assistantLabel: "Shopping assistant", + heroTitle: "Shop with an AI assistant", + heroDescription: "Discover products in chat, track orders, and move into checkout without leaving the conversation.", + heroExample: "Try: help me restock my weekly groceries", + capabilityGiftTitle: "Gift Picks", + capabilityGiftBody: "Use the advisor or jump straight into birthday, anniversary, and grocery prompts.", + capabilityTrackingTitle: "Order Tracking", + capabilityTrackingBody: "Ask for a tracking update anytime and the assistant will prompt for the order number if needed.", + capabilityCheckoutTitle: "Checkout Flow", + capabilityCheckoutBody: "Save delivery details in the side drawer, then come back to chat for order placement and payment.", + openGiftAdvisor: "Open Gift Advisor", + trackMyOrder: "Track My Order", + browseCategories: "Browse Categories", + openCheckout: "Open Checkout", + cart: "Cart", + giftAdvisor: "Gift advisor", + birthdayGifts: "Birthday gifts", + anniversaryGifts: "Anniversary gifts", + trackOrder: "Track order", + showCategories: "Show categories", + inputPlaceholder: "Search products, ask for recommendations...", + suggestions: [ + { label: "Restock groceries", prompt: "Help me restock weekly groceries on Kapruka" }, + { label: "Birthday cake", prompt: "Find birthday cakes on Kapruka under Rs. 10,000" }, + { label: "Send flowers", prompt: "I want to send flowers to someone special" }, + { label: "Party supplies", prompt: "Show party supplies for 10 people" }, + ], + }, + si: { + appName: "KaprukaAI", + assistantLabel: "සාප්පු සහායක", + heroTitle: "AI සහායකයෙක් සමඟ සාප්පු යන්න", + heroDescription: "චැට් එකෙන් නිෂ්පාදන සොයන්න, ඇණවුම් ලුහුබඳින්න, සහ conversation එකෙන්ම checkout වෙන්න.", + heroExample: "උදාහරණයක්: මගේ සතිපතා groceries නැවත ගන්න උදව් කරන්න", + capabilityGiftTitle: "තෑගි අදහස්", + capabilityGiftBody: "Gift Advisor එකෙන් පටන් ගන්න, නැත්නම් birthday, anniversary, grocery prompts වලටම යන්න.", + capabilityTrackingTitle: "ඇණවුම් ලුහුබැඳීම", + capabilityTrackingBody: "ඕනෑම වෙලාවක tracking update එකක් අහන්න. අවශ්‍ය නම් order number එක ඉල්ලයි.", + capabilityCheckoutTitle: "Checkout Flow", + capabilityCheckoutBody: "Delivery details side drawer එකේ save කරලා, payment සහ order placement සඳහා chat එකට ආපහු එන්න.", + openGiftAdvisor: "Gift Advisor අරඹන්න", + trackMyOrder: "මගේ ඇණවුම ලුහුබඳින්න", + browseCategories: "කාණ්ඩ බලන්න", + openCheckout: "Checkout අරින්න", + cart: "Cart", + giftAdvisor: "Gift advisor", + birthdayGifts: "උපන්දින තෑගි", + anniversaryGifts: "සංවත්සර තෑගි", + trackOrder: "Track order", + showCategories: "කාණ්ඩ පෙන්වන්න", + inputPlaceholder: "නිෂ්පාදන සොයන්න, recommendations අහන්න...", + suggestions: [ + { label: "Groceries නැවත ගන්න", prompt: "Kapruka එකෙන් මගේ සතිපතා groceries නැවත ගන්න උදව් කරන්න" }, + { label: "Birthday cake", prompt: "රු. 10,000ට අඩු birthday cakes Kapruka එකෙන් හොයන්න" }, + { label: "මල් යවන්න", prompt: "කෙනෙකුට flowers යවන්න ඕන" }, + { label: "Party supplies", prompt: "10 දෙනෙකුට party supplies පෙන්නන්න" }, + ], + }, + ta: { + appName: "KaprukaAI", + assistantLabel: "ஷாப்பிங் உதவியாளர்", + heroTitle: "AI உதவியாளருடன் வாங்குங்கள்", + heroDescription: "அரட்டையிலேயே பொருட்கள் தேடுங்கள், order track செய்யுங்கள், checkout-க்கும் அங்கிருந்தே செல்லுங்கள்.", + heroExample: "உதாரணம்: என் வாராந்த groceries மீண்டும் வாங்க உதவி செய்", + capabilityGiftTitle: "பரிசு தேர்வுகள்", + capabilityGiftBody: "Gift Advisor-இல் தொடங்கலாம், இல்லையெனில் birthday, anniversary, grocery prompts-க்கு நேராக செல்லலாம்.", + capabilityTrackingTitle: "ஆர்டர் கண்காணிப்பு", + capabilityTrackingBody: "எப்போதும் tracking update கேளுங்கள். தேவைப்பட்டால் order number கேட்கும்.", + capabilityCheckoutTitle: "Checkout Flow", + capabilityCheckoutBody: "Delivery details-ஐ side drawer-இல் save செய்து, payment மற்றும் order placement காக மீண்டும் chat-க்கு வாருங்கள்.", + openGiftAdvisor: "Gift Advisor திறக்க", + trackMyOrder: "என் order-ஐ track செய்", + browseCategories: "Categories பாருங்கள்", + openCheckout: "Checkout திறக்க", + cart: "Cart", + giftAdvisor: "Gift advisor", + birthdayGifts: "பிறந்தநாள் பரிசுகள்", + anniversaryGifts: "ஆண்டுவிழா பரிசுகள்", + trackOrder: "Track order", + showCategories: "Categories காட்டு", + inputPlaceholder: "பொருட்கள் தேடுங்கள், recommendations கேளுங்கள்...", + suggestions: [ + { label: "Groceries மீண்டும் வாங்க", prompt: "Kapruka-வில் என் வாராந்த groceries மீண்டும் வாங்க உதவி செய்" }, + { label: "Birthday cake", prompt: "Rs. 10,000க்கு குறைவான birthday cakes Kapruka-வில் காண்பி" }, + { label: "Flowers அனுப்பு", prompt: "ஒருவருக்கு flowers அனுப்ப வேண்டும்" }, + { label: "Party supplies", prompt: "10 பேருக்கு party supplies காட்டு" }, + ], + }, +}; + +export function detectUiLanguage(locale?: string): UiLanguage { + const normalized = (locale || "").toLowerCase(); + if (normalized.startsWith("si")) return "si"; + if (normalized.startsWith("ta")) return "ta"; + return "en"; +} + +export function getUiCopy(language: UiLanguage): UiCopy { + return copy[language] || copy.en; +}