diff --git a/.gitignore b/.gitignore
index 3b0b403..f8dd907 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,4 +23,5 @@ dist-ssr
*.sln
*.sw?
-.env
\ No newline at end of file
+.env
+.vercel
diff --git a/package-lock.json b/package-lock.json
index 1d52648..3b2239a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,10 +12,12 @@
"@tsparticles/react": "^3.0.0",
"axios": "^1.13.5",
"clsx": "^2.1.1",
+ "i18next": "^26.0.5",
"lucide-react": "^0.575.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-dropzone": "^15.0.0",
+ "react-i18next": "^17.0.3",
"react-router-dom": "^7.13.0",
"tailwind-merge": "^3.5.0",
"tsparticles-slim": "^2.12.0",
@@ -287,6 +289,15 @@
"@babel/core": "^7.0.0-0"
}
},
+ "node_modules/@babel/runtime": {
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz",
+ "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
"node_modules/@babel/template": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
@@ -3123,6 +3134,47 @@
"hermes-estree": "0.25.1"
}
},
+ "node_modules/html-parse-stringify": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz",
+ "integrity": "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==",
+ "license": "MIT",
+ "dependencies": {
+ "void-elements": "3.1.0"
+ }
+ },
+ "node_modules/i18next": {
+ "version": "26.0.5",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.0.5.tgz",
+ "integrity": "sha512-9uHb4T27TdV36phJXcbpnRPt5yzAfqHXVrdASvmHZyPuZJtrLythd+GyXhiaHV5LlpuuskbAqhwPjmfTbKbi8w==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://www.locize.com/i18next"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+ },
+ {
+ "type": "individual",
+ "url": "https://www.locize.com"
+ }
+ ],
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "@babel/runtime": "^7.29.2"
+ },
+ "peerDependencies": {
+ "typescript": "^5 || ^6"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -3958,6 +4010,33 @@
"react": ">= 16.8 || 18.0.0"
}
},
+ "node_modules/react-i18next": {
+ "version": "17.0.3",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.3.tgz",
+ "integrity": "sha512-x4xjvUNZ56T+zfXWNedNnCET9Xq1IBYWX7IsWo5cCQ/RT+Rm7GWqt0h9PShFi4IhyMnsdiu1C6Jc4DE+/S3PFQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.29.2",
+ "html-parse-stringify": "^3.0.1",
+ "use-sync-external-store": "^1.6.0"
+ },
+ "peerDependencies": {
+ "i18next": ">= 26.0.1",
+ "react": ">= 16.8.0",
+ "typescript": "^5 || ^6"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ },
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -4866,7 +4945,7 @@
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
- "dev": true,
+ "devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
@@ -4949,6 +5028,16 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peer": true,
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
"node_modules/util-deprecate": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@@ -5032,6 +5121,15 @@
}
}
},
+ "node_modules/void-elements": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-3.1.0.tgz",
+ "integrity": "sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/package.json b/package.json
index e68f16b..f837dad 100644
--- a/package.json
+++ b/package.json
@@ -14,10 +14,12 @@
"@tsparticles/react": "^3.0.0",
"axios": "^1.13.5",
"clsx": "^2.1.1",
+ "i18next": "^26.0.5",
"lucide-react": "^0.575.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-dropzone": "^15.0.0",
+ "react-i18next": "^17.0.3",
"react-router-dom": "^7.13.0",
"tailwind-merge": "^3.5.0",
"tsparticles-slim": "^2.12.0",
diff --git a/src/components/AdminLayout.tsx b/src/components/AdminLayout.tsx
index 0920670..a988b7d 100644
--- a/src/components/AdminLayout.tsx
+++ b/src/components/AdminLayout.tsx
@@ -28,9 +28,6 @@ export const AdminLayout: React.FC<{ children: React.ReactNode }> = ({ children
navigate('/login')
}
- const isActive = (item: typeof NAV_ITEMS[0]) =>
- item.exact ? location.pathname === item.href : location.pathname.startsWith(item.href)
-
return (
{/* Mobile backdrop */}
diff --git a/src/components/ChatInterface.tsx b/src/components/ChatInterface.tsx
index ca9e7da..ad26eb1 100644
--- a/src/components/ChatInterface.tsx
+++ b/src/components/ChatInterface.tsx
@@ -25,10 +25,10 @@ interface ChatInterfaceProps {
export const ChatInterface: React.FC = ({
chatbotId, chatbotName, welcomeMessage, primaryColor, logoUrl,
isPreview = false, sessionId: externalSessionId,
- showBranding = false, leadCaptureEnabled = false,
- leadCaptureFields = ['email'], leadCaptureTrigger = 'after_first_message',
- handoffEnabled = false, handoffMessage: _handoffMessage,
- chatbotIdForLeads, conversationId,
+ showBranding = false, leadCaptureEnabled: _leadCaptureEnabled = false,
+ leadCaptureFields = ['email'], leadCaptureTrigger: _leadCaptureTrigger = 'after_first_message',
+ handoffEnabled: _handoffEnabled = false, handoffMessage: _handoffMessage,
+ chatbotIdForLeads: _chatbotIdForLeads, conversationId,
}) => {
const [messages, setMessages] = useState([
{ id: '0', role: 'assistant', content: welcomeMessage }
@@ -39,14 +39,28 @@ export const ChatInterface: React.FC = ({
const [sessionId] = useState(() => {
if (externalSessionId) return externalSessionId
const storageKey = `chat-session-${chatbotId}`
- const stored = sessionStorage.getItem(storageKey)
+ const stored = localStorage.getItem(storageKey)
if (stored) return stored
const newId = crypto.randomUUID()
- sessionStorage.setItem(storageKey, newId)
+ localStorage.setItem(storageKey, newId)
return newId
})
- const [feedbackSent, setFeedbackSent] = useState>(new Set())
+ useEffect(() => {
+ if (isPreview || externalSessionId) return
+ chatAPI.history(chatbotId, sessionId).then((msgs: { id: string; role: string; content: string }[]) => {
+ if (msgs && msgs.length > 0) {
+ setMessages([
+ { id: '0', role: 'assistant', content: welcomeMessage },
+ ...msgs.map(m => ({ id: m.id, role: m.role as 'user' | 'assistant', content: m.content })),
+ ])
+ }
+ }).catch(() => {})
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [])
+
+ const [feedbackGiven, setFeedbackGiven] = useState>({})
+
const [showLeadForm, setShowLeadForm] = useState(false)
const [leadSubmitted, setLeadSubmitted] = useState(false)
const [leadFormData, setLeadFormData] = useState({ email: '', name: '', phone: '', company: '' })
@@ -77,7 +91,7 @@ export const ChatInterface: React.FC = ({
const response = await chatAPI.send(chatbotId, {
message: text,
session_id: sessionId,
- language: navigator.language.split('-')[0] || 'en',
+ language: 'auto',
})
const assistantMsg: ChatMessage = {
@@ -117,10 +131,10 @@ export const ChatInterface: React.FC = ({
}
const handleFeedback = async (msgId: string, feedback: 'positive' | 'negative') => {
- if (feedbackSent.has(msgId)) return
+ if (feedbackGiven[msgId]) return
+ setFeedbackGiven(prev => ({ ...prev, [msgId]: feedback }))
try {
await chatAPI.feedback(chatbotId, msgId, feedback)
- setFeedbackSent(prev => new Set(prev).add(msgId))
} catch {
// silently fail
}
@@ -253,33 +267,34 @@ export const ChatInterface: React.FC = ({
{msg.role === 'assistant' && msg.id !== '0' && (
-
-
-
+
+ {feedbackGiven[msg.id] ? (
+
+ {feedbackGiven[msg.id] === 'positive' ? '👍 Thanks!' : '👎 Got it'}
+
+ ) : (
+ <>
+
+
+ >
+ )}
)}
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
index 843c1e4..2d5c320 100644
--- a/src/components/Layout.tsx
+++ b/src/components/Layout.tsx
@@ -4,29 +4,30 @@ import { cn } from '@/lib/utils'
import { useAuthStore } from '@/store/authStore'
import { authAPI } from '@/services/api'
import { getPlanColor } from '@/lib/utils'
+import { useTranslation } from 'react-i18next'
import {
LayoutDashboard, ShoppingBag, Settings,
LogOut, Menu, Sparkles, BarChart3, Mail, Users,
Shield, X, CalendarDays, Megaphone,
} from 'lucide-react'
-import { Divider } from './ui'
-
-const NAV_ITEMS = [
- { label: 'Dashboard', href: '/dashboard', icon: LayoutDashboard },
- { label: 'Inbox', href: '/inbox', icon: Mail },
- { label: 'Leads', href: '/leads', icon: Users },
- { label: 'Appointments', href: '/appointments', icon: CalendarDays },
- { label: 'Campaigns', href: '/campaigns', icon: Megaphone },
- { label: 'Analytics', href: '/analytics', icon: BarChart3 },
- { label: 'Marketplace', href: '/marketplace', icon: ShoppingBag },
- { label: 'Settings', href: '/settings', icon: Settings },
-]
export const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { user, logout } = useAuthStore()
const location = useLocation()
const navigate = useNavigate()
const [sidebarOpen, setSidebarOpen] = useState(false)
+ const { t } = useTranslation()
+
+ const NAV_ITEMS = [
+ { label: t('nav.dashboard'), href: '/dashboard', icon: LayoutDashboard },
+ { label: t('nav.inbox'), href: '/inbox', icon: Mail },
+ { label: t('nav.leads'), href: '/leads', icon: Users },
+ { label: t('nav.appointments'), href: '/appointments', icon: CalendarDays },
+ { label: t('nav.campaigns'), href: '/campaigns', icon: Megaphone },
+ { label: t('nav.analytics'), href: '/analytics', icon: BarChart3 },
+ { label: t('nav.marketplace'), href: '/marketplace', icon: ShoppingBag },
+ { label: t('nav.settings'), href: '/settings', icon: Settings },
+ ]
const handleLogout = async () => {
try { await authAPI.logout() } catch { /* ignore */ }
@@ -107,7 +108,7 @@ export const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children })
className="flex items-center gap-2.5 px-3 py-2 rounded-xl text-sm font-medium text-red-600 hover:bg-red-50 transition-colors"
>
- Admin Panel
+ {t('nav.admin_panel')}
)}
@@ -126,7 +127,7 @@ export const AppLayout: React.FC<{ children: React.ReactNode }> = ({ children })
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm text-gray-500 hover:text-red-600 hover:bg-red-50 rounded-xl transition-all duration-150"
>
- Sign out
+ {t('nav.sign_out')}
diff --git a/src/components/OnboardingChecklist.tsx b/src/components/OnboardingChecklist.tsx
new file mode 100644
index 0000000..edcf82f
--- /dev/null
+++ b/src/components/OnboardingChecklist.tsx
@@ -0,0 +1,311 @@
+import React, { useState, useEffect, useCallback } from 'react'
+import { useNavigate } from 'react-router-dom'
+import { useTranslation } from 'react-i18next'
+import {
+ CheckCircle2, ChevronDown, ChevronUp, X,
+ Sparkles, Bot, BookOpen, Eye, Globe, Share2,
+} from 'lucide-react'
+import { cn } from '@/lib/utils'
+import type { Chatbot } from '@/types'
+
+interface Props {
+ userId: string
+ userName?: string
+ chatbots: Chatbot[]
+}
+
+interface StoredState {
+ dismissed?: boolean
+ collapsed?: boolean
+ manuallyDone?: string[]
+}
+
+const storageKey = (userId: string) => `onboarding_v1_${userId}`
+
+const load = (userId: string): StoredState => {
+ try {
+ return JSON.parse(localStorage.getItem(storageKey(userId)) || '{}')
+ } catch {
+ return {}
+ }
+}
+
+const save = (userId: string, updates: Partial) => {
+ const current = load(userId)
+ localStorage.setItem(storageKey(userId), JSON.stringify({ ...current, ...updates }))
+}
+
+export const OnboardingChecklist: React.FC = ({ userId, userName, chatbots }) => {
+ const navigate = useNavigate()
+ const { t } = useTranslation()
+
+ const [dismissed, setDismissed] = useState(false)
+ const [collapsed, setCollapsed] = useState(false)
+ const [manuallyDone, setManuallyDone] = useState>(new Set())
+ const [celebrating, setCelebrating] = useState(false)
+ const [prevAllDone, setPrevAllDone] = useState(false)
+
+ useEffect(() => {
+ const stored = load(userId)
+ if (stored.dismissed) setDismissed(true)
+ if (stored.collapsed) setCollapsed(true)
+ if (stored.manuallyDone) setManuallyDone(new Set(stored.manuallyDone))
+ }, [userId])
+
+ const markDone = useCallback((stepId: string) => {
+ setManuallyDone(prev => {
+ const next = new Set(prev).add(stepId)
+ save(userId, { manuallyDone: [...next] })
+ return next
+ })
+ }, [userId])
+
+ const firstChatbot = chatbots[0]
+
+ const steps = [
+ {
+ id: 'create_chatbot',
+ icon: Bot,
+ title: t('onboarding.step_create_title'),
+ description: t('onboarding.step_create_desc'),
+ done: chatbots.length > 0,
+ cta: t('onboarding.step_create_cta'),
+ action: () => navigate('/chatbots/new'),
+ },
+ {
+ id: 'add_knowledge',
+ icon: BookOpen,
+ title: t('onboarding.step_knowledge_title'),
+ description: t('onboarding.step_knowledge_desc'),
+ done: chatbots.some(c => c.document_count > 0) || manuallyDone.has('add_knowledge'),
+ cta: t('onboarding.step_knowledge_cta'),
+ action: firstChatbot
+ ? () => navigate(`/chatbots/${firstChatbot.id}/edit`)
+ : () => navigate('/chatbots/new'),
+ },
+ {
+ id: 'test_chatbot',
+ icon: Eye,
+ title: t('onboarding.step_test_title'),
+ description: t('onboarding.step_test_desc'),
+ done: manuallyDone.has('test_chatbot'),
+ cta: t('onboarding.step_test_cta'),
+ action: firstChatbot
+ ? () => { markDone('test_chatbot'); navigate(`/chatbots/${firstChatbot.id}/preview`) }
+ : undefined,
+ },
+ {
+ id: 'publish_chatbot',
+ icon: Globe,
+ title: t('onboarding.step_publish_title'),
+ description: t('onboarding.step_publish_desc'),
+ done: chatbots.some(c => c.is_published) || manuallyDone.has('publish_chatbot'),
+ cta: t('onboarding.step_publish_cta'),
+ action: firstChatbot
+ ? () => navigate(`/chatbots/${firstChatbot.id}/edit`)
+ : undefined,
+ },
+ {
+ id: 'share_chatbot',
+ icon: Share2,
+ title: t('onboarding.step_share_title'),
+ description: t('onboarding.step_share_desc'),
+ done: manuallyDone.has('share_chatbot'),
+ cta: t('onboarding.step_share_cta'),
+ action: firstChatbot
+ ? () => { markDone('share_chatbot'); navigate(`/chatbots/${firstChatbot.id}/edit`) }
+ : undefined,
+ },
+ ]
+
+ const completedCount = steps.filter(s => s.done).length
+ const allDone = completedCount === steps.length
+ const progress = (completedCount / steps.length) * 100
+ const nextStep = steps.find(s => !s.done)
+
+ // Trigger celebration once when all steps are completed
+ useEffect(() => {
+ if (allDone && !prevAllDone && !dismissed) {
+ setCelebrating(true)
+ const t = setTimeout(() => {
+ setCelebrating(false)
+ setDismissed(true)
+ save(userId, { dismissed: true })
+ }, 4000)
+ return () => clearTimeout(t)
+ }
+ setPrevAllDone(allDone)
+ }, [allDone, prevAllDone, dismissed, userId])
+
+ const dismiss = () => {
+ setDismissed(true)
+ save(userId, { dismissed: true })
+ }
+
+ const toggleCollapsed = () => {
+ const next = !collapsed
+ setCollapsed(next)
+ save(userId, { collapsed: next })
+ }
+
+ if (dismissed) return null
+
+ return (
+
+ {/* Header */}
+
+
+ {/* Progress bar */}
+
+
+ {/* Body */}
+ {!collapsed && (
+ <>
+ {celebrating ? (
+
+
🎉
+
+ {t('onboarding.all_done_title')}
+
+
+ {t('onboarding.all_done_desc')}
+
+
+ ) : (
+ <>
+ {userName && completedCount === 0 && (
+
+
+ {t('onboarding.welcome', { name: userName })}
+
+
+ )}
+
+ {steps.map((step, idx) => {
+ const isNext = step.id === nextStep?.id
+ const Icon = step.icon
+
+ return (
+
+ {/* Step icon */}
+
+ {step.done ? (
+
+ ) : (
+
+
+ {idx + 1}
+
+
+ )}
+
+
+ {/* Content */}
+
+
+
+ {!step.done && (
+
+ {step.description}
+
+ )}
+
+ {isNext && step.action && (
+
+ )}
+
+
+ )
+ })}
+
+
+ {/* Footer */}
+
+
+ {t('onboarding.est_time')}
+
+
+
+ >
+ )}
+ >
+ )}
+
+ )
+}
diff --git a/src/components/PublicLayout.tsx b/src/components/PublicLayout.tsx
index f8d78ff..917def4 100644
--- a/src/components/PublicLayout.tsx
+++ b/src/components/PublicLayout.tsx
@@ -1,22 +1,40 @@
import React, { useState } from 'react'
import { Link, useLocation } from 'react-router-dom'
+import { useTranslation } from 'react-i18next'
import { Sparkles, Menu, X } from 'lucide-react'
+import i18n from '@/i18n/i18n'
-/**
- * R-07 FIX: PublicLayout provides navigation for unauthenticated users
- * on public pages (Marketplace, Pricing, ChatbotDetail).
- * Previously these pages had NO navigation header, making it impossible
- * for unauthenticated users to navigate between public pages.
- */
export const PublicLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => {
+ const { t } = useTranslation()
const location = useLocation()
const [mobileMenuOpen, setMobileMenuOpen] = useState(false)
+ const currentLang = i18n.language?.startsWith('fr') ? 'fr' : 'en'
const isActive = (path: string) => location.pathname.startsWith(path)
+ const setLang = (lang: string) => {
+ i18n.changeLanguage(lang)
+ }
+
+ const LangToggle = ({ size = 'md' }: { size?: 'sm' | 'md' }) => (
+
+
+
+
+ )
+
return (
- {/* Navigation Header */}
)}
- {/* Page content */}
{children}
)
diff --git a/src/components/ui.tsx b/src/components/ui.tsx
index a410a9c..b9235b1 100644
--- a/src/components/ui.tsx
+++ b/src/components/ui.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useRef } from 'react'
+import React, { useEffect } from 'react'
import { cn } from '@/lib/utils'
import { X } from 'lucide-react'
diff --git a/src/i18n/i18n.ts b/src/i18n/i18n.ts
new file mode 100644
index 0000000..0ddef41
--- /dev/null
+++ b/src/i18n/i18n.ts
@@ -0,0 +1,18 @@
+import i18n from 'i18next'
+import { initReactI18next } from 'react-i18next'
+import en from './locales/en.json'
+import fr from './locales/fr.json'
+
+i18n
+ .use(initReactI18next)
+ .init({
+ resources: {
+ en: { translation: en },
+ fr: { translation: fr },
+ },
+ lng: 'fr',
+ fallbackLng: 'fr',
+ interpolation: { escapeValue: false },
+ })
+
+export default i18n
diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json
new file mode 100644
index 0000000..82bc972
--- /dev/null
+++ b/src/i18n/locales/en.json
@@ -0,0 +1,656 @@
+{
+ "onboarding": {
+ "title": "Getting started",
+ "welcome": "Welcome! Let's get your first chatbot live in 7 minutes.",
+ "est_time": "~7 min to finish",
+ "dismiss": "Dismiss",
+ "all_done_title": "You're all set!",
+ "all_done_desc": "Your chatbot is live. Share it with the world.",
+ "step_create_title": "Create your first chatbot",
+ "step_create_desc": "Give it a name, personality, and brand color.",
+ "step_create_cta": "Create chatbot",
+ "step_knowledge_title": "Train it with your content",
+ "step_knowledge_desc": "Upload documents or add website URLs so it can answer questions.",
+ "step_knowledge_cta": "Add content",
+ "step_test_title": "Test your chatbot",
+ "step_test_desc": "Chat with it and make sure the answers are accurate.",
+ "step_test_cta": "Test now",
+ "step_publish_title": "Publish your chatbot",
+ "step_publish_desc": "Make it live and accessible to visitors.",
+ "step_publish_cta": "Publish",
+ "step_share_title": "Share or embed it",
+ "step_share_desc": "Add the chat widget to your website or share the link.",
+ "step_share_cta": "Get embed code"
+ },
+ "common": {
+ "cancel": "Cancel",
+ "delete": "Delete",
+ "save": "Save",
+ "save_changes": "Save Changes",
+ "close": "Close",
+ "confirm": "Confirm",
+ "filter": "Filter",
+ "export_csv": "Export CSV",
+ "all_chatbots": "All chatbots",
+ "all_statuses": "All statuses",
+ "published": "Published",
+ "draft": "Draft",
+ "preview": "Preview",
+ "publish": "Publish",
+ "unpublish": "Unpublish",
+ "edit_settings": "Edit Settings",
+ "analytics": "Analytics",
+ "no_data": "No data yet",
+ "loading": "Loading...",
+ "back": "Back",
+ "no_changes": "No changes to save"
+ },
+ "nav": {
+ "dashboard": "Dashboard",
+ "inbox": "Inbox",
+ "leads": "Leads",
+ "appointments": "Appointments",
+ "campaigns": "Campaigns",
+ "analytics": "Analytics",
+ "marketplace": "Marketplace",
+ "settings": "Settings",
+ "admin_panel": "Admin Panel",
+ "sign_out": "Sign out",
+ "pricing": "Pricing",
+ "features": "Features",
+ "signin": "Sign in",
+ "get_started": "Get started free"
+ },
+ "dashboard": {
+ "title": "Dashboard",
+ "subtitle_empty": "Manage your AI chatbots",
+ "chatbot_count_one": "{{count}} chatbot",
+ "chatbot_count_other": "{{count}} chatbots",
+ "new_chatbot": "New Chatbot",
+ "no_chatbots_title": "No chatbots yet",
+ "no_chatbots_desc": "Create your first AI chatbot powered by your documents. Free to build and test.",
+ "create_first": "Create your first chatbot",
+ "delete_chatbot": "Delete Chatbot",
+ "delete_confirm": "All documents, conversation history, and settings will be permanently removed. This cannot be undone.",
+ "publish_to_marketplace": "Publish to Marketplace",
+ "unpublish_chatbot": "Unpublish Chatbot",
+ "publish_confirm": "Your chatbot will be publicly visible on the marketplace.",
+ "unpublish_confirm": "Your chatbot will be removed from the marketplace.",
+ "chatbot_deleted": "Chatbot deleted",
+ "chatbot_published": "Chatbot published to marketplace!",
+ "chatbot_unpublished": "Chatbot unpublished"
+ },
+ "inbox": {
+ "title": "Inbox",
+ "conversation_count_one": "{{count}} conversation",
+ "conversation_count_other": "{{count}} conversations",
+ "filter_all": "All",
+ "filter_open": "Open",
+ "filter_agent": "Agent",
+ "filter_resolved": "Resolved",
+ "no_conversations": "No conversations",
+ "try_different_filter": "Try a different filter",
+ "no_messages": "(No messages)",
+ "select_conversation": "Select a conversation",
+ "select_conversation_desc": "Choose one from the list to view the full exchange",
+ "take_over": "Take Over",
+ "resolve": "Resolve",
+ "reopen": "Reopen",
+ "you_agent": "You (agent)",
+ "handoff_requested": "Handoff requested",
+ "low_confidence": "Low confidence",
+ "conversation_resolved": "Conversation resolved —",
+ "reopen_link": "reopen",
+ "to_reply": "to reply",
+ "type_reply": "Type a reply as agent...",
+ "delete_conversation": "Delete this conversation?",
+ "failed_to_delete": "Failed to delete conversation",
+ "status_open": "Open",
+ "status_agent": "Agent",
+ "status_resolved": "Resolved",
+ "upgrade_title": "Conversation Inbox",
+ "upgrade_desc": "Upgrade to Starter to read all your chatbot conversations in one place."
+ },
+ "leads": {
+ "title": "Leads",
+ "subtitle": "Contacts collected by your chatbots",
+ "total_leads": "Total leads",
+ "this_month": "This month",
+ "filter_by_chatbot": "Filter by chatbot",
+ "clear_status_filter": "Clear status filter",
+ "col_contact": "Contact",
+ "col_phone": "Phone",
+ "col_company": "Company",
+ "col_status": "Status",
+ "col_notes": "Notes",
+ "col_date": "Date",
+ "add_note": "Add note",
+ "notes_modal_title": "Notes — {{name}}",
+ "notes_placeholder": "Add notes about this lead...",
+ "no_leads_title": "No leads yet",
+ "no_leads_with_status": "No leads with status \"{{status}}\"",
+ "no_leads_desc": "Enable lead capture on your chatbots to start collecting contact information.",
+ "no_leads_status_desc": "Try a different filter or clear the current one.",
+ "export_failed": "Export failed",
+ "status_new": "New",
+ "status_contacted": "Contacted",
+ "status_qualified": "Qualified",
+ "status_closed": "Closed",
+ "status_lost": "Lost",
+ "upgrade_title": "Lead Capture",
+ "upgrade_desc": "Upgrade to Starter to capture and manage leads from your chatbots."
+ },
+ "appointments": {
+ "title": "Appointments",
+ "subtitle": "Bookings made through your chatbots",
+ "stat_today": "Today",
+ "stat_upcoming": "Upcoming",
+ "stat_confirmed": "Confirmed",
+ "stat_pending": "Pending",
+ "filter": "Filter",
+ "hours_label": "Hours:",
+ "configure_chatbot_hours": "Configure chatbot...",
+ "enable_booking_title": "Enable booking on a chatbot",
+ "enable_booking_desc": "Go to a chatbot's Deploy tab and enable \"Appointment Booking\" to start accepting bookings.",
+ "configure_chatbot": "Configure chatbot →",
+ "no_appointments_title": "No appointments yet",
+ "no_appointments_desc": "Once customers book through your chatbot, appointments will appear here.",
+ "today_label": "Today",
+ "to": "to",
+ "confirm_btn": "Confirm",
+ "decline_btn": "Decline",
+ "mark_complete": "Mark Complete",
+ "cancel_btn": "Cancel",
+ "restore_btn": "Restore",
+ "hours_title": "Business Hours",
+ "hours_desc": "Configure when customers can book appointments.",
+ "hours_back": "← Back",
+ "hours_closed": "Closed",
+ "save_hours": "Save Hours",
+ "hours_saved": "✓ Saved!",
+ "status_pending": "Pending",
+ "status_confirmed": "Confirmed",
+ "status_cancelled": "Cancelled",
+ "status_completed": "Completed",
+ "days_mon": "Mon",
+ "days_tue": "Tue",
+ "days_wed": "Wed",
+ "days_thu": "Thu",
+ "days_fri": "Fri",
+ "days_sat": "Sat",
+ "days_sun": "Sun",
+ "upgrade_title": "Appointment Booking",
+ "upgrade_desc": "Upgrade to Starter to enable appointment booking for your chatbots."
+ },
+ "campaigns": {
+ "title": "Campaigns",
+ "subtitle": "Broadcast messages to Telegram subscribers",
+ "new_campaign": "New Campaign",
+ "stat_campaigns": "Campaigns",
+ "stat_sent": "Sent",
+ "stat_delivered": "Messages delivered",
+ "chatbot_label": "Chatbot",
+ "chatbot_hint": "Will broadcast to all Telegram subscribers of this chatbot.",
+ "campaign_name": "Campaign name",
+ "campaign_name_placeholder": "e.g. Summer promotion, New menu announcement...",
+ "message_label": "Message",
+ "message_placeholder": "Write your broadcast message here...",
+ "characters": "{{count}}/4000 characters",
+ "create_campaign": "Create Campaign",
+ "send_campaign": "Send Campaign",
+ "delete_record": "Delete record",
+ "send_modal_title": "Send this campaign?",
+ "send_modal_desc_one": "\"{{title}}\" will be sent to {{count}} subscriber via Telegram.",
+ "send_modal_desc_other": "\"{{title}}\" will be sent to {{count}} subscribers via Telegram.",
+ "send_modal_warning": "This action cannot be undone. The message will be delivered immediately.",
+ "send_now": "Send Now",
+ "delete_campaign": "Delete this campaign?",
+ "delete_campaign_record": "Delete this campaign record?",
+ "subscriber_one": "{{count}} subscriber",
+ "subscriber_other": "{{count}} subscribers",
+ "delivered": "delivered",
+ "no_campaigns_title": "No campaigns yet",
+ "no_campaigns_desc": "Create a campaign to broadcast a message to all your Telegram subscribers at once.",
+ "no_chatbots_needed": "You need at least one chatbot to create a campaign.",
+ "status_draft": "Draft",
+ "status_sending": "Sending...",
+ "status_sent": "Sent",
+ "status_failed": "Failed",
+ "upgrade_title": "Telegram Campaigns",
+ "upgrade_desc": "Upgrade to Starter to broadcast messages to your Telegram subscribers."
+ },
+ "analytics": {
+ "title": "Analytics",
+ "subtitle": "Track how your chatbots are performing",
+ "stat_conversations": "Conversations",
+ "stat_unique_users": "Unique users",
+ "stat_messages": "Messages",
+ "stat_avg_rating": "Avg rating",
+ "stat_this_month": "{{count}} this month",
+ "stat_across_all": "Across all chatbots",
+ "stat_total_exchanged": "Total exchanged",
+ "stat_across_rated": "Across rated chatbots",
+ "stat_no_ratings": "No ratings yet",
+ "monthly_conversations": "Monthly conversations",
+ "your_chatbots": "Your chatbots",
+ "published": "published",
+ "today": "Today",
+ "this_week": "This week",
+ "this_month": "This month",
+ "avg_msgs": "Avg msgs/convo",
+ "last_30_days": "Last 30 days",
+ "top_questions": "Top questions",
+ "languages": "Languages",
+ "knowledge_gaps": "Knowledge gaps — {{count}} unanswered",
+ "add_content": "+ Add content →",
+ "gaps_desc": "Customers asked these questions but your bot couldn't answer well. Add documents or URL sources covering these topics.",
+ "more_gaps_one": "+{{count}} more gap",
+ "more_gaps_other": "+{{count}} more gaps",
+ "feedback": "Feedback",
+ "helpful_pct": "{{pct}}% helpful",
+ "peak_hour": "Peak: {{from}}:00 – {{to}}:00",
+ "conversations_today": "{{count}} today",
+ "no_chatbots_title": "No chatbots yet",
+ "no_chatbots_desc": "Create your first chatbot to start seeing analytics.",
+ "create_chatbot": "Create chatbot",
+ "unable_to_load": "Unable to load analytics",
+ "try_refreshing": "Please try refreshing the page.",
+ "upgrade_title": "Analytics Dashboard",
+ "upgrade_desc": "Unlock analytics to see how your chatbots are performing — conversations, user engagement, top questions, and more.",
+ "upgrade_button": "Upgrade to Starter — $3/mo",
+ "upgrade_note": "Available on Starter and Pro plans",
+ "plan_badge": "{{plan}} plan"
+ },
+ "settings": {
+ "title": "Settings",
+ "light_mode": "Light mode",
+ "dark_mode": "Dark mode",
+ "tab_profile": "Profile",
+ "tab_billing": "Billing",
+ "profile_info": "Profile Information",
+ "email": "Email",
+ "email_hint": "Email cannot be changed",
+ "company_name": "Company Name",
+ "company_placeholder": "Your company name",
+ "plan_label": "Plan",
+ "manage_plan": "Manage plan",
+ "change_password": "Change Password",
+ "current_password": "Current Password",
+ "current_password_placeholder": "Enter current password",
+ "new_password": "New Password",
+ "new_password_placeholder": "Min 8 characters",
+ "new_password_hint": "Leave blank to keep current password",
+ "danger_zone": "Danger Zone",
+ "danger_desc": "Permanently delete your account, all chatbots, documents, and data. This cannot be undone.",
+ "delete_account_btn": "Delete Account",
+ "delete_account_title": "Delete Account",
+ "delete_account_desc": "This will permanently delete your account and all associated data including chatbots, documents, conversations, and leads.",
+ "delete_account_desc_bold": "This action cannot be undone.",
+ "type_delete": "Type DELETE to confirm:",
+ "profile_updated": "Profile updated successfully",
+ "update_failed": "Failed to update profile",
+ "language_label": "Language",
+ "language_updated": "Language updated",
+ "lang_en": "English",
+ "lang_fr": "Français",
+ "current_plan": "Current Plan",
+ "status_label": "Status:",
+ "status_active": "Active",
+ "renewal_date": "Renewal Date",
+ "upgrade_plan": "✨ Upgrade Plan",
+ "manage_billing": "Manage Billing",
+ "plan_features": "Plan Features",
+ "chatbots_published": "Chatbots published",
+ "conversations_per_month": "Conversations / month",
+ "code_export": "Code export",
+ "chatbot_suffix": "chatbot(s)",
+ "conversations_suffix": "conversations",
+ "billing_footer_paid": "💳 Simplified subscription management",
+ "billing_footer_free": "🚀 Unlock more features by upgrading your plan"
+ },
+ "builder": {
+ "loading": "Loading chatbot…",
+ "choose_template": "Choose a template",
+ "choose_template_sub": "Start from a template or build from scratch",
+ "scratch": "Start from scratch",
+ "create_chatbot": "Create Chatbot",
+ "untitled": "Untitled Chatbot",
+ "published": "Published",
+ "draft": "Draft",
+ "create": "Create",
+ "save": "Save",
+ "tab_settings": "Settings",
+ "tab_documents": "Documents",
+ "tab_preview": "Preview",
+ "tab_testing": "Testing",
+ "tab_deploy": "Deploy",
+ "save_first_testing": "Save your chatbot first to run tests.",
+ "testing_title": "Bot Testing",
+ "testing_desc": "Run questions against your chatbot and inspect answers, confidence scores, and source documents.",
+ "testing_placeholder": "e.g. What are your opening hours?",
+ "testing_add": "Add question",
+ "testing_run": "Run tests",
+ "testing_running": "Running…",
+ "testing_results": "{{count}} result(s)",
+ "testing_sources": "Sources used",
+ "testing_model": "Model",
+ "testing_error": "Test failed. Make sure your chatbot has a knowledge base.",
+ "refresh_url": "Re-scrape this URL",
+ "created": "Chatbot created!",
+ "create_failed": "Failed to create",
+ "saved": "Settings saved!",
+ "save_failed": "Save failed",
+ "name_required": "Chatbot name is required",
+ "save_first_docs": "Save your chatbot first to upload documents.",
+ "save_first_preview": "Save your chatbot first to preview it.",
+ "save_first_deploy": "Save your chatbot first to access deployment options.",
+ "save_first_hint": "Fill in the Settings tab and click Save to continue.",
+ "section_basic": "Basic Info",
+ "section_basic_desc": "Name, description, and greeting message for your chatbot",
+ "chatbot_name": "Chatbot Name",
+ "chatbot_name_placeholder": "e.g. Customer Support Bot",
+ "description": "Description",
+ "description_placeholder": "What does this chatbot do?",
+ "welcome_message": "Welcome Message",
+ "welcome_hint": "The first message visitors will see when opening the chat",
+ "system_prompt": "System Prompt",
+ "system_prompt_placeholder": "You are a helpful assistant for...",
+ "system_prompt_hint": "Custom instructions for the AI's behavior and personality (optional)",
+ "section_appearance": "Appearance",
+ "section_appearance_desc": "Logo and brand color shown in the chat widget",
+ "logo_label": "Chatbot Logo",
+ "logo_hint": "Upload your company logo. It will appear in the chat header.",
+ "brand_color": "Brand Color",
+ "color_preview": "Preview of how the chat button will look",
+ "section_advanced": "Advanced Settings",
+ "section_advanced_desc": "AI model, temperature, response length",
+ "ai_model": "AI Model",
+ "models_loading": "Loading available models...",
+ "models_empty": "No models available on your current plan.",
+ "models_upgrade": "Upgrade",
+ "models_upgrade_suffix": "to access AI models.",
+ "response_params": "Response Parameters",
+ "temperature": "Temperature",
+ "temp_precise": "Precise",
+ "temp_creative": "Creative",
+ "max_tokens": "Max Tokens",
+ "max_tokens_hint": "Max response length",
+ "section_classification": "Classification",
+ "section_classification_desc": "Helps users discover your chatbot in the marketplace",
+ "select_category": "Select category",
+ "select_industry": "Select industry",
+ "logo_uploaded": "Logo uploaded",
+ "logo_remove": "Remove logo",
+ "logo_drop": "Drop your logo here",
+ "logo_click": "Click or drag to upload a logo",
+ "logo_formats": "PNG, JPG, SVG, or WebP · Max 2MB",
+ "logo_processing": "Processing...",
+ "logo_error_type": "Please upload a PNG, JPG, GIF, SVG, or WebP image.",
+ "logo_error_size": "Image must be under 2MB.",
+ "logo_error_upload": "Upload failed. Please try again.",
+ "section_upload": "Upload Documents",
+ "section_upload_desc": "PDF, DOCX, CSV, XLSX, TXT, MD — used to train your chatbot's knowledge base",
+ "drop_files": "Drop files here",
+ "click_upload": "Click or drag files to upload",
+ "uploading": "Uploading...",
+ "upload_success": "Documents uploaded successfully!",
+ "docs_empty": "No documents yet",
+ "docs_empty_hint": "Upload files above to build your chatbot's knowledge base.",
+ "doc_count_one": "{{count}} document",
+ "doc_count_other": "{{count}} documents",
+ "chunks": "{{n}} chunks",
+ "section_urls": "URL Sources",
+ "section_urls_desc": "Add web pages to your chatbot's knowledge base",
+ "add_url": "Add URL",
+ "url_failed": "Failed to add URL",
+ "section_chat_link": "Public Chat Link",
+ "section_chat_link_desc": "Share a direct link to your chatbot with anyone",
+ "copy": "Copy",
+ "copied": "Copied",
+ "publish_for_link": "Publish your chatbot in the Deploy settings to get a public chat link.",
+ "section_embed": "Embed Code",
+ "section_embed_desc": "Add a chat widget to any website with one line of code",
+ "publish_for_embed": "Publish your chatbot first to get the embed code.",
+ "section_lead": "Lead Capture",
+ "section_lead_desc": "Collect visitor information before or during the conversation",
+ "lead_enable": "Enable lead capture",
+ "lead_enable_sub": "Ask visitors for their contact info",
+ "collect_fields": "Collect fields",
+ "required": "required",
+ "when_show": "When to show form",
+ "after_first": "After first message",
+ "before_first": "Before first message",
+ "section_handoff": "Human Handoff",
+ "section_handoff_desc": "Let visitors request to speak with a human agent",
+ "handoff_enable": "Enable human handoff",
+ "handoff_enable_sub": "Triggered when user says \"human\", \"agent\", etc.",
+ "handoff_message_label": "Handoff message",
+ "handoff_webhook_note": "Configure the n8n webhook URL in your backend to receive notifications.",
+ "section_branding": "Branding",
+ "section_branding_desc": "Control the Contexta attribution in your chat widget",
+ "show_branding": "Show \"Powered by Contexta\"",
+ "show_branding_sub": "Remove branding by upgrading to Pro plan or above",
+ "section_booking": "Appointment Booking",
+ "section_booking_desc": "Let customers book appointments directly through your chatbot",
+ "booking_enable": "Enable appointment booking",
+ "booking_enable_sub": "When enabled, the chatbot will guide users to your booking page and mention it in conversations.",
+ "booking_url_label": "Booking page URL:",
+ "booking_url_hint": "Share this link on your website or social media. Set your available hours in the",
+ "booking_url_hint_link": "Appointments page",
+ "section_channels": "Messaging Channels",
+ "section_channels_desc": "Connect your chatbot to Telegram",
+ "telegram_connected": "Connected",
+ "telegram_share": "Share this bot link with your customers — they open it and start chatting.",
+ "telegram_owner_notice": "To receive handoff alerts, open your bot and send it",
+ "telegram_owner_notice2": "It will register you as the owner and notify you here whenever a visitor needs human help.",
+ "telegram_disconnect": "Disconnect",
+ "telegram_how_title": "How to create a Telegram bot (2 minutes):",
+ "telegram_step1": "Open Telegram and search for @BotFather",
+ "telegram_step2": "Send /newbot",
+ "telegram_step3": "Choose a name and username for your bot",
+ "telegram_step4": "BotFather will send you a token — copy it",
+ "telegram_step5": "Paste the token below and click Connect",
+ "telegram_share_hint": "Once connected, share your bot link (e.g. t.me/YourBotName) with customers.",
+ "telegram_placeholder": "Bot token from @BotFather",
+ "telegram_connect": "Connect",
+ "telegram_connect_failed": "Failed to connect. Check your token.",
+ "embed_hint_html": "Paste before the closing