fix: comprehensive i18n — replace hardcoded French/English strings with t() calls
Some checks failed
Deploy to Production / Build and Deploy (push) Failing after 1m7s

Replaced ~100+ hardcoded French and English text strings across 30+ components
with proper i18n t() calls. Added 57 new translation keys to all 15 locale files
(ar, de, en, es, fa, fr, hi, it, ja, ko, nl, pl, pt, ru, zh).

Key changes:
- contextual-ai-chat.tsx: 30 French strings → t() (actions, toasts, labels, placeholders)
- ai-chat.tsx: 15 French/English strings → t() (header, tabs, welcome, insights, history)
- note-inline-editor.tsx: 20 French fallbacks removed (toolbar, save status, checklist)
- lab-skeleton.tsx: French loading text → t()
- admin-header.tsx, header.tsx, editor-connections-section.tsx: French fallbacks removed
- New AI chat component, agent cards, sidebar, settings panel i18n cleanup

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 21:14:45 +02:00
parent e358171c45
commit 153c921960
60 changed files with 4125 additions and 1677 deletions

View File

@@ -6,7 +6,7 @@
*/
import { useState, useCallback, useMemo, useEffect, useRef } from 'react'
import { Plus, Bot, LifeBuoy, Search } from 'lucide-react'
import { Plus, Bot, LayoutTemplate, Search, HelpCircle } from 'lucide-react'
import { toast } from 'sonner'
import { useLanguage } from '@/lib/i18n'
@@ -79,6 +79,7 @@ export function AgentsPageClient({
const [showHelp, setShowHelp] = useState(false)
const [searchQuery, setSearchQuery] = useState('')
const [typeFilter, setTypeFilter] = useState('')
const [activeTab, setActiveTab] = useState<'dashboard' | 'templates'>('dashboard')
const refreshAgents = useCallback(async () => {
try {
@@ -90,20 +91,15 @@ export function AgentsPageClient({
}
}, [])
// Track latest action ID per agent to detect new executions
const prevActionsRef = useRef<Record<string, string | null>>({})
// Check for new actions and show toast notifications
const checkForNewActions = useCallback((updated: AgentItem[]) => {
for (const agent of updated) {
const lastAction = agent.actions[0]
if (!lastAction) continue
const prevId = prevActionsRef.current[agent.id]
if (prevId === undefined) continue // Not tracked
if (prevId === undefined) continue
if (prevId !== lastAction.id) {
// Only toast for recently created actions (within 5 min)
const age = Date.now() - new Date(lastAction.createdAt).getTime()
if (age < 5 * 60 * 1000) {
if (lastAction.status === 'success') {
@@ -118,18 +114,13 @@ export function AgentsPageClient({
}, [t])
useEffect(() => {
// Initialize tracking from initial data
for (const agent of initialAgents) {
prevActionsRef.current[agent.id] = agent.actions[0]?.id ?? null
}
// Interval polling every 30s
const interval = setInterval(async () => {
const updated = await refreshAgents()
if (updated) checkForNewActions(updated)
}, 30000)
// Refresh immediately when user comes back to the tab
const onVisible = async () => {
if (document.visibilityState === 'visible') {
const updated = await refreshAgents()
@@ -137,7 +128,6 @@ export function AgentsPageClient({
}
}
document.addEventListener('visibilitychange', onVisible)
return () => {
clearInterval(interval)
document.removeEventListener('visibilitychange', onVisible)
@@ -179,7 +169,6 @@ export function AgentsPageClient({
scheduledDay: formData.get('scheduledDay') ? Number(formData.get('scheduledDay')) : undefined,
timezone: (formData.get('timezone') as string) || undefined,
}
if (editingAgent) {
await updateAgent(editingAgent.id, data)
toast.success(t('agents.toasts.updated'))
@@ -187,7 +176,6 @@ export function AgentsPageClient({
await createAgent(data)
toast.success(t('agents.toasts.created'))
}
setShowForm(false)
setEditingAgent(null)
await refreshAgents()
@@ -208,110 +196,142 @@ export function AgentsPageClient({
const existingAgentNames = useMemo(() => agents.map(a => a.name), [agents])
return (
<>
{/* Header */}
<div className="flex items-center justify-between gap-4 mb-8">
<div className="flex items-center gap-4">
<div className="p-3 bg-primary/10 rounded-2xl shadow-sm border border-primary/20">
<Bot className="h-8 w-8 text-primary" />
</div>
<div>
<h1 className="text-3xl font-bold tracking-tight">{t('agents.title')}</h1>
<p className="text-muted-foreground text-sm">{t('agents.subtitle')}</p>
/* Full-bleed layout: -m-4 cancels the p-4 of the parent <main> */
<div className="flex -m-4 h-[calc(100vh-4rem)] overflow-hidden">
{/* ── LEFT SIDEBAR ── */}
<aside className="w-60 flex-shrink-0 flex flex-col bg-muted/30 border-r border-border/40 h-full font-display">
{/* Brand */}
<div className="flex items-center gap-3 px-5 py-5 border-b border-border/40">
<div className="w-8 h-8 rounded-lg bg-primary flex items-center justify-center flex-shrink-0">
<Bot className="w-4 h-4 text-primary-foreground" />
</div>
<span className="font-bold text-base tracking-tight">{t('agents.title')}</span>
</div>
</div>
{/* Action buttons */}
<div className="flex items-center justify-between mb-6">
<button
onClick={() => setShowHelp(true)}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-primary bg-primary/10 border border-primary/20 rounded-lg hover:bg-primary/15 transition-colors"
>
<LifeBuoy className="w-4 h-4" />
{t('agents.help.btnLabel')}
</button>
<button
onClick={handleCreate}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-white bg-primary rounded-lg hover:bg-primary/90 transition-colors"
>
<Plus className="w-4 h-4" />
{t('agents.newAgent')}
</button>
</div>
{/* Agents grid */}
{agents.length > 0 && (
<div className="mb-10">
<h3 className="text-sm font-semibold text-muted-foreground uppercase tracking-wider mb-3">
{/* Nav */}
<nav className="flex-1 p-3 space-y-0.5">
<button
onClick={() => setActiveTab('dashboard')}
className={`w-full flex items-center gap-2.5 px-3 py-2 text-sm font-medium rounded-lg transition-all ${
activeTab === 'dashboard'
? 'bg-primary/10 text-primary border-r-2 border-primary'
: 'text-muted-foreground hover:bg-background/70 hover:text-foreground'
}`}
>
<Bot className="w-4 h-4" />
{t('agents.myAgents')}
</h3>
</button>
</nav>
{/* Search and filter */}
<div className="flex flex-col sm:flex-row items-start sm:items-center gap-3 mb-4">
<div className="relative flex-1 w-full sm:max-w-xs">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder={t('agents.searchPlaceholder')}
className="w-full pl-9 pr-3 py-2 text-sm bg-card border border-border rounded-lg outline-none focus:border-primary/40 focus:ring-2 focus:ring-primary/10 transition-all"
/>
</div>
<div className="flex items-center gap-1.5 flex-wrap">
{typeFilterOptions.map(opt => (
<button
key={opt.value}
onClick={() => setTypeFilter(opt.value)}
className={`px-3 py-1.5 text-xs font-medium rounded-full transition-colors ${
typeFilter === opt.value
? 'bg-primary text-white'
: 'bg-muted text-muted-foreground hover:bg-accent'
}`}
>
{t(opt.labelKey)}
</button>
))}
</div>
{/* Footer: Help */}
<div className="p-3 border-t border-border/40">
<button
onClick={() => setShowHelp(true)}
className="w-full flex items-center gap-2.5 px-3 py-2 text-sm font-medium text-muted-foreground hover:bg-background/70 hover:text-foreground rounded-lg transition-all"
>
<HelpCircle className="w-4 h-4" />
{t('agents.help.btnLabel')}
</button>
</div>
</aside>
{/* ── MAIN CONTENT ── */}
<div className="flex-1 flex flex-col min-w-0 bg-background overflow-hidden">
{/* Top header bar */}
<header className="flex items-center justify-between px-8 py-4 border-b border-border/40 bg-background flex-shrink-0 font-display">
<div>
<h1 className="text-xl font-bold tracking-tight">
{t('agents.myAgents')}
</h1>
<p className="text-xs text-muted-foreground mt-0.5">
{t('agents.subtitle')}
</p>
</div>
<button
onClick={handleCreate}
className="flex items-center gap-2 px-4 py-2 text-sm font-semibold text-primary-foreground bg-primary hover:bg-primary/90 rounded-lg shadow-sm hover:shadow-md hover:shadow-primary/20 transition-all"
>
<Plus className="w-4 h-4" />
{t('agents.newAgent')}
</button>
</header>
{filteredAgents.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{filteredAgents.map(agent => (
<AgentCard
key={agent.id}
agent={agent}
onEdit={handleEdit}
onRefresh={refreshAgents}
onToggle={handleToggle}
/>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center py-12 text-center">
<Search className="w-10 h-10 text-muted-foreground/30 mb-3" />
<p className="text-sm text-muted-foreground">{t('agents.noResults')}</p>
</div>
{/* Scrollable content area */}
<main className="flex-1 overflow-y-auto p-8">
{/* Dashboard tab - agents + templates */}
{activeTab === 'dashboard' && (
<>
{agents.length > 0 && (
<>
{/* Filter pills + search */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3 mb-6">
<div className="flex items-center gap-1 bg-muted/40 p-1 rounded-lg border border-border/40">
{typeFilterOptions.map(opt => (
<button
key={opt.value}
onClick={() => setTypeFilter(opt.value)}
className={`px-3 py-1.5 text-[12px] font-semibold rounded-md transition-all ${
typeFilter === opt.value
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground hover:text-foreground'
}`}
>
{t(opt.labelKey)}
</button>
))}
</div>
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-3.5 h-3.5 text-muted-foreground/60" />
<input
type="text"
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder={t('agents.searchPlaceholder')}
className="pl-9 pr-4 py-2 text-[13px] bg-card border border-border/50 rounded-lg outline-none focus:border-primary/50 focus:ring-2 focus:ring-primary/10 transition-all placeholder:text-muted-foreground/40 w-56"
/>
</div>
</div>
{filteredAgents.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 2xl:grid-cols-4 gap-3 mb-10">
{filteredAgents.map(agent => (
<AgentCard
key={agent.id}
agent={agent}
onEdit={handleEdit}
onRefresh={refreshAgents}
onToggle={handleToggle}
/>
))}
</div>
) : (
<div className="flex flex-col items-center justify-center py-12 text-center mb-10">
<Search className="w-10 h-10 text-muted-foreground/20 mb-3" />
<p className="text-sm text-muted-foreground">{t('agents.noResults')}</p>
</div>
)}
</>
)}
{agents.length === 0 && (
<div className="flex flex-col items-center justify-center py-16 text-center bg-card rounded-xl border border-border/40 shadow-sm mb-10">
<Bot className="w-12 h-12 text-muted-foreground/20 mb-4" />
<h3 className="text-base font-semibold text-foreground mb-2">{t('agents.noAgents')}</h3>
<p className="text-sm text-muted-foreground max-w-sm mb-2">{t('agents.noAgentsDescription')}</p>
</div>
)}
{/* Templates always visible on dashboard */}
<AgentTemplates onInstalled={refreshAgents} existingAgentNames={existingAgentNames} />
</>
)}
</div>
)}
</main>
</div>
{/* Empty state */}
{agents.length === 0 && (
<div className="flex flex-col items-center justify-center py-16 text-center mb-10">
<Bot className="w-16 h-16 text-muted-foreground/30 mb-4" />
<h3 className="text-lg font-medium text-muted-foreground mb-2">{t('agents.noAgents')}</h3>
<p className="text-sm text-muted-foreground max-w-sm">
{t('agents.noAgentsDescription')}
</p>
</div>
)}
{/* Templates */}
<AgentTemplates onInstalled={refreshAgents} existingAgentNames={existingAgentNames} />
{/* Form modal */}
{/* Sliding panels */}
{showForm && (
<AgentForm
agent={editingAgent}
@@ -320,8 +340,6 @@ export function AgentsPageClient({
onCancel={() => { setShowForm(false); setEditingAgent(null) }}
/>
)}
{/* Run log modal */}
{logAgent && (
<AgentRunLog
agentId={logAgent.id}
@@ -329,11 +347,9 @@ export function AgentsPageClient({
onClose={() => setLogAgent(null)}
/>
)}
{/* Help modal */}
{showHelp && (
<AgentHelp onClose={() => setShowHelp(false)} />
)}
</>
</div>
)
}

View File

@@ -19,15 +19,9 @@ export default async function AgentsPage() {
])
return (
<div className="flex-1 flex flex-col h-full bg-background">
<div className="flex-1 p-8 overflow-y-auto">
<div className="max-w-6xl mx-auto">
<AgentsPageClient
agents={agents}
notebooks={notebooks}
/>
</div>
</div>
</div>
<AgentsPageClient
agents={agents}
notebooks={notebooks}
/>
)
}

View File

@@ -5,6 +5,9 @@ import { ProvidersWrapper } from "@/components/providers-wrapper";
import { auth } from "@/auth";
import { detectUserLanguage } from "@/lib/i18n/detect-user-language";
import { loadTranslations } from "@/lib/i18n/load-translations";
import { getAISettings } from "@/app/actions/ai-settings";
import { AIChat } from "@/components/ai-chat";
export default async function MainLayout({
children,
@@ -20,6 +23,12 @@ export default async function MainLayout({
// Load initial translations server-side to prevent hydration mismatch
const initialTranslations = await loadTranslations(initialLanguage);
// Load AI settings to conditionally render AI features
const aiSettings = session?.user?.id
? await getAISettings(session.user.id)
: null;
const showAIAssistant = aiSettings?.paragraphRefactor !== false;
return (
<ProvidersWrapper initialLanguage={initialLanguage} initialTranslations={initialTranslations}>
<div className="bg-background-light dark:bg-background-dark font-display text-slate-900 dark:text-white overflow-hidden h-screen flex flex-col">
@@ -27,7 +36,7 @@ export default async function MainLayout({
<HeaderWrapper user={session?.user} />
{/* Main Layout */}
<div className="flex flex-1 overflow-hidden">
<div className="flex flex-1 overflow-hidden relative">
{/* Sidebar Navigation - Style Keep */}
<Suspense fallback={<div className="w-64 flex-none hidden md:flex" />}>
<Sidebar className="w-64 flex-none flex-col bg-white dark:bg-[#1e2128] border-e border-slate-200 dark:border-slate-800 overflow-y-auto hidden md:flex" user={session?.user} />
@@ -37,8 +46,12 @@ export default async function MainLayout({
<main className="flex min-h-0 flex-1 flex-col overflow-y-auto bg-background-light dark:bg-background-dark p-4 scroll-smooth">
{children}
</main>
{/* AI Chat Drawer — only shown if user has Assistant IA enabled */}
{showAIAssistant && <AIChat />}
</div>
</div>
</ProvidersWrapper>
);
}