-
AI chat isn't configured
+
+ {t('chat.ai_not_configured')}
+
- Set OPENAI_API_KEY and/or{' '}
- ANTHROPIC_API_KEY on the
- API service to enable AI chat, then restart it. The model picker
- shows only providers with a configured key.
+ ,
+ anthropic: ,
+ }}
+ i18nKey="chat.ai_not_configured_setup"
+ />
@@ -67,7 +81,7 @@ export function ChatDrawerNotConfigured() {
rel="noopener"
className="inline-flex items-center gap-1.5 text-sm text-muted-foreground hover:text-foreground"
>
- View setup docs
+ {t('chat.view_setup_docs')}
@@ -75,9 +89,9 @@ export function ChatDrawerNotConfigured() {
}
type Suggestions = {
- headline: string;
- description: string;
- prompts: string[];
+ headlineKey: string;
+ descriptionKey: string;
+ promptKeys: string[];
};
function getSuggestionsForContext(
@@ -85,13 +99,12 @@ function getSuggestionsForContext(
): Suggestions {
if (!ctx) {
return {
- headline: 'Ask about your data',
- description:
- 'I can answer questions about your analytics, generate reports, and dig into users, sessions, or pages.',
- prompts: [
- 'What happened yesterday?',
- 'Show me last 7 days of traffic',
- 'Which pages have the highest bounce rate?',
+ headlineKey: 'chat.empty_no_context_headline',
+ descriptionKey: 'chat.empty_no_context_description',
+ promptKeys: [
+ 'chat.empty_no_context_prompt_what_happened_yesterday',
+ 'chat.empty_no_context_prompt_last_seven_days_traffic',
+ 'chat.empty_no_context_prompt_highest_bounce_rate',
],
};
}
@@ -99,142 +112,131 @@ function getSuggestionsForContext(
switch (ctx.page) {
case 'overview':
return {
- headline: 'Ask about this overview',
- description:
- 'I can explain trends, compare periods, or filter the page. Try a question or pick a starter.',
- prompts: [
- 'What changed compared to the previous period?',
- 'Filter to mobile only',
- 'Show me last 7 days of traffic',
- 'Which referrers drove the most sessions?',
+ headlineKey: 'chat.empty_overview_headline',
+ descriptionKey: 'chat.empty_overview_description',
+ promptKeys: [
+ 'chat.empty_overview_prompt_compare_previous_period',
+ 'chat.empty_overview_prompt_filter_mobile_only',
+ 'chat.empty_overview_prompt_last_seven_days_traffic',
+ 'chat.empty_overview_prompt_top_referrers',
],
};
case 'insights':
return {
- headline: 'Explore your insights',
- description:
- 'I can explain why an insight fired, find related ones, or walk you through the most important.',
- prompts: [
- 'What are the most important insights right now?',
- 'Explain the biggest anomaly this week',
- 'Any insights related to device trends?',
+ headlineKey: 'chat.empty_insights_headline',
+ descriptionKey: 'chat.empty_insights_description',
+ promptKeys: [
+ 'chat.empty_insights_prompt_most_important',
+ 'chat.empty_insights_prompt_biggest_anomaly',
+ 'chat.empty_insights_prompt_device_trends',
],
};
case 'pages':
return {
- headline: 'Ask about your pages',
- description:
- 'I can rank pages by performance, find declining ones, or identify entry/exit patterns.',
- prompts: [
- 'Which pages are declining vs last month?',
- 'Show pages with the highest bounce rate',
- 'Top entry pages in the last 7 days',
- 'Find pages with underperforming SEO',
+ headlineKey: 'chat.empty_pages_headline',
+ descriptionKey: 'chat.empty_pages_description',
+ promptKeys: [
+ 'chat.empty_pages_prompt_declining_pages',
+ 'chat.empty_pages_prompt_highest_bounce_rate',
+ 'chat.empty_pages_prompt_top_entry_pages',
+ 'chat.empty_pages_prompt_underperforming_seo',
],
};
case 'seo':
return {
- headline: 'Dig into your SEO',
- description:
- 'I can surface high-opportunity queries, check for cannibalization, and correlate SEO with on-site engagement.',
- prompts: [
- 'Queries on page 2 with high impressions (easy wins)',
- 'Any query cannibalization I should fix?',
- 'Which pages bring GSC clicks but bounce hard?',
- 'Top SEO queries last 30 days',
+ headlineKey: 'chat.empty_seo_headline',
+ descriptionKey: 'chat.empty_seo_description',
+ promptKeys: [
+ 'chat.empty_seo_prompt_page_two_queries',
+ 'chat.empty_seo_prompt_query_cannibalization',
+ 'chat.empty_seo_prompt_gsc_clicks_bounce_hard',
+ 'chat.empty_seo_prompt_top_seo_queries',
],
};
case 'events':
return {
- headline: 'Analyze your events',
- description:
- 'I can analyze distribution, correlate events, and drill into properties.',
- prompts: [
- 'Which events often happen together?',
- 'Analyze event distribution by country',
- 'Filter to signup events only',
- 'What are the most common event properties?',
+ headlineKey: 'chat.empty_events_headline',
+ descriptionKey: 'chat.empty_events_description',
+ promptKeys: [
+ 'chat.empty_events_prompt_events_together',
+ 'chat.empty_events_prompt_distribution_by_country',
+ 'chat.empty_events_prompt_signup_events_only',
+ 'chat.empty_events_prompt_common_properties',
],
};
case 'profileDetail':
return {
- headline: 'Ask about this user',
- description:
- 'I can summarize this profile, build a journey, or compare them to the average user.',
- prompts: [
- "Tell me about this user's journey",
- 'How does this user compare to average?',
- 'What was their last session?',
- 'Do they have unusual behavior?',
+ headlineKey: 'chat.empty_profile_detail_headline',
+ descriptionKey: 'chat.empty_profile_detail_description',
+ promptKeys: [
+ 'chat.empty_profile_detail_prompt_user_journey',
+ 'chat.empty_profile_detail_prompt_compare_average',
+ 'chat.empty_profile_detail_prompt_last_session',
+ 'chat.empty_profile_detail_prompt_unusual_behavior',
],
};
case 'sessionDetail':
return {
- headline: 'Ask about this session',
- description:
- 'I can walk through the path, compare to typical sessions, or explain the referrer context.',
- prompts: [
- 'Walk me through this session',
- 'How does this compare to typical sessions?',
- 'Where did this traffic come from?',
- 'Are there similar sessions today?',
+ headlineKey: 'chat.empty_session_detail_headline',
+ descriptionKey: 'chat.empty_session_detail_description',
+ promptKeys: [
+ 'chat.empty_session_detail_prompt_walk_through',
+ 'chat.empty_session_detail_prompt_compare_typical',
+ 'chat.empty_session_detail_prompt_traffic_source',
+ 'chat.empty_session_detail_prompt_similar_sessions',
],
};
case 'groupDetail':
return {
- headline: 'Ask about this group',
- description:
- 'I can show metrics, list members, and compare to peer groups.',
- prompts: [
- "Summarize this group's activity",
- 'Compare to other groups',
- 'Who are the most active members?',
- "What's this group's engagement trend?",
+ headlineKey: 'chat.empty_group_detail_headline',
+ descriptionKey: 'chat.empty_group_detail_description',
+ promptKeys: [
+ 'chat.empty_group_detail_prompt_summarize_activity',
+ 'chat.empty_group_detail_prompt_compare_groups',
+ 'chat.empty_group_detail_prompt_active_members',
+ 'chat.empty_group_detail_prompt_engagement_trend',
],
};
case 'reportEditor':
return {
- headline: 'Edit this report with me',
- description:
- 'I can preview changes, suggest breakdowns, or compare to the previous period.',
- prompts: [
- 'Suggest useful breakdowns for this report',
- 'Compare to the previous period',
- 'Find anomalies in the current data',
- 'Add a country breakdown',
+ headlineKey: 'chat.empty_report_editor_headline',
+ descriptionKey: 'chat.empty_report_editor_description',
+ promptKeys: [
+ 'chat.empty_report_editor_prompt_useful_breakdowns',
+ 'chat.empty_report_editor_prompt_compare_previous_period',
+ 'chat.empty_report_editor_prompt_find_anomalies',
+ 'chat.empty_report_editor_prompt_country_breakdown',
],
};
case 'dashboard':
return {
- headline: 'Ask about this dashboard',
- description:
- 'I can summarize every report on this dashboard at once, flag what changed, or zoom into a single chart.',
- prompts: [
- 'Summarize this dashboard',
- "What's underperforming here?",
- 'Compare this dashboard to the previous period',
- 'Which report has the biggest change?',
+ headlineKey: 'chat.empty_dashboard_headline',
+ descriptionKey: 'chat.empty_dashboard_description',
+ promptKeys: [
+ 'chat.empty_dashboard_prompt_summarize_dashboard',
+ 'chat.empty_dashboard_prompt_underperforming',
+ 'chat.empty_dashboard_prompt_compare_previous_period',
+ 'chat.empty_dashboard_prompt_biggest_change',
],
};
default:
return {
- headline: 'Ask about your data',
- description:
- 'I can answer questions about the page you\'re viewing, generate reports, and dig into specific users, sessions, or pages.',
- prompts: [
- "What's our visitor count this week?",
- 'Top traffic sources right now',
- 'Show me top pages by bounce rate',
+ headlineKey: 'chat.empty_default_context_headline',
+ descriptionKey: 'chat.empty_default_context_description',
+ promptKeys: [
+ 'chat.empty_default_context_prompt_visitor_count',
+ 'chat.empty_default_context_prompt_top_traffic_sources',
+ 'chat.empty_default_context_prompt_top_pages_bounce_rate',
],
};
}
diff --git a/apps/start/src/components/chat/chat-drawer-footer.tsx b/apps/start/src/components/chat/chat-drawer-footer.tsx
index 0c59260b4..707b3ad14 100644
--- a/apps/start/src/components/chat/chat-drawer-footer.tsx
+++ b/apps/start/src/components/chat/chat-drawer-footer.tsx
@@ -2,6 +2,7 @@ import { Button } from '@/components/ui/button';
import { cn } from '@/utils/cn';
import { ArrowUpIcon, StopCircleIcon } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
+import { useTranslation } from 'react-i18next';
import { useChatRuntime } from './chat-runtime';
import { ModelPicker } from './model-picker';
@@ -13,6 +14,7 @@ import { ModelPicker } from './model-picker';
* input state — `useChat` v5+ doesn't manage input.
*/
export function ChatDrawerFooter() {
+ const { t } = useTranslation();
const { send, stop, status } = useChatRuntime();
const [text, setText] = useState('');
const isStreaming = status === 'streaming' || status === 'submitted';
@@ -61,7 +63,7 @@ export function ChatDrawerFooter() {
ref={textareaRef}
value={text}
onChange={(e) => setText(e.target.value)}
- placeholder="Ask anything about your data…"
+ placeholder={t('chat.ask_anything_placeholder')}
rows={2}
className={cn(
'block w-full bg-transparent text-sm leading-[1.5] text-foreground',
@@ -86,7 +88,7 @@ export function ChatDrawerFooter() {
variant="secondary"
className="size-7 rounded-md shrink-0"
onClick={stop}
- aria-label="Stop generating"
+ aria-label={t('chat.stop_generating')}
>
@@ -97,7 +99,7 @@ export function ChatDrawerFooter() {
variant="default"
className="size-7 rounded-md shrink-0"
disabled={!text.trim()}
- aria-label="Send message"
+ aria-label={t('chat.send_message')}
>
diff --git a/apps/start/src/components/chat/chat-drawer-header.tsx b/apps/start/src/components/chat/chat-drawer-header.tsx
index 73cbeeb26..9513375b5 100644
--- a/apps/start/src/components/chat/chat-drawer-header.tsx
+++ b/apps/start/src/components/chat/chat-drawer-header.tsx
@@ -11,6 +11,7 @@ import { useTRPC } from '@/integrations/trpc/react';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { MessageSquarePlusIcon, Trash2Icon, XIcon } from 'lucide-react';
import { useEffect, useRef, useState } from 'react';
+import { useTranslation } from 'react-i18next';
import { useChatState } from './chat-context';
/**
@@ -28,6 +29,7 @@ export function ChatDrawerHeader({
projectId: string;
onClose: () => void;
}) {
+ const { t } = useTranslation();
const trpc = useTRPC();
const queryClient = useQueryClient();
const { conversationId, switchConversation, newConversation, streamingTitle } =
@@ -55,7 +57,7 @@ export function ChatDrawerHeader({
// 3. "New chat" fallback when nothing is known yet
const persistedTitle =
conversations?.find((c) => c.id === conversationId)?.title ?? null;
- const activeTitle = streamingTitle ?? persistedTitle ?? 'New chat';
+ const activeTitle = streamingTitle ?? persistedTitle ?? t('chat.new_chat');
return (
);
}
@@ -127,7 +131,7 @@ export function ChatMessage({
input: part.args ? safeParse(part.args) : undefined,
output: matched?.result,
errorText:
- matched?.status === 'error' ? 'Tool failed' : undefined,
+ matched?.status === 'error' ? t('chat.tool_failed') : undefined,
};
return (