feat: add i18n support with VITE_LOCALE env var (en/fr)

- Lightweight i18n system in src/lib/i18n.ts (no external deps)
- All UI strings extracted to translation keys
- English (default) and French locales included
- Set VITE_LOCALE=fr in .env for French UI
- Fallback to English for unknown locales
This commit is contained in:
Nicolas Varrot
2026-02-11 13:19:20 +00:00
parent 8132ddb59f
commit 99b7db9793
9 changed files with 134 additions and 31 deletions

View File

@@ -1,5 +1,6 @@
import { Menu, Bot, Sparkles, LogOut } from 'lucide-react';
import type { ConnectionStatus, Session } from '../types';
import { t } from '../lib/i18n';
interface Props {
status: ConnectionStatus;
@@ -15,7 +16,7 @@ export function Header({ status, sessionKey, onToggleSidebar, activeSessionData,
return (
<>
<header className="h-14 border-b border-white/8 bg-[#232329]/90 backdrop-blur-xl flex items-center px-4 gap-3 shrink-0" role="banner">
<button onClick={onToggleSidebar} aria-label="Toggle sidebar" className="lg:hidden p-2 rounded-2xl hover:bg-white/5 text-zinc-400 transition-colors">
<button onClick={onToggleSidebar} aria-label={t('header.toggleSidebar')} className="lg:hidden p-2 rounded-2xl hover:bg-white/5 text-zinc-400 transition-colors">
<Menu size={20} />
</button>
<div className="flex items-center gap-3 flex-1 min-w-0">
@@ -24,7 +25,7 @@ export function Header({ status, sessionKey, onToggleSidebar, activeSessionData,
</div>
<div className="min-w-0">
<div className="flex items-center gap-2">
<span className="font-semibold text-zinc-300 text-sm tracking-wide">PinchChat</span>
<span className="font-semibold text-zinc-300 text-sm tracking-wide">{t('header.title')}</span>
<Sparkles className="h-3.5 w-3.5 text-cyan-300/60" />
</div>
<span className="text-xs text-zinc-500 truncate block">{sessionLabel}</span>
@@ -34,25 +35,25 @@ export function Header({ status, sessionKey, onToggleSidebar, activeSessionData,
{status === 'connected' ? (
<div className="flex items-center gap-2 rounded-2xl border border-white/8 bg-zinc-800/30 px-3 py-1.5">
<span className="w-2 h-2 rounded-full bg-cyan-300/80 shadow-[0_0_12px_rgba(34,211,238,0.6)]" />
<span className="text-xs text-zinc-300 hidden sm:inline">Connected</span>
<span className="text-xs text-zinc-300 hidden sm:inline">{t('header.connected')}</span>
</div>
) : status === 'connecting' ? (
<div className="flex items-center gap-2 rounded-2xl border border-white/8 bg-zinc-800/30 px-3 py-1.5">
<span className="w-2 h-2 rounded-full bg-yellow-400/80 pulse-dot" />
<span className="text-xs text-zinc-300 hidden sm:inline">Connecting</span>
<span className="text-xs text-zinc-300 hidden sm:inline">{t('login.connecting')}</span>
</div>
) : (
<div className="flex items-center gap-2 rounded-2xl border border-white/8 bg-zinc-800/30 px-3 py-1.5">
<span className="w-2 h-2 rounded-full bg-red-400/80" />
<span className="text-xs text-zinc-300 hidden sm:inline">Disconnected</span>
<span className="text-xs text-zinc-300 hidden sm:inline">{t('header.disconnected')}</span>
</div>
)}
{onLogout && (
<button
onClick={onLogout}
aria-label="Disconnect and logout"
aria-label={t('header.logout')}
className="p-2 rounded-2xl hover:bg-white/5 text-zinc-500 hover:text-zinc-300 transition-colors"
title="Logout"
title={t('header.logout')}
>
<LogOut size={16} />
</button>