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.title} +

+
+ + {!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 tag in your HTML file.", + "embed_hint_react": "Add to your index.html (in the public/ folder) before the closing tag.", + "embed_hint_nextjs": "Use the built-in Script component inside your root layout so it loads on every page.", + "embed_hint_wordpress": "Go to Appearance → Theme File Editor → footer.php and paste before . Or use \"Insert Headers and Footers\" plugin.", + "embed_hint_webflow": "Go to Site Settings → Custom Code → Footer Code and paste the script there. Republish your site.", + "embed_hint_shopify": "Go to Online Store → Themes → Edit code → layout/theme.liquid and paste before .", + "model_default": "(default)" + }, + "marketplace": { + "title": "AI Chatbot Marketplace", + "subtitle": "Discover and interact with AI-powered chatbots built by businesses — ready to answer your questions instantly.", + "search_placeholder": "Search chatbots by name or description...", + "filters": "Filters", + "category": "Category", + "all": "All", + "industry": "Industry", + "all_industries": "All Industries", + "clear_all_filters": "Clear all filters", + "clear_filters": "Clear filters", + "no_chatbots_title": "No chatbots found", + "no_chatbots_filtered": "Try adjusting your filters or search query.", + "no_chatbots_empty": "Be the first to publish your AI chatbot to the marketplace!", + "create_chatbot": "Create Chatbot", + "available_one": "{{count}} chatbot available", + "available_other": "{{count}} chatbots available", + "by": "by {{name}}", + "chat_now": "Chat now →", + "conversations": "{{count}} conversations", + "back_to_marketplace": "Back to Marketplace", + "not_found_title": "Chatbot not found", + "not_found_desc": "This chatbot may have been unpublished or removed.", + "submit_rating": "Submit", + "your_rating": "Your rating", + "login_to_rate": "Sign in to rate this chatbot", + "ratings": "ratings" + }, + "auth": { + "login_title": "Welcome back", + "login_subtitle": "Sign in to your Contexta account", + "email": "Email", + "password": "Password", + "sign_in": "Sign in", + "no_account": "No account?", + "sign_up_free": "Sign up free", + "forgot_password": "Forgot password?", + "login_failed": "Login failed. Please check your credentials.", + "signup_title": "Create your account", + "signup_subtitle": "Start building AI chatbots — free forever", + "company_name": "Company Name", + "create_free_account": "Create free account", + "terms_text": "By signing up you agree to our", + "terms_of_service": "Terms of Service", + "and": "and", + "privacy_policy": "Privacy Policy", + "already_account": "Already have an account?", + "already_confirmed": "Already confirmed?", + "check_inbox_title": "Check your inbox", + "check_inbox_desc": "A confirmation link was sent to", + "password_min_8": "Password must be at least 8 characters", + "signup_failed": "Signup failed. Please try again.", + "forgot_title": "Reset your password", + "forgot_subtitle": "We'll send a reset link to your email address.", + "email_address": "Email address", + "send_reset_link": "Send reset link", + "back_to_signin": "Back to sign in", + "forgot_sent_title": "Check your inbox", + "forgot_sent_desc_one": "If {{email}} is registered,", + "forgot_sent_desc_two": "a password reset link has been sent.", + "forgot_error": "Something went wrong. Please try again.", + "reset_title": "Set new password", + "reset_subtitle": "Choose a strong password for your account.", + "new_password": "New Password", + "confirm_password": "Confirm Password", + "confirm_placeholder": "Repeat password", + "set_new_password": "Set new password", + "link_expired_title": "Link expired", + "request_new_link": "Request a new reset link", + "passwords_dont_match": "Passwords do not match", + "failed_to_reset": "Failed to reset password. The link may have expired.", + "branding_headline": "Your AI chatbot,\nready in minutes.", + "branding_subtext": "Train on your documents, manage every conversation, capture leads and book appointments — all in one place.", + "branding_feature_1": "PDF, DOCX, CSV, URLs — any source", + "branding_feature_2": "Live inbox, lead capture & appointment booking", + "branding_feature_3": "Web embed, Telegram & marketplace listing", + "branding_footer": "Trusted by businesses building smarter customer experiences." + }, + "pricing": { + "badge": "Pricing", + "title": "Simple, transparent pricing", + "subtitle": "Start free and go live for $12/month. Built for individuals, small businesses, agencies, and enterprises alike.", + "monthly": "Monthly", + "yearly": "Yearly", + "per_month": "/mo", + "save_yr": "Save ${{amount}}/yr", + "custom_price": "Custom", + "current_plan_badge": "Current Plan", + "most_popular": "Most Popular", + "cta_current": "Current Plan", + "cta_free": "Get Started Free", + "cta_contact": "Contact Sales", + "cta_downgrade": "Downgrade", + "cta_upgrade": "Upgrade Now", + "faq_title": "Frequently Asked Questions", + "faq_subtitle": "Everything you need to know about Contexta's plans.", + "plan_free": "Free", + "plan_free_desc": "Build, test and launch your first chatbot — no card needed", + "plan_starter": "Starter", + "plan_starter_desc": "For solo operators: live chat, leads, booking, and campaigns", + "plan_business": "Business", + "plan_business_desc": "For growing businesses: premium AI, unlimited booking, full analytics", + "plan_agency": "Agency", + "plan_agency_desc": "For agencies: unlimited everything, white-label ready", + "plan_enterprise": "Enterprise", + "plan_enterprise_desc": "For large organizations with custom needs and SLAs", + "feat_free": [ + "1 published chatbot", + "100 conversations/month", + "3 documents per chatbot", + "Public chat link + website embed", + "Llama 3.3 70B model", + "Read-only inbox (no agent replies)", + "View-only leads (no editing)", + "Analytics dashboard", + "Appointments & campaigns", + "Messaging channels", + "Remove \"Powered by Contexta\"" + ], + "feat_starter": [ + "Everything in Free", + "3 published chatbots", + "1,500 conversations/month", + "10 documents per chatbot", + "4 Fireworks AI models (Qwen3, DeepSeek, Kimi, Llama)", + "Live chat inbox + agent replies", + "Full lead CRM (status + notes)", + "Appointment booking (1 chatbot)", + "Telegram campaigns (3/mo · 500 recipients)", + "Analytics dashboard", + "Knowledge gap suggestions", + "Premium models (GPT-4o, Claude, Gemini)", + "Remove \"Powered by Contexta\"" + ], + "feat_business": [ + "Everything in Starter", + "10 published chatbots", + "5,000 conversations/month", + "50 documents per chatbot", + "GPT-4o, Claude Haiku 4.5, Gemini 2.5", + "Appointment booking (all chatbots)", + "Unlimited campaigns · 5,000 recipients", + "Knowledge gap suggestions", + "Remove \"Powered by Contexta\"", + "Unlimited URL sources" + ], + "feat_agency": [ + "Everything in Business", + "Unlimited published chatbots", + "20,000 conversations/month", + "Unlimited documents", + "Unlimited campaign recipients", + "Code export (FastAPI + React)", + "Dedicated support" + ], + "feat_enterprise": [ + "Everything in Agency", + "Unlimited conversations", + "White-label platform", + "SSO (SAML)", + "SLA guarantees", + "Dedicated account manager", + "24/7 phone support" + ], + "faq": [ + { + "q": "Can I use the free tier forever?", + "a": "Yes! Build and test unlimited chatbots for free. Your chatbots will remain in preview mode until you subscribe." + }, + { + "q": "What is code export?", + "a": "Agency plan users can export their chatbot as a complete, production-ready package including a FastAPI backend and React TypeScript widget — giving you full control to self-host." + }, + { + "q": "Do I need my own API keys?", + "a": "No — API keys are handled by Contexta. If you export the code on the Agency plan, you'll need your own keys for self-hosted deployment." + }, + { + "q": "Can I cancel anytime?", + "a": "Yes, cancel anytime. Your chatbots will revert to the free tier at the end of your billing period." + }, + { + "q": "What happens if I hit my conversation limit?", + "a": "Your chatbot will show a friendly message to try again later. Upgrade your plan for more conversations." + }, + { + "q": "I'm a small business. Which plan is right for me?", + "a": "Start with Starter at $12/month — 1 published chatbot, 1,500 conversations, analytics, lead capture and Telegram. Perfect for restaurants, shops, salons, and more. Upgrade to Business for premium AI models and more capacity." + } + ] + } +} diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json new file mode 100644 index 0000000..6b527ff --- /dev/null +++ b/src/i18n/locales/fr.json @@ -0,0 +1,656 @@ +{ + "onboarding": { + "title": "Premiers pas", + "welcome": "Bienvenue ! Mettez votre premier chatbot en ligne en 7 minutes.", + "est_time": "~7 min pour terminer", + "dismiss": "Masquer", + "all_done_title": "Vous êtes prêt !", + "all_done_desc": "Votre chatbot est en ligne. Partagez-le avec le monde.", + "step_create_title": "Créez votre premier chatbot", + "step_create_desc": "Donnez-lui un nom, une personnalité et une couleur.", + "step_create_cta": "Créer le chatbot", + "step_knowledge_title": "Entraînez-le avec votre contenu", + "step_knowledge_desc": "Importez des documents ou ajoutez des URLs pour qu'il puisse répondre aux questions.", + "step_knowledge_cta": "Ajouter du contenu", + "step_test_title": "Testez votre chatbot", + "step_test_desc": "Discutez avec lui et vérifiez que les réponses sont correctes.", + "step_test_cta": "Tester maintenant", + "step_publish_title": "Publiez votre chatbot", + "step_publish_desc": "Rendez-le accessible aux visiteurs.", + "step_publish_cta": "Publier", + "step_share_title": "Partagez ou intégrez-le", + "step_share_desc": "Ajoutez le widget sur votre site ou partagez le lien.", + "step_share_cta": "Obtenir le code" + }, + "common": { + "cancel": "Annuler", + "delete": "Supprimer", + "save": "Enregistrer", + "save_changes": "Enregistrer les modifications", + "close": "Fermer", + "confirm": "Confirmer", + "filter": "Filtrer", + "export_csv": "Exporter en CSV", + "all_chatbots": "Tous les chatbots", + "all_statuses": "Tous les statuts", + "published": "Publié", + "draft": "Brouillon", + "preview": "Aperçu", + "publish": "Publier", + "unpublish": "Dépublier", + "edit_settings": "Modifier les paramètres", + "analytics": "Analytiques", + "no_data": "Aucune donnée", + "loading": "Chargement...", + "back": "Retour", + "no_changes": "Aucune modification à enregistrer" + }, + "nav": { + "dashboard": "Tableau de bord", + "inbox": "Boîte de réception", + "leads": "Prospects", + "appointments": "Rendez-vous", + "campaigns": "Campagnes", + "analytics": "Analytiques", + "marketplace": "Marketplace", + "settings": "Paramètres", + "admin_panel": "Panel d'administration", + "sign_out": "Se déconnecter", + "pricing": "Tarifs", + "features": "Fonctionnalités", + "signin": "Se connecter", + "get_started": "Commencer gratuitement" + }, + "dashboard": { + "title": "Tableau de bord", + "subtitle_empty": "Gérez vos chatbots IA", + "chatbot_count_one": "{{count}} chatbot", + "chatbot_count_other": "{{count}} chatbots", + "new_chatbot": "Nouveau chatbot", + "no_chatbots_title": "Aucun chatbot pour l'instant", + "no_chatbots_desc": "Créez votre premier chatbot IA alimenté par vos documents. Gratuit pour construire et tester.", + "create_first": "Créer votre premier chatbot", + "delete_chatbot": "Supprimer le chatbot", + "delete_confirm": "Tous les documents, l'historique des conversations et les paramètres seront définitivement supprimés. Cette action est irréversible.", + "publish_to_marketplace": "Publier sur la Marketplace", + "unpublish_chatbot": "Dépublier le chatbot", + "publish_confirm": "Votre chatbot sera visible publiquement sur la marketplace.", + "unpublish_confirm": "Votre chatbot sera retiré de la marketplace.", + "chatbot_deleted": "Chatbot supprimé", + "chatbot_published": "Chatbot publié sur la marketplace !", + "chatbot_unpublished": "Chatbot dépublié" + }, + "inbox": { + "title": "Boîte de réception", + "conversation_count_one": "{{count}} conversation", + "conversation_count_other": "{{count}} conversations", + "filter_all": "Tous", + "filter_open": "Ouvert", + "filter_agent": "Agent", + "filter_resolved": "Résolu", + "no_conversations": "Aucune conversation", + "try_different_filter": "Essayez un autre filtre", + "no_messages": "(Aucun message)", + "select_conversation": "Sélectionnez une conversation", + "select_conversation_desc": "Choisissez-en une dans la liste pour voir l'échange complet", + "take_over": "Prendre en charge", + "resolve": "Résoudre", + "reopen": "Rouvrir", + "you_agent": "Vous (agent)", + "handoff_requested": "Transfert demandé", + "low_confidence": "Faible confiance", + "conversation_resolved": "Conversation résolue —", + "reopen_link": "rouvrir", + "to_reply": "pour répondre", + "type_reply": "Écrivez une réponse en tant qu'agent...", + "delete_conversation": "Supprimer cette conversation ?", + "failed_to_delete": "Échec de la suppression de la conversation", + "status_open": "Ouvert", + "status_agent": "Agent", + "status_resolved": "Résolu", + "upgrade_title": "Boîte de réception", + "upgrade_desc": "Passez à Starter pour lire toutes vos conversations de chatbot au même endroit." + }, + "leads": { + "title": "Prospects", + "subtitle": "Contacts collectés par vos chatbots", + "total_leads": "Total des prospects", + "this_month": "Ce mois-ci", + "filter_by_chatbot": "Filtrer par chatbot", + "clear_status_filter": "Effacer le filtre de statut", + "col_contact": "Contact", + "col_phone": "Téléphone", + "col_company": "Entreprise", + "col_status": "Statut", + "col_notes": "Notes", + "col_date": "Date", + "add_note": "Ajouter une note", + "notes_modal_title": "Notes — {{name}}", + "notes_placeholder": "Ajoutez des notes sur ce prospect...", + "no_leads_title": "Aucun prospect pour l'instant", + "no_leads_with_status": "Aucun prospect avec le statut « {{status}} »", + "no_leads_desc": "Activez la capture de prospects sur vos chatbots pour commencer à collecter des contacts.", + "no_leads_status_desc": "Essayez un autre filtre ou effacez celui en cours.", + "export_failed": "Échec de l'exportation", + "status_new": "Nouveau", + "status_contacted": "Contacté", + "status_qualified": "Qualifié", + "status_closed": "Fermé", + "status_lost": "Perdu", + "upgrade_title": "Capture de prospects", + "upgrade_desc": "Passez à Starter pour capturer et gérer les prospects de vos chatbots." + }, + "appointments": { + "title": "Rendez-vous", + "subtitle": "Réservations effectuées via vos chatbots", + "stat_today": "Aujourd'hui", + "stat_upcoming": "À venir", + "stat_confirmed": "Confirmé", + "stat_pending": "En attente", + "filter": "Filtrer", + "hours_label": "Horaires :", + "configure_chatbot_hours": "Configurer un chatbot...", + "enable_booking_title": "Activer la réservation sur un chatbot", + "enable_booking_desc": "Allez dans l'onglet Déploiement d'un chatbot et activez « Prise de rendez-vous » pour commencer à accepter des réservations.", + "configure_chatbot": "Configurer le chatbot →", + "no_appointments_title": "Aucun rendez-vous pour l'instant", + "no_appointments_desc": "Une fois que des clients réservent via votre chatbot, les rendez-vous apparaîtront ici.", + "today_label": "Aujourd'hui", + "to": "à", + "confirm_btn": "Confirmer", + "decline_btn": "Refuser", + "mark_complete": "Marquer terminé", + "cancel_btn": "Annuler", + "restore_btn": "Restaurer", + "hours_title": "Horaires d'ouverture", + "hours_desc": "Configurez quand les clients peuvent prendre rendez-vous.", + "hours_back": "← Retour", + "hours_closed": "Fermé", + "save_hours": "Enregistrer les horaires", + "hours_saved": "✓ Enregistré !", + "status_pending": "En attente", + "status_confirmed": "Confirmé", + "status_cancelled": "Annulé", + "status_completed": "Terminé", + "days_mon": "Lun", + "days_tue": "Mar", + "days_wed": "Mer", + "days_thu": "Jeu", + "days_fri": "Ven", + "days_sat": "Sam", + "days_sun": "Dim", + "upgrade_title": "Prise de rendez-vous", + "upgrade_desc": "Passez à Starter pour activer la prise de rendez-vous pour vos chatbots." + }, + "campaigns": { + "title": "Campagnes", + "subtitle": "Diffusez des messages à vos abonnés Telegram", + "new_campaign": "Nouvelle campagne", + "stat_campaigns": "Campagnes", + "stat_sent": "Envoyé", + "stat_delivered": "Messages délivrés", + "chatbot_label": "Chatbot", + "chatbot_hint": "Sera diffusé à tous les abonnés Telegram de ce chatbot.", + "campaign_name": "Nom de la campagne", + "campaign_name_placeholder": "ex. Promotion estivale, Annonce du nouveau menu...", + "message_label": "Message", + "message_placeholder": "Rédigez votre message de diffusion ici...", + "characters": "{{count}}/4000 caractères", + "create_campaign": "Créer la campagne", + "send_campaign": "Envoyer la campagne", + "delete_record": "Supprimer l'enregistrement", + "send_modal_title": "Envoyer cette campagne ?", + "send_modal_desc_one": "« {{title}} » sera envoyé à {{count}} abonné via Telegram.", + "send_modal_desc_other": "« {{title}} » sera envoyé à {{count}} abonnés via Telegram.", + "send_modal_warning": "Cette action est irréversible. Le message sera délivré immédiatement.", + "send_now": "Envoyer maintenant", + "delete_campaign": "Supprimer cette campagne ?", + "delete_campaign_record": "Supprimer cet enregistrement de campagne ?", + "subscriber_one": "{{count}} abonné", + "subscriber_other": "{{count}} abonnés", + "delivered": "délivrés", + "no_campaigns_title": "Aucune campagne pour l'instant", + "no_campaigns_desc": "Créez une campagne pour diffuser un message à tous vos abonnés Telegram en une fois.", + "no_chatbots_needed": "Vous avez besoin d'au moins un chatbot pour créer une campagne.", + "status_draft": "Brouillon", + "status_sending": "Envoi en cours...", + "status_sent": "Envoyé", + "status_failed": "Échec", + "upgrade_title": "Campagnes Telegram", + "upgrade_desc": "Passez à Starter pour diffuser des messages à vos abonnés Telegram." + }, + "analytics": { + "title": "Analytiques", + "subtitle": "Suivez les performances de vos chatbots", + "stat_conversations": "Conversations", + "stat_unique_users": "Utilisateurs uniques", + "stat_messages": "Messages", + "stat_avg_rating": "Note moyenne", + "stat_this_month": "{{count}} ce mois-ci", + "stat_across_all": "Tous les chatbots", + "stat_total_exchanged": "Total échangé", + "stat_across_rated": "Chatbots notés", + "stat_no_ratings": "Aucune note", + "monthly_conversations": "Conversations mensuelles", + "your_chatbots": "Vos chatbots", + "published": "publié", + "today": "Aujourd'hui", + "this_week": "Cette semaine", + "this_month": "Ce mois-ci", + "avg_msgs": "Msgs moy./convo", + "last_30_days": "30 derniers jours", + "top_questions": "Questions fréquentes", + "languages": "Langues", + "knowledge_gaps": "Lacunes — {{count}} sans réponse", + "add_content": "+ Ajouter du contenu →", + "gaps_desc": "Des clients ont posé ces questions mais votre bot n'a pas pu y répondre correctement. Ajoutez des documents ou des URL couvrant ces sujets.", + "more_gaps_one": "+{{count}} lacune supplémentaire", + "more_gaps_other": "+{{count}} lacunes supplémentaires", + "feedback": "Retours", + "helpful_pct": "{{pct}}% utile", + "peak_hour": "Pic : {{from}}h – {{to}}h", + "conversations_today": "{{count}} aujourd'hui", + "no_chatbots_title": "Aucun chatbot pour l'instant", + "no_chatbots_desc": "Créez votre premier chatbot pour commencer à voir les analytiques.", + "create_chatbot": "Créer un chatbot", + "unable_to_load": "Impossible de charger les analytiques", + "try_refreshing": "Veuillez actualiser la page.", + "upgrade_title": "Tableau de bord analytique", + "upgrade_desc": "Débloquez les analytiques pour voir les performances de vos chatbots — conversations, engagement, questions fréquentes, et plus.", + "upgrade_button": "Passer à Starter — 3$/mois", + "upgrade_note": "Disponible sur les plans Starter et Pro", + "plan_badge": "Plan {{plan}}" + }, + "settings": { + "title": "Paramètres", + "light_mode": "Mode clair", + "dark_mode": "Mode sombre", + "tab_profile": "Profil", + "tab_billing": "Facturation", + "profile_info": "Informations du profil", + "email": "E-mail", + "email_hint": "L'e-mail ne peut pas être modifié", + "company_name": "Nom de l'entreprise", + "company_placeholder": "Nom de votre entreprise", + "plan_label": "Plan", + "manage_plan": "Gérer le plan", + "change_password": "Modifier le mot de passe", + "current_password": "Mot de passe actuel", + "current_password_placeholder": "Entrez le mot de passe actuel", + "new_password": "Nouveau mot de passe", + "new_password_placeholder": "Min. 8 caractères", + "new_password_hint": "Laissez vide pour conserver le mot de passe actuel", + "danger_zone": "Zone dangereuse", + "danger_desc": "Supprimez définitivement votre compte, tous vos chatbots, documents et données. Cette action est irréversible.", + "delete_account_btn": "Supprimer le compte", + "delete_account_title": "Supprimer le compte", + "delete_account_desc": "Cela supprimera définitivement votre compte et toutes les données associées, y compris les chatbots, documents, conversations et prospects.", + "delete_account_desc_bold": "Cette action est irréversible.", + "type_delete": "Tapez SUPPRIMER pour confirmer :", + "profile_updated": "Profil mis à jour avec succès", + "update_failed": "Échec de la mise à jour du profil", + "language_label": "Langue", + "language_updated": "Langue mise à jour", + "lang_en": "English", + "lang_fr": "Français", + "current_plan": "Plan actuel", + "status_label": "Statut :", + "status_active": "Actif", + "renewal_date": "Date de renouvellement", + "upgrade_plan": "✨ Améliorer le plan", + "manage_billing": "Gérer la facturation", + "plan_features": "Fonctionnalités du plan", + "chatbots_published": "Chatbots publiés", + "conversations_per_month": "Conversations / mois", + "code_export": "Export de code", + "chatbot_suffix": "chatbot(s)", + "conversations_suffix": "conversations", + "billing_footer_paid": "💳 Gestion simplifiée de l'abonnement", + "billing_footer_free": "🚀 Débloquez plus de fonctionnalités en améliorant votre plan" + }, + "builder": { + "loading": "Chargement du chatbot…", + "choose_template": "Choisir un modèle", + "choose_template_sub": "Démarrez à partir d'un modèle ou créez de zéro", + "scratch": "Créer de zéro", + "create_chatbot": "Créer le chatbot", + "untitled": "Chatbot sans titre", + "published": "Publié", + "draft": "Brouillon", + "create": "Créer", + "save": "Enregistrer", + "tab_settings": "Paramètres", + "tab_documents": "Documents", + "tab_preview": "Aperçu", + "tab_testing": "Tests", + "tab_deploy": "Déploiement", + "save_first_testing": "Enregistrez d'abord votre chatbot pour lancer des tests.", + "testing_title": "Tests du bot", + "testing_desc": "Posez des questions à votre chatbot et inspectez les réponses, scores de confiance et documents sources.", + "testing_placeholder": "ex. Quels sont vos horaires d'ouverture ?", + "testing_add": "Ajouter une question", + "testing_run": "Lancer les tests", + "testing_running": "En cours…", + "testing_results": "{{count}} résultat(s)", + "testing_sources": "Sources utilisées", + "testing_model": "Modèle", + "testing_error": "Test échoué. Vérifiez que votre chatbot a une base de connaissances.", + "refresh_url": "Re-scraper cette URL", + "created": "Chatbot créé !", + "create_failed": "Échec de la création", + "saved": "Paramètres enregistrés !", + "save_failed": "Échec de l'enregistrement", + "name_required": "Le nom du chatbot est requis", + "save_first_docs": "Enregistrez votre chatbot avant d'importer des documents.", + "save_first_preview": "Enregistrez votre chatbot avant de le prévisualiser.", + "save_first_deploy": "Enregistrez votre chatbot pour accéder aux options de déploiement.", + "save_first_hint": "Remplissez l'onglet Paramètres et cliquez sur Enregistrer pour continuer.", + "section_basic": "Informations de base", + "section_basic_desc": "Nom, description et message d'accueil de votre chatbot", + "chatbot_name": "Nom du chatbot", + "chatbot_name_placeholder": "ex. Bot de support client", + "description": "Description", + "description_placeholder": "Que fait ce chatbot ?", + "welcome_message": "Message d'accueil", + "welcome_hint": "Le premier message que les visiteurs verront en ouvrant le chat", + "system_prompt": "Invite système", + "system_prompt_placeholder": "Vous êtes un assistant utile pour...", + "system_prompt_hint": "Instructions personnalisées pour le comportement et la personnalité de l'IA (facultatif)", + "section_appearance": "Apparence", + "section_appearance_desc": "Logo et couleur de marque affichés dans le widget de chat", + "logo_label": "Logo du chatbot", + "logo_hint": "Importez le logo de votre entreprise. Il apparaîtra dans l'en-tête du chat.", + "brand_color": "Couleur de marque", + "color_preview": "Aperçu du bouton de chat", + "section_advanced": "Paramètres avancés", + "section_advanced_desc": "Modèle IA, température, longueur des réponses", + "ai_model": "Modèle IA", + "models_loading": "Chargement des modèles disponibles...", + "models_empty": "Aucun modèle disponible sur votre plan actuel.", + "models_upgrade": "Améliorer", + "models_upgrade_suffix": "pour accéder aux modèles IA.", + "response_params": "Paramètres de réponse", + "temperature": "Température", + "temp_precise": "Précis", + "temp_creative": "Créatif", + "max_tokens": "Tokens max", + "max_tokens_hint": "Longueur maximale de la réponse", + "section_classification": "Classification", + "section_classification_desc": "Aide les utilisateurs à découvrir votre chatbot sur la marketplace", + "select_category": "Sélectionner une catégorie", + "select_industry": "Sélectionner un secteur", + "logo_uploaded": "Logo importé", + "logo_remove": "Supprimer le logo", + "logo_drop": "Déposez votre logo ici", + "logo_click": "Cliquez ou faites glisser pour importer un logo", + "logo_formats": "PNG, JPG, SVG ou WebP · Max 2 Mo", + "logo_processing": "Traitement en cours...", + "logo_error_type": "Veuillez importer une image PNG, JPG, GIF, SVG ou WebP.", + "logo_error_size": "L'image doit faire moins de 2 Mo.", + "logo_error_upload": "Échec de l'importation. Veuillez réessayer.", + "section_upload": "Importer des documents", + "section_upload_desc": "PDF, DOCX, CSV, XLSX, TXT, MD — utilisés pour entraîner la base de connaissances de votre chatbot", + "drop_files": "Déposez les fichiers ici", + "click_upload": "Cliquez ou faites glisser des fichiers pour les importer", + "uploading": "Importation en cours...", + "upload_success": "Documents importés avec succès !", + "docs_empty": "Aucun document pour l'instant", + "docs_empty_hint": "Importez des fichiers ci-dessus pour construire la base de connaissances de votre chatbot.", + "doc_count_one": "{{count}} document", + "doc_count_other": "{{count}} documents", + "chunks": "{{n}} fragments", + "section_urls": "Sources URL", + "section_urls_desc": "Ajoutez des pages web à la base de connaissances de votre chatbot", + "add_url": "Ajouter une URL", + "url_failed": "Échec de l'ajout de l'URL", + "section_chat_link": "Lien de chat public", + "section_chat_link_desc": "Partagez un lien direct vers votre chatbot avec n'importe qui", + "copy": "Copier", + "copied": "Copié", + "publish_for_link": "Publiez votre chatbot dans les paramètres de déploiement pour obtenir un lien de chat public.", + "section_embed": "Code d'intégration", + "section_embed_desc": "Ajoutez un widget de chat à n'importe quel site web en une ligne de code", + "publish_for_embed": "Publiez votre chatbot pour obtenir le code d'intégration.", + "section_lead": "Capture de prospects", + "section_lead_desc": "Collectez les informations des visiteurs avant ou pendant la conversation", + "lead_enable": "Activer la capture de prospects", + "lead_enable_sub": "Demander aux visiteurs leurs coordonnées", + "collect_fields": "Champs à collecter", + "required": "requis", + "when_show": "Quand afficher le formulaire", + "after_first": "Après le premier message", + "before_first": "Avant le premier message", + "section_handoff": "Transfert à un humain", + "section_handoff_desc": "Permettre aux visiteurs de demander à parler à un agent humain", + "handoff_enable": "Activer le transfert humain", + "handoff_enable_sub": "Déclenché quand l'utilisateur dit « humain », « agent », etc.", + "handoff_message_label": "Message de transfert", + "handoff_webhook_note": "Configurez l'URL webhook n8n dans votre backend pour recevoir les notifications.", + "section_branding": "Marque", + "section_branding_desc": "Contrôlez l'attribution Contexta dans votre widget de chat", + "show_branding": "Afficher « Propulsé par Contexta »", + "show_branding_sub": "Supprimez la marque en passant au plan Pro ou supérieur", + "section_booking": "Prise de rendez-vous", + "section_booking_desc": "Permettez aux clients de réserver des rendez-vous directement via votre chatbot", + "booking_enable": "Activer la prise de rendez-vous", + "booking_enable_sub": "Lorsqu'activé, le chatbot guidera les utilisateurs vers votre page de réservation et en parlera dans les conversations.", + "booking_url_label": "URL de la page de réservation :", + "booking_url_hint": "Partagez ce lien sur votre site web ou vos réseaux sociaux. Définissez vos horaires disponibles dans la", + "booking_url_hint_link": "page Rendez-vous", + "section_channels": "Canaux de messagerie", + "section_channels_desc": "Connectez votre chatbot à Telegram", + "telegram_connected": "Connecté", + "telegram_share": "Partagez ce lien de bot avec vos clients — ils l'ouvrent et commencent à discuter.", + "telegram_owner_notice": "Pour recevoir les alertes de transfert, ouvrez votre bot et envoyez-lui", + "telegram_owner_notice2": "Cela vous enregistrera en tant que propriétaire et vous notifiera ici quand un visiteur a besoin d'aide humaine.", + "telegram_disconnect": "Déconnecter", + "telegram_how_title": "Comment créer un bot Telegram (2 minutes) :", + "telegram_step1": "Ouvrez Telegram et recherchez @BotFather", + "telegram_step2": "Envoyez /newbot", + "telegram_step3": "Choisissez un nom et un nom d'utilisateur pour votre bot", + "telegram_step4": "BotFather vous enverra un token — copiez-le", + "telegram_step5": "Collez le token ci-dessous et cliquez sur Connecter", + "telegram_share_hint": "Une fois connecté, partagez le lien de votre bot (ex. t.me/VotreBotName) avec vos clients.", + "telegram_placeholder": "Token du bot depuis @BotFather", + "telegram_connect": "Connecter", + "telegram_connect_failed": "Échec de la connexion. Vérifiez votre token.", + "embed_hint_html": "Collez avant la balise fermante dans votre fichier HTML.", + "embed_hint_react": "Ajoutez à votre index.html (dans le dossier public/) avant la balise fermante .", + "embed_hint_nextjs": "Utilisez le composant Script intégré dans votre layout racine pour qu'il se charge sur chaque page.", + "embed_hint_wordpress": "Allez dans Apparence → Éditeur de fichiers → footer.php et collez avant . Ou utilisez le plugin « Insert Headers and Footers ».", + "embed_hint_webflow": "Allez dans Paramètres du site → Code personnalisé → Code de pied de page et collez le script. Republiez votre site.", + "embed_hint_shopify": "Allez dans Boutique en ligne → Thèmes → Modifier le code → layout/theme.liquid et collez avant .", + "model_default": "(par défaut)" + }, + "marketplace": { + "title": "Marketplace de chatbots IA", + "subtitle": "Découvrez et interagissez avec des chatbots IA créés par des entreprises — prêts à répondre à vos questions instantanément.", + "search_placeholder": "Rechercher des chatbots par nom ou description...", + "filters": "Filtres", + "category": "Catégorie", + "all": "Tous", + "industry": "Secteur", + "all_industries": "Tous les secteurs", + "clear_all_filters": "Effacer tous les filtres", + "clear_filters": "Effacer les filtres", + "no_chatbots_title": "Aucun chatbot trouvé", + "no_chatbots_filtered": "Essayez d'ajuster vos filtres ou votre recherche.", + "no_chatbots_empty": "Soyez le premier à publier votre chatbot IA sur la marketplace !", + "create_chatbot": "Créer un chatbot", + "available_one": "{{count}} chatbot disponible", + "available_other": "{{count}} chatbots disponibles", + "by": "par {{name}}", + "chat_now": "Démarrer →", + "conversations": "{{count}} conversations", + "back_to_marketplace": "Retour à la marketplace", + "not_found_title": "Chatbot introuvable", + "not_found_desc": "Ce chatbot a peut-être été dépublié ou supprimé.", + "submit_rating": "Soumettre", + "your_rating": "Votre note", + "login_to_rate": "Connectez-vous pour noter ce chatbot", + "ratings": "avis" + }, + "auth": { + "login_title": "Bon retour", + "login_subtitle": "Connectez-vous à votre compte Contexta", + "email": "E-mail", + "password": "Mot de passe", + "sign_in": "Se connecter", + "no_account": "Pas de compte ?", + "sign_up_free": "S'inscrire gratuitement", + "forgot_password": "Mot de passe oublié ?", + "login_failed": "Échec de la connexion. Vérifiez vos identifiants.", + "signup_title": "Créez votre compte", + "signup_subtitle": "Commencez à créer des chatbots IA — gratuit pour toujours", + "company_name": "Nom de l'entreprise", + "create_free_account": "Créer un compte gratuit", + "terms_text": "En vous inscrivant, vous acceptez nos", + "terms_of_service": "Conditions d'utilisation", + "and": "et notre", + "privacy_policy": "Politique de confidentialité", + "already_account": "Déjà un compte ?", + "already_confirmed": "Déjà confirmé ?", + "check_inbox_title": "Vérifiez votre boîte mail", + "check_inbox_desc": "Un lien de confirmation a été envoyé à", + "password_min_8": "Le mot de passe doit comporter au moins 8 caractères", + "signup_failed": "Échec de l'inscription. Veuillez réessayer.", + "forgot_title": "Réinitialisez votre mot de passe", + "forgot_subtitle": "Nous enverrons un lien de réinitialisation à votre adresse e-mail.", + "email_address": "Adresse e-mail", + "send_reset_link": "Envoyer le lien de réinitialisation", + "back_to_signin": "Retour à la connexion", + "forgot_sent_title": "Vérifiez votre boîte mail", + "forgot_sent_desc_one": "Si {{email}} est enregistré,", + "forgot_sent_desc_two": "un lien de réinitialisation a été envoyé.", + "forgot_error": "Une erreur s'est produite. Veuillez réessayer.", + "reset_title": "Définir un nouveau mot de passe", + "reset_subtitle": "Choisissez un mot de passe fort pour votre compte.", + "new_password": "Nouveau mot de passe", + "confirm_password": "Confirmer le mot de passe", + "confirm_placeholder": "Répétez le mot de passe", + "set_new_password": "Définir le nouveau mot de passe", + "link_expired_title": "Lien expiré", + "request_new_link": "Demander un nouveau lien de réinitialisation", + "passwords_dont_match": "Les mots de passe ne correspondent pas", + "failed_to_reset": "Échec de la réinitialisation du mot de passe. Le lien a peut-être expiré.", + "branding_headline": "Votre chatbot IA,\nprêt en quelques minutes.", + "branding_subtext": "Formez-le sur vos documents, gérez chaque conversation, capturez des prospects et acceptez des rendez-vous — tout en un.", + "branding_feature_1": "PDF, DOCX, CSV, URL — toutes vos sources", + "branding_feature_2": "Boîte de réception, prospects & rendez-vous", + "branding_feature_3": "Site web, Telegram & marketplace", + "branding_footer": "La confiance des entreprises qui modernisent leur relation client." + }, + "pricing": { + "badge": "Tarifs", + "title": "Des tarifs simples et transparents", + "subtitle": "Démarrez gratuitement et publiez votre chatbot dès 12 $/mois. Conçu pour les particuliers, les PME, les agences et les grandes entreprises.", + "monthly": "Mensuel", + "yearly": "Annuel", + "per_month": "/mois", + "save_yr": "Économisez {{amount}} $/an", + "custom_price": "Sur mesure", + "current_plan_badge": "Plan actuel", + "most_popular": "Le plus populaire", + "cta_current": "Plan actuel", + "cta_free": "Commencer gratuitement", + "cta_contact": "Contacter les ventes", + "cta_downgrade": "Rétrograder", + "cta_upgrade": "Améliorer", + "faq_title": "Questions fréquentes", + "faq_subtitle": "Tout ce que vous devez savoir sur les plans Contexta.", + "plan_free": "Gratuit", + "plan_free_desc": "Créez, testez et publiez votre premier chatbot — sans carte bancaire", + "plan_starter": "Starter", + "plan_starter_desc": "Pour les indépendants : chat en direct, prospects, réservation et campagnes", + "plan_business": "Business", + "plan_business_desc": "Pour les entreprises en croissance : IA premium, réservation illimitée, analytics complet", + "plan_agency": "Agency", + "plan_agency_desc": "Pour les agences : tout illimité, prêt pour le white-label", + "plan_enterprise": "Enterprise", + "plan_enterprise_desc": "Pour les grandes organisations avec des besoins personnalisés et des SLA", + "feat_free": [ + "1 chatbot publié", + "100 conversations/mois", + "3 documents par chatbot", + "Lien de chat public + intégration site web", + "Modèle Llama 3.3 70B", + "Boîte de réception en lecture seule (sans réponses)", + "Prospects en lecture seule (sans modification)", + "Tableau de bord analytics", + "Rendez-vous et campagnes", + "Canaux de messagerie", + "Supprimer « Propulsé par Contexta »" + ], + "feat_starter": [ + "Tout le plan Gratuit", + "3 chatbots publiés", + "1 500 conversations/mois", + "10 documents par chatbot", + "4 modèles Fireworks IA (Qwen3, DeepSeek, Kimi, Llama)", + "Boîte de réception + réponses agent", + "CRM prospects complet (statut + notes)", + "Prise de rendez-vous (1 chatbot)", + "Campagnes Telegram (3/mois · 500 destinataires)", + "Tableau de bord analytics", + "Suggestions de lacunes de connaissances", + "Modèles premium (GPT-4o, Claude, Gemini)", + "Supprimer « Propulsé par Contexta »" + ], + "feat_business": [ + "Tout le plan Starter", + "10 chatbots publiés", + "5 000 conversations/mois", + "50 documents par chatbot", + "GPT-4o, Claude Haiku 4.5, Gemini 2.5", + "Prise de rendez-vous (tous les chatbots)", + "Campagnes illimitées · 5 000 destinataires", + "Suggestions de lacunes de connaissances", + "Supprimer « Propulsé par Contexta »", + "Sources URL illimitées" + ], + "feat_agency": [ + "Tout le plan Business", + "Chatbots publiés illimités", + "20 000 conversations/mois", + "Documents illimités", + "Destinataires de campagnes illimités", + "Export de code (FastAPI + React)", + "Support dédié" + ], + "feat_enterprise": [ + "Tout le plan Agency", + "Conversations illimitées", + "Plateforme white-label", + "SSO (SAML)", + "Garanties SLA", + "Responsable de compte dédié", + "Support téléphonique 24h/24 7j/7" + ], + "faq": [ + { + "q": "Puis-je utiliser le plan gratuit indéfiniment ?", + "a": "Oui ! Créez et testez des chatbots gratuitement. Vos chatbots resteront en mode prévisualisation jusqu'à votre abonnement." + }, + { + "q": "Qu'est-ce que l'export de code ?", + "a": "Les utilisateurs du plan Agency peuvent exporter leur chatbot sous forme de package complet prêt pour la production, incluant un backend FastAPI et un widget React TypeScript — pour un hébergement autonome total." + }, + { + "q": "Ai-je besoin de mes propres clés API ?", + "a": "Non — les clés API sont gérées par Contexta. Si vous exportez le code avec le plan Agency, vous aurez besoin de vos propres clés pour un déploiement auto-hébergé." + }, + { + "q": "Puis-je annuler à tout moment ?", + "a": "Oui, annulez à tout moment. Vos chatbots reviendront au plan gratuit à la fin de votre période de facturation." + }, + { + "q": "Que se passe-t-il si j'atteins ma limite de conversations ?", + "a": "Votre chatbot affichera un message convivial pour réessayer plus tard. Améliorez votre plan pour plus de conversations." + }, + { + "q": "Je suis une petite entreprise. Quel plan me convient ?", + "a": "Commencez avec Starter à 12 $/mois — 1 chatbot publié, 1 500 conversations, analytics, capture de prospects et Telegram. Parfait pour les restaurants, boutiques, salons et plus. Passez à Business pour des modèles IA premium et plus de capacité." + } + ] + } +} diff --git a/src/main.tsx b/src/main.tsx index c469073..12f44e4 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -5,10 +5,18 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query' import { App } from './App' import { ToastProvider } from '@/contexts/ToastContext' import { initTheme } from '@/store/themeStore' +import { useAuthStore } from '@/store/authStore' +import i18n from '@/i18n/i18n' import './index.css' initTheme() +// Restore persisted language on page reload +const persistedUser = useAuthStore.getState().user +if (persistedUser?.language) { + i18n.changeLanguage(persistedUser.language) +} + const queryClient = new QueryClient({ defaultOptions: { queries: { diff --git a/src/pages/AnalyticsPage.tsx b/src/pages/AnalyticsPage.tsx index 04f5e7d..71e6953 100644 --- a/src/pages/AnalyticsPage.tsx +++ b/src/pages/AnalyticsPage.tsx @@ -1,6 +1,7 @@ import React, { useState } from 'react' import { useQuery } from '@tanstack/react-query' import { useNavigate } from 'react-router-dom' +import { useTranslation } from 'react-i18next' import { analyticsAPI } from '@/services/api' import { Card, Button, Badge } from '@/components/ui' import { @@ -11,12 +12,6 @@ import { import { SkeletonStatCard } from '@/components/Skeletons' import { cn } from '@/lib/utils' -// ═══════════════════════════════════════════════════════════════════════════════ -// ANALYTICS PAGE — Available for Starter and Pro plans -// Shows: conversations, unique users, ratings, top queries, daily trends -// Does NOT show: LLM costs, token usage costs, API spending -// ═══════════════════════════════════════════════════════════════════════════════ - interface DailyConversation { date: string count: number @@ -65,19 +60,19 @@ interface OverviewData { // ─── Mini bar chart component ───────────────────────────────────────────────── const MiniBarChart: React.FC<{ data: DailyConversation[] }> = ({ data }) => { + const { t } = useTranslation() const [tooltip, setTooltip] = useState<{ date: string; count: number; idx: number } | null>(null) if (!data.length) { return (
- No data yet + {t('common.no_data')}
) } const max = Math.max(...data.map(d => d.count), 1) - // Fill last 30 days const today = new Date() const days: { date: string; count: number }[] = [] for (let i = 29; i >= 0; i--) { @@ -90,7 +85,6 @@ const MiniBarChart: React.FC<{ data: DailyConversation[] }> = ({ data }) => { return (
- {/* Grid lines */}
{[0, 1, 2, 3].map(i => (
@@ -117,7 +111,6 @@ const MiniBarChart: React.FC<{ data: DailyConversation[] }> = ({ data }) => { ))}
- {/* Tooltip */} {tooltip && (
= ({ used, limit }) => { + const { t } = useTranslation() const pct = limit > 0 ? Math.min((used / limit) * 100, 100) : 0 const isHigh = pct > 80 const isFull = pct >= 100 @@ -178,7 +172,7 @@ const UsageBar: React.FC<{ used: number; limit: number }> = ({ used, limit }) => return (
- Monthly conversations + {t('analytics.monthly_conversations')} = ({ used, limit }) =>
0 - {Math.round(pct)}% used + {Math.round(pct)}% {limit.toLocaleString()}
@@ -208,6 +202,8 @@ const UsageBar: React.FC<{ used: number; limit: number }> = ({ used, limit }) => // ─── Chatbot detail row ─────────────────────────────────────────────────────── const ChatbotRow: React.FC<{ chatbot: ChatbotAnalytics }> = ({ chatbot }) => { + const { t } = useTranslation() + const navigate = useNavigate() const [expanded, setExpanded] = useState(false) const feedbackTotal = chatbot.feedback_positive + chatbot.feedback_negative @@ -228,7 +224,7 @@ const ChatbotRow: React.FC<{ chatbot: ChatbotAnalytics }> = ({ chatbot }) => {

{chatbot.chatbot_name}

- {chatbot.total_conversations.toLocaleString()} conversations · {chatbot.unique_sessions.toLocaleString()} users + {chatbot.total_conversations.toLocaleString()} · {chatbot.unique_sessions.toLocaleString()}

@@ -240,7 +236,7 @@ const ChatbotRow: React.FC<{ chatbot: ChatbotAnalytics }> = ({ chatbot }) => {
)}
- {chatbot.conversations_today} today + {t('analytics.conversations_today', { count: chatbot.conversations_today })}
{expanded ? : } @@ -254,10 +250,10 @@ const ChatbotRow: React.FC<{ chatbot: ChatbotAnalytics }> = ({ chatbot }) => { {/* Stats row */}
{[ - { label: 'Today', value: chatbot.conversations_today }, - { label: 'This week', value: chatbot.conversations_this_week }, - { label: 'This month', value: chatbot.conversations_this_month }, - { label: 'Avg msgs/convo', value: chatbot.average_messages_per_conversation }, + { label: t('analytics.today'), value: chatbot.conversations_today }, + { label: t('analytics.this_week'), value: chatbot.conversations_this_week }, + { label: t('analytics.this_month'), value: chatbot.conversations_this_month }, + { label: t('analytics.avg_msgs'), value: chatbot.average_messages_per_conversation }, ].map(({ label, value }) => (

{label}

@@ -268,7 +264,7 @@ const ChatbotRow: React.FC<{ chatbot: ChatbotAnalytics }> = ({ chatbot }) => { {/* Daily chart */}
-

Last 30 days

+

{t('analytics.last_30_days')}

@@ -277,7 +273,7 @@ const ChatbotRow: React.FC<{ chatbot: ChatbotAnalytics }> = ({ chatbot }) => { {chatbot.top_queries.length > 0 && (
-

Top questions

+

{t('analytics.top_questions')}

{chatbot.top_queries.slice(0, 5).map((q, i) => (
@@ -296,7 +292,7 @@ const ChatbotRow: React.FC<{ chatbot: ChatbotAnalytics }> = ({ chatbot }) => { {Object.keys(chatbot.languages_used).length > 0 && (
-

Languages

+

{t('analytics.languages')}

{Object.entries(chatbot.languages_used) .sort(([, a], [, b]) => b - a) @@ -327,26 +323,24 @@ const ChatbotRow: React.FC<{ chatbot: ChatbotAnalytics }> = ({ chatbot }) => { )}
- {/* Knowledge Gaps — Phase 3: actionable suggestions */} + {/* Knowledge Gaps */} {chatbot.unanswered_queries && chatbot.unanswered_queries.length > 0 && (

- Knowledge gaps — {chatbot.unanswered_count} unanswered + {t('analytics.knowledge_gaps', { count: chatbot.unanswered_count })}

-

- Customers asked these questions but your bot couldn't answer well. Add documents or URL sources covering these topics. -

+

{t('analytics.gaps_desc')}

{chatbot.unanswered_queries.slice(0, 6).map((q, i) => (
= ({ chatbot }) => { "{q.query}"
- {q.count}× asked + {q.count}×
@@ -364,7 +358,7 @@ const ChatbotRow: React.FC<{ chatbot: ChatbotAnalytics }> = ({ chatbot }) => {
{chatbot.unanswered_queries.length > 6 && (

- +{chatbot.unanswered_queries.length - 6} more gaps + {t('analytics.more_gaps', { count: chatbot.unanswered_queries.length - 6 })}

)}
@@ -374,7 +368,7 @@ const ChatbotRow: React.FC<{ chatbot: ChatbotAnalytics }> = ({ chatbot }) => {
{feedbackTotal > 0 && (
- Feedback + {t('analytics.feedback')}
{chatbot.feedback_positive} @@ -385,7 +379,7 @@ const ChatbotRow: React.FC<{ chatbot: ChatbotAnalytics }> = ({ chatbot }) => {
{helpfulPct !== null && ( - {helpfulPct}% helpful + {t('analytics.helpful_pct', { pct: helpfulPct })} )}
@@ -394,7 +388,7 @@ const ChatbotRow: React.FC<{ chatbot: ChatbotAnalytics }> = ({ chatbot }) => { {chatbot.peak_hour !== null && (
- Peak: {chatbot.peak_hour}:00 – {chatbot.peak_hour + 1}:00 + {t('analytics.peak_hour', { from: chatbot.peak_hour, to: chatbot.peak_hour + 1 })}
)}
@@ -409,16 +403,16 @@ const ChatbotRow: React.FC<{ chatbot: ChatbotAnalytics }> = ({ chatbot }) => { // ═══════════════════════════════════════════════════════════════════════════════ export const AnalyticsPage: React.FC = () => { + const { t } = useTranslation() const navigate = useNavigate() const { data, isLoading, error } = useQuery({ queryKey: ['analytics-overview'], queryFn: analyticsAPI.overview, - staleTime: 60_000, // 1 min cache + staleTime: 60_000, retry: false, }) - // Handle plan gate (402 response) if (error && (error as { response?: { status?: number } })?.response?.status === 402) { return (
@@ -426,14 +420,10 @@ export const AnalyticsPage: React.FC = () => {
-

Analytics Dashboard

-

- Unlock analytics to see how your chatbots are performing — conversations, user engagement, top questions, and more. -

- -

Available on Starter and Pro plans

+

{t('analytics.upgrade_title')}

+

{t('analytics.upgrade_desc')}

+ +

{t('analytics.upgrade_note')}

) @@ -442,7 +432,6 @@ export const AnalyticsPage: React.FC = () => { if (isLoading) { return (
- {/* Header skeleton */}
@@ -450,13 +439,10 @@ export const AnalyticsPage: React.FC = () => {
- {/* Usage bar skeleton */}
- {/* Stat cards skeleton */}
{[0, 1, 2, 3].map(i => )}
- {/* Chatbot rows skeleton */}
{[0, 1].map(i => (
@@ -481,8 +467,8 @@ export const AnalyticsPage: React.FC = () => {
-

Unable to load analytics

-

Please try refreshing the page.

+

{t('analytics.unable_to_load')}

+

{t('analytics.try_refreshing')}

) @@ -497,11 +483,11 @@ export const AnalyticsPage: React.FC = () => {
-

Analytics

-

Track how your chatbots are performing

+

{t('analytics.title')}

+

{t('analytics.subtitle')}

- {data.plan} plan + {t('analytics.plan_badge', { plan: data.plan })}
{/* ── Usage bar ── */} @@ -512,31 +498,31 @@ export const AnalyticsPage: React.FC = () => { {/* ── Overview stat cards ── */}
} - subtitle={`${data.conversations_this_month} this month`} + subtitle={t('analytics.stat_this_month', { count: data.conversations_this_month })} color="primary" /> } - subtitle="Across all chatbots" + subtitle={t('analytics.stat_across_all')} color="sky" /> } - subtitle="Total exchanged" + subtitle={t('analytics.stat_total_exchanged')} color="violet" /> } - subtitle={data.average_rating ? 'Across rated chatbots' : 'No ratings yet'} + subtitle={data.average_rating ? t('analytics.stat_across_rated') : t('analytics.stat_no_ratings')} color="amber" />
@@ -544,13 +530,13 @@ export const AnalyticsPage: React.FC = () => { {/* ── Chatbot breakdown header ── */}

- Your chatbots + {t('analytics.your_chatbots')} {data.total_chatbots}

- {data.published_chatbots} published + {data.published_chatbots} {t('analytics.published')}

@@ -560,10 +546,10 @@ export const AnalyticsPage: React.FC = () => {
-

No chatbots yet

-

Create your first chatbot to start seeing analytics.

+

{t('analytics.no_chatbots_title')}

+

{t('analytics.no_chatbots_desc')}

) : ( diff --git a/src/pages/AppointmentsPage.tsx b/src/pages/AppointmentsPage.tsx index b3de485..f4aaa98 100644 --- a/src/pages/AppointmentsPage.tsx +++ b/src/pages/AppointmentsPage.tsx @@ -1,27 +1,19 @@ import React, { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { useNavigate } from 'react-router-dom' +import { useTranslation } from 'react-i18next' import { appointmentsAPI, chatbotsAPI } from '@/services/api' import { Card, Button, Spinner } from '@/components/ui' import { - Calendar, Clock, User, Phone, Filter, Lock, CheckCircle2, - XCircle, RotateCcw, ChevronDown, Settings, CalendarDays, + Calendar, Clock, Phone, Filter, Lock, CheckCircle2, + XCircle, RotateCcw, Settings, CalendarDays, } from 'lucide-react' import type { Appointment, Chatbot, BusinessHoursEntry } from '@/types' import { cn } from '@/lib/utils' -const STATUS_CONFIG: Record = { - pending: { label: 'Pending', color: 'bg-yellow-100 text-yellow-700', icon: Clock }, - confirmed: { label: 'Confirmed', color: 'bg-green-100 text-green-700', icon: CheckCircle2 }, - cancelled: { label: 'Cancelled', color: 'bg-red-100 text-red-600', icon: XCircle }, - completed: { label: 'Completed', color: 'bg-gray-100 text-gray-600', icon: CheckCircle2 }, -} - -const DAY_LABELS = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] - -const DEFAULT_HOURS: BusinessHoursEntry[] = DAY_LABELS.map((_, i) => ({ +const DEFAULT_HOURS: BusinessHoursEntry[] = Array.from({ length: 7 }, (_, i) => ({ day_of_week: i, - is_open: i < 5, // Mon–Fri open by default + is_open: i < 5, open_time: '09:00', close_time: '17:00', slot_duration_minutes: 60, @@ -33,15 +25,17 @@ const HoursSettings: React.FC<{ chatbotId: string; onClose: () => void }> = ({ c const queryClient = useQueryClient() const [hours, setHours] = useState(DEFAULT_HOURS) const [saved, setSaved] = useState(false) + const { t } = useTranslation() + + const DAY_KEYS = ['days_mon', 'days_tue', 'days_wed', 'days_thu', 'days_fri', 'days_sat', 'days_sun'] as const const { isLoading } = useQuery({ queryKey: ['business-hours', chatbotId], queryFn: () => appointmentsAPI.getHours(chatbotId), - onSuccess: (data) => { + onSuccess: (data: BusinessHoursEntry[]) => { if (data && data.length > 0) { - // Merge fetched data with defaults const merged = DEFAULT_HOURS.map(d => { - const found = data.find(h => h.day_of_week === d.day_of_week) + const found = data.find((h: BusinessHoursEntry) => h.day_of_week === d.day_of_week) return found ? { ...d, ...found } : d }) setHours(merged) @@ -67,10 +61,10 @@ const HoursSettings: React.FC<{ chatbotId: string; onClose: () => void }> = ({ c return (
-

Business Hours

- +

{t('appointments.hours_title')}

+
-

Configure when customers can book appointments.

+

{t('appointments.hours_desc')}

{hours.map((h, i) => ( @@ -84,7 +78,7 @@ const HoursSettings: React.FC<{ chatbotId: string; onClose: () => void }> = ({ c className="w-3.5 h-3.5 accent-primary-600" /> - {DAY_LABELS[i].slice(0, 3)} + {t(`appointments.${DAY_KEYS[i]}`)}
@@ -96,7 +90,7 @@ const HoursSettings: React.FC<{ chatbotId: string; onClose: () => void }> = ({ c onChange={e => update(i, 'open_time', e.target.value)} className="text-xs border border-gray-200 rounded-lg px-2 py-1.5 bg-white focus:outline-none focus:ring-1 focus:ring-primary-400" /> - to + {t('appointments.to')} void }> = ({ c ) : ( - Closed + {t('appointments.hours_closed')} )}
))} @@ -128,7 +122,7 @@ const HoursSettings: React.FC<{ chatbotId: string; onClose: () => void }> = ({ c className="w-full gap-2" size="sm" > - {save.isPending ? : saved ? '✓ Saved!' : 'Save Hours'} + {save.isPending ? : saved ? t('appointments.hours_saved') : t('appointments.save_hours')}
) @@ -142,6 +136,14 @@ export const AppointmentsPage: React.FC = () => { const [chatbotFilter, setChatbotFilter] = useState('') const [statusFilter, setStatusFilter] = useState('') const [settingsChatbotId, setSettingsChatbotId] = useState(null) + const { t } = useTranslation() + + const STATUS_CONFIG: Record = { + pending: { label: t('appointments.status_pending'), color: 'bg-yellow-100 text-yellow-700', icon: Clock }, + confirmed: { label: t('appointments.status_confirmed'), color: 'bg-green-100 text-green-700', icon: CheckCircle2 }, + cancelled: { label: t('appointments.status_cancelled'), color: 'bg-red-100 text-red-600', icon: XCircle }, + completed: { label: t('appointments.status_completed'), color: 'bg-gray-100 text-gray-600', icon: CheckCircle2 }, + } const { data: chatbots = [] } = useQuery({ queryKey: ['chatbots'], @@ -172,9 +174,9 @@ export const AppointmentsPage: React.FC = () => {
-

Appointment Booking

+

{t('appointments.upgrade_title')}

- Upgrade to Starter to enable appointment booking for your chatbots. + {t('appointments.upgrade_desc')}

@@ -200,8 +202,8 @@ export const AppointmentsPage: React.FC = () => {
-

Appointments

-

Bookings made through your chatbots

+

{t('appointments.title')}

+

{t('appointments.subtitle')}

@@ -224,17 +226,15 @@ export const AppointmentsPage: React.FC = () => {
-

Enable booking on a chatbot

-

- Go to a chatbot's Deploy tab and enable "Appointment Booking" to start accepting bookings. -

+

{t('appointments.enable_booking_title')}

+

{t('appointments.enable_booking_desc')}

{chatbots.length > 0 && ( )}
@@ -246,10 +246,10 @@ export const AppointmentsPage: React.FC = () => { {appointments.length > 0 && (
{[ - { label: 'Today', count: today.length, color: 'text-blue-600', bg: 'bg-blue-50' }, - { label: 'Upcoming', count: upcoming.length, color: 'text-primary-600', bg: 'bg-primary-50' }, - { label: 'Confirmed', count: appointments.filter(a => a.status === 'confirmed').length, color: 'text-green-600', bg: 'bg-green-50' }, - { label: 'Pending', count: appointments.filter(a => a.status === 'pending').length, color: 'text-yellow-600', bg: 'bg-yellow-50' }, + { label: t('appointments.stat_today'), count: today.length, color: 'text-blue-600', bg: 'bg-blue-50' }, + { label: t('appointments.stat_upcoming'), count: upcoming.length, color: 'text-primary-600', bg: 'bg-primary-50' }, + { label: t('appointments.stat_confirmed'), count: appointments.filter(a => a.status === 'confirmed').length, color: 'text-green-600', bg: 'bg-green-50' }, + { label: t('appointments.stat_pending'), count: appointments.filter(a => a.status === 'pending').length, color: 'text-yellow-600', bg: 'bg-yellow-50' }, ].map(stat => (
@@ -269,14 +269,14 @@ export const AppointmentsPage: React.FC = () => {
- Filter + {t('appointments.filter')}
{/* Per-chatbot hours settings */} {bookingEnabledChatbots.length > 0 && (
- Hours: + {t('appointments.hours_label')} @@ -314,9 +314,9 @@ export const AppointmentsPage: React.FC = () => {
-

No appointments yet

+

{t('appointments.no_appointments_title')}

- Once customers book through your chatbot, appointments will appear here. + {t('appointments.no_appointments_desc')}

) : ( @@ -337,7 +337,7 @@ export const AppointmentsPage: React.FC = () => { {slotDate.toLocaleDateString(undefined, { month: 'short' })}

{slotDate.getDate()}

- {isToday &&

Today

} + {isToday &&

{t('appointments.today_label')}

}
{/* Details */} @@ -376,14 +376,14 @@ export const AppointmentsPage: React.FC = () => { disabled={updateStatus.isPending} className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-green-700 bg-green-50 hover:bg-green-100 border border-green-200 rounded-lg transition-colors disabled:opacity-50" > - Confirm + {t('appointments.confirm_btn')}
)} @@ -394,14 +394,14 @@ export const AppointmentsPage: React.FC = () => { disabled={updateStatus.isPending} className="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-gray-700 bg-gray-100 hover:bg-gray-200 border border-gray-200 rounded-lg transition-colors disabled:opacity-50" > - Mark Complete + {t('appointments.mark_complete')}
)} @@ -411,7 +411,7 @@ export const AppointmentsPage: React.FC = () => { disabled={updateStatus.isPending} className="flex items-center gap-1.5 px-3 py-1.5 mt-3 text-xs font-medium text-gray-600 bg-gray-100 hover:bg-gray-200 border border-gray-200 rounded-lg transition-colors disabled:opacity-50" > - Restore + {t('appointments.restore_btn')} )}
diff --git a/src/pages/AuthPages.tsx b/src/pages/AuthPages.tsx index a17b4f2..fc100a7 100644 --- a/src/pages/AuthPages.tsx +++ b/src/pages/AuthPages.tsx @@ -1,56 +1,52 @@ import React, { useState } from 'react' import { Link, useNavigate } from 'react-router-dom' +import { useTranslation } from 'react-i18next' import { useAuthStore } from '@/store/authStore' import { authAPI } from '@/services/api' -import { Button, Input } from '@/components/ui' +import { Button } from '@/components/ui' import { Sparkles, Eye, EyeOff, Mail, Lock, Building2, Check, X, MessageSquare, FileText, Globe } from 'lucide-react' // ─── Shared branding panel ──────────────────────────────────────────────────── -const BrandingPanel: React.FC = () => ( -
- {/* decorative circles */} -
-
-
+const BrandingPanel: React.FC = () => { + const { t } = useTranslation() + return ( +
+
+
+
- {/* Logo */} -
-
- + +
+ +
+ Contexta + + +
+

+ {t('auth.branding_headline')} +

+

{t('auth.branding_subtext')}

+
    + {[ + { icon: MessageSquare, key: 'auth.branding_feature_1' }, + { icon: FileText, key: 'auth.branding_feature_2' }, + { icon: Globe, key: 'auth.branding_feature_3' }, + ].map(({ key }) => ( +
  • + + + + {t(key)} +
  • + ))} +
- Contexta -
- {/* Center content */} -
-

- Build AI chatbots
that actually work. -

-

- Upload your docs, train your bot, and publish it anywhere — in minutes. -

-
    - {[ - { icon: MessageSquare, text: 'Custom chatbots trained on your content' }, - { icon: FileText, text: 'PDF, DOCX, CSV, and URL sources' }, - { icon: Globe, text: 'Embed on any website or channel' }, - ].map(({ icon: Icon, text }) => ( -
  • - - - - {text} -
  • - ))} -
+

{t('auth.branding_footer')}

- - {/* Footer quote */} -

- Trusted by businesses building smarter customer experiences. -

-
-) + ) +} // ─── Shared page wrapper ────────────────────────────────────────────────────── const AuthLayout: React.FC<{ children: React.ReactNode }> = ({ children }) => ( @@ -115,6 +111,7 @@ const IconInput: React.FC<{ // ─── LoginPage ───────────────────────────────────────────────────────────────── export const LoginPage: React.FC = () => { + const { t } = useTranslation() const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [showPass, setShowPass] = useState(false) @@ -133,7 +130,7 @@ export const LoginPage: React.FC = () => { navigate('/dashboard') } catch (err) { const e = err as { response?: { data?: { detail?: string } } } - setError(e.response?.data?.detail || 'Login failed. Please check your credentials.') + setError(e.response?.data?.detail || t('auth.login_failed')) } finally { setLoading(false) } @@ -141,22 +138,21 @@ export const LoginPage: React.FC = () => { return ( - {/* Mobile logo */} -
+
Contexta -
+
-

Welcome back

-

Sign in to your Contexta account

+

{t('auth.login_title')}

+

{t('auth.login_subtitle')}

} type="email" value={email} @@ -166,7 +162,7 @@ export const LoginPage: React.FC = () => { /> } type={showPass ? 'text' : 'password'} value={password} @@ -192,25 +188,25 @@ export const LoginPage: React.FC = () => { className="w-full mt-2 bg-primary-600 hover:bg-primary-700 shadow-sm hover:shadow-md transition-all duration-200" size="lg" > - Sign in + {t('auth.sign_in')}
- No account?{' '} + {t('auth.no_account')}{' '} - Sign up free + {t('auth.sign_up_free')} - Forgot password? + {t('auth.forgot_password')}
@@ -219,6 +215,7 @@ export const LoginPage: React.FC = () => { // ─── SignupPage ──────────────────────────────────────────────────────────────── export const SignupPage: React.FC = () => { + const { t } = useTranslation() const [form, setForm] = useState({ email: '', password: '', company_name: '' }) const [showPass, setShowPass] = useState(false) const [error, setError] = useState('') @@ -231,23 +228,21 @@ export const SignupPage: React.FC = () => { e.preventDefault() setError('') if (form.password.length < 8) { - setError('Password must be at least 8 characters') + setError(t('auth.password_min_8')) return } setLoading(true) try { const data = await authAPI.signup(form) if (data.access_token) { - // Email confirmation not required — go straight to dashboard setAuth(data.user, data.access_token) navigate('/dashboard') } else { - // Supabase requires email confirmation setEmailSent(true) } } catch (err) { const e = err as { response?: { data?: { detail?: string } } } - setError(e.response?.data?.detail || 'Signup failed. Please try again.') + setError(e.response?.data?.detail || t('auth.signup_failed')) } finally { setLoading(false) } @@ -260,14 +255,12 @@ export const SignupPage: React.FC = () => {
-

Check your inbox

-

- A confirmation link was sent to -

+

{t('auth.check_inbox_title')}

+

{t('auth.check_inbox_desc')}

{form.email}

- Already confirmed?{' '} - Sign in + {t('auth.already_confirmed')}{' '} + {t('auth.sign_in')}

@@ -276,22 +269,21 @@ export const SignupPage: React.FC = () => { return ( - {/* Mobile logo */} -
+
Contexta -
+
-

Create your account

-

Start building AI chatbots — free forever

+

{t('auth.signup_title')}

+

{t('auth.signup_subtitle')}

} type="text" value={form.company_name} @@ -301,7 +293,7 @@ export const SignupPage: React.FC = () => { /> } type="email" value={form.email} @@ -311,7 +303,7 @@ export const SignupPage: React.FC = () => { /> } type={showPass ? 'text' : 'password'} value={form.password} @@ -337,24 +329,24 @@ export const SignupPage: React.FC = () => { className="w-full mt-2 bg-primary-600 hover:bg-primary-700 shadow-sm hover:shadow-md transition-all duration-200" size="lg" > - Create free account + {t('auth.create_free_account')}

- By signing up you agree to our{' '} - Terms of Service{' '} - and{' '} - Privacy Policy + {t('auth.terms_text')}{' '} + {t('auth.terms_of_service')}{' '} + {t('auth.and')}{' '} + {t('auth.privacy_policy')}

- Already have an account?{' '} + {t('auth.already_account')}{' '} - Sign in + {t('auth.sign_in')}
diff --git a/src/pages/CampaignsPage.tsx b/src/pages/CampaignsPage.tsx index 62a8a6f..28b601f 100644 --- a/src/pages/CampaignsPage.tsx +++ b/src/pages/CampaignsPage.tsx @@ -1,30 +1,23 @@ import React, { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' +import { useTranslation } from 'react-i18next' import { campaignsAPI, chatbotsAPI } from '@/services/api' import { Card, Button, Spinner } from '@/components/ui' import { Megaphone, Send, Trash2, Lock, Users, CheckCircle2, Clock, - AlertCircle, Plus, X, ChevronDown, + AlertCircle, Plus, X, } from 'lucide-react' import type { Campaign, Chatbot } from '@/types' import { cn } from '@/lib/utils' -const STATUS_CONFIG: Record = { - draft: { label: 'Draft', color: 'bg-gray-100 text-gray-600', icon: Clock }, - sending: { label: 'Sending...', color: 'bg-blue-100 text-blue-700', icon: Clock }, - sent: { label: 'Sent', color: 'bg-green-100 text-green-700', icon: CheckCircle2 }, - failed: { label: 'Failed', color: 'bg-red-100 text-red-600', icon: AlertCircle }, -} - -function timeAgo(dateStr?: string): string { - if (!dateStr) return '' - const diff = Date.now() - new Date(dateStr).getTime() - const mins = Math.floor(diff / 60000) - if (mins < 1) return 'just now' - if (mins < 60) return `${mins}m ago` - const hrs = Math.floor(mins / 60) - if (hrs < 24) return `${hrs}h ago` - return new Date(dateStr).toLocaleDateString() +function useStatusConfig() { + const { t } = useTranslation() + return { + draft: { label: t('campaigns.status_draft'), color: 'bg-gray-100 text-gray-600', icon: Clock }, + sending: { label: t('campaigns.status_sending'), color: 'bg-blue-100 text-blue-700', icon: Clock }, + sent: { label: t('campaigns.status_sent'), color: 'bg-green-100 text-green-700', icon: CheckCircle2 }, + failed: { label: t('campaigns.status_failed'), color: 'bg-red-100 text-red-600', icon: AlertCircle }, + } as Record } // ── New Campaign Form ───────────────────────────────────────────────────────── @@ -35,12 +28,11 @@ const NewCampaignForm: React.FC<{ onCreate: (data: { chatbot_id: string; title: string; message: string }) => void creating: boolean }> = ({ chatbots, onClose, onCreate, creating }) => { + const { t } = useTranslation() const [chatbotId, setChatbotId] = useState(chatbots[0]?.id || '') const [title, setTitle] = useState('') const [message, setMessage] = useState('') - const telegramChatbots = chatbots // All chatbots can have Telegram connected - const canSubmit = chatbotId && title.trim() && message.trim() return ( @@ -48,7 +40,7 @@ const NewCampaignForm: React.FC<{

- New Campaign + {t('campaigns.new_campaign')}