feat: translate all UI strings to English for international release

Complete French→English translation across all frontend pages and backend
services — every label, button, header, empty state, toast, and nav item
is now in English. Build verified clean (tsc + vite). No i18n library
added; direct string replacement throughout.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-22 09:06:37 +02:00
parent f8a0a6d023
commit dcbc9f19fc
24 changed files with 1443 additions and 1444 deletions

View File

@@ -16,14 +16,14 @@ function jaccard(a: string[], b: string[]): number {
const TRIGGERS = ['military', 'sanctions', 'elections', 'natural_disaster', 'health_crisis', 'resource_scarcity', 'trade_war', 'energy', 'political_speech', 'financial_crisis']
const ASSET_CLASSES = ['energy', 'metals', 'agriculture', 'equities', 'indices', 'forex', 'rates']
const ASSET_CLASS_LABELS: Record<string, string> = {
energy: '⚡ Énergie', metals: '🥇 Métaux', agriculture: '🌾 Agri',
equities: '📈 Actions', indices: '📊 Indices', forex: '💱 Forex', rates: '📉 Taux',
energy: '⚡ Energy', metals: '🥇 Metals', agriculture: '🌾 Agri',
equities: '📈 Equities', indices: '📊 Indices', forex: '💱 Forex', rates: '📉 Rates',
}
const TRIGGER_LABELS: Record<string, string> = {
military: '⚔️ Militaire', sanctions: '🚫 Sanctions', elections: '🗳️ Élections',
natural_disaster: '🌪️ Catastrophe', health_crisis: '🏥 Santé',
resource_scarcity: '⚠️ Ressources', trade_war: '🤝 Commerce',
energy: '⚡ Énergie', political_speech: '🎙️ Discours', financial_crisis: '💸 Finance',
military: '⚔️ Military', sanctions: '🚫 Sanctions', elections: '🗳️ Elections',
natural_disaster: '🌪️ Disaster', health_crisis: '🏥 Health',
resource_scarcity: '⚠️ Resources', trade_war: '🤝 Trade',
energy: '⚡ Energy', political_speech: '🎙️ Speech', financial_crisis: '💸 Finance',
}
const EMPTY_PATTERN = {
@@ -35,7 +35,7 @@ const EMPTY_PATTERN = {
function QualityBadge({ score }: { score: number }) {
const color = score >= 75 ? 'badge-green' : score >= 50 ? 'badge-yellow' : 'badge-red'
const label = score >= 75 ? 'Excellent' : score >= 50 ? 'Bon' : 'Faible'
const label = score >= 75 ? 'Excellent' : score >= 50 ? 'Good' : 'Weak'
return <span className={clsx('badge', color)}>{score}/100 {label}</span>
}
@@ -72,7 +72,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
<div className="flex items-center gap-2 mb-0.5 flex-wrap">
<div className="text-sm font-semibold text-white">{p.name}</div>
{isCustom && <span className="badge badge-purple text-xs">Custom</span>}
{!isActive && <span className="badge badge-red text-xs">Désactivé</span>}
{!isActive && <span className="badge badge-red text-xs">Disabled</span>}
{p.ai_quality_score && <QualityBadge score={p.ai_quality_score} />}
<ReliabilityBadge rel={reliability} />
</div>
@@ -83,15 +83,15 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
<div className={clsx('text-sm font-bold', p.expected_move_pct > 0 ? 'positive' : 'negative')}>
{p.expected_move_pct > 0 ? '+' : ''}{p.expected_move_pct}%
</div>
<div className="text-xs text-slate-600">{p.horizon_days}j</div>
<div className="text-xs text-slate-600">{p.horizon_days}d</div>
</div>
<button onClick={onEdit} title="Modifier" className="text-slate-500 hover:text-blue-400 transition-colors">
<button onClick={onEdit} title="Edit" className="text-slate-500 hover:text-blue-400 transition-colors">
<Edit3 className="w-4 h-4" />
</button>
<button onClick={onToggle} title={isActive ? 'Désactiver' : 'Activer'} className={clsx('transition-colors', isActive ? 'text-slate-500 hover:text-yellow-400' : 'text-yellow-500 hover:text-yellow-300')}>
<button onClick={onToggle} title={isActive ? 'Disable' : 'Enable'} className={clsx('transition-colors', isActive ? 'text-slate-500 hover:text-yellow-400' : 'text-yellow-500 hover:text-yellow-300')}>
{isActive ? <Eye className="w-4 h-4" /> : <EyeOff className="w-4 h-4" />}
</button>
<button onClick={onDelete} title="Supprimer" className="text-slate-500 hover:text-red-400 transition-colors">
<button onClick={onDelete} title="Delete" className="text-slate-500 hover:text-red-400 transition-colors">
<Trash2 className="w-4 h-4" />
</button>
</div>
@@ -101,7 +101,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
<div className="flex flex-wrap gap-1 mb-2">
{similarTo.map((s, i) => (
<span key={i} className="inline-flex items-center gap-1 text-[10px] bg-amber-900/30 border border-amber-600/30 text-amber-400 rounded px-1.5 py-0.5">
Similaire à <span className="font-semibold max-w-[140px] truncate">{s.name}</span>
Similar to <span className="font-semibold max-w-[140px] truncate">{s.name}</span>
<span className="font-mono">{Math.round(s.similarity * 100)}%</span>
</span>
))}
@@ -115,7 +115,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
<span className="badge badge-blue text-xs">{p.asset_class}</span>
{aiScore != null ? (
<span className={clsx('badge text-xs font-bold', aiScore >= 50 ? 'badge-green' : aiScore >= 25 ? 'badge-yellow' : 'badge-red')}>
{aiScore}/100 IA
{aiScore}/100 AI
</span>
) : (
<span className="badge badge-purple text-xs">{Math.round(p.probability * 100)}% prob.</span>
@@ -123,14 +123,14 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
</div>
<button onClick={() => setExpanded(!expanded)} className="text-xs text-slate-600 hover:text-slate-400">
{expanded ? '▲ Réduire' : '▼ Voir détails'}
{expanded ? '▲ Collapse' : '▼ See details'}
</button>
{expanded && (
<div className="mt-3 space-y-2">
{p.keywords?.length > 0 && (
<div>
<div className="text-xs text-slate-600 mb-1">Mots-clés de détection</div>
<div className="text-xs text-slate-600 mb-1">Detection keywords</div>
<div className="flex flex-wrap gap-1">
{p.keywords.map((kw: string) => (
<span key={kw} className="text-xs bg-dark-700 text-slate-400 px-1.5 py-0.5 rounded">#{kw}</span>
@@ -140,7 +140,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
)}
{p.historical_instances?.length > 0 && (
<div>
<div className="text-xs text-slate-600 mb-1">Instances historiques</div>
<div className="text-xs text-slate-600 mb-1">Historical instances</div>
{p.historical_instances.map((h: any, i: number) => (
<div key={i} className="card-sm mb-1">
<span className="text-xs text-slate-500">{h.date}</span>
@@ -151,7 +151,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
)}
{p.suggested_trades?.length > 0 && (
<div>
<div className="text-xs text-slate-600 mb-1">Trades suggérés</div>
<div className="text-xs text-slate-600 mb-1">Suggested trades</div>
{p.suggested_trades.map((t: any, i: number) => (
<div key={i} className="flex items-center gap-2 text-xs mb-1 flex-wrap">
<span className="badge badge-green">{t.strategy}</span>
@@ -165,11 +165,11 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
{(p.counter_thesis || p.invalidation_trigger) && (
<div className="card-sm border-red-700/30 bg-red-900/10">
<div className="text-xs text-red-400 font-semibold mb-1 flex items-center gap-1">
<ShieldAlert className="w-3 h-3" /> Contre-thèse & Invalidation
<ShieldAlert className="w-3 h-3" /> Counter-thesis & Invalidation
</div>
{p.counter_thesis && (
<div className="text-xs text-slate-300 mb-1">
<span className="text-slate-500">Contre-thèse :</span> {p.counter_thesis}
<span className="text-slate-500">Counter-thesis:</span> {p.counter_thesis}
</div>
)}
{p.invalidation_trigger && (
@@ -184,7 +184,7 @@ function PatternCard({ p, onEdit, onDelete, onToggle, similarTo, aiScore, reliab
)}
{p.ai_evaluation && Object.keys(p.ai_evaluation).length > 0 && (
<div className="card-sm border-blue-700/30">
<div className="text-xs text-blue-400 font-semibold mb-1">Évaluation IA</div>
<div className="text-xs text-blue-400 font-semibold mb-1">AI Evaluation</div>
{p.ai_evaluation.strengths?.length > 0 && (
<div className="text-xs text-emerald-400 mb-1"> {p.ai_evaluation.strengths[0]}</div>
)}
@@ -233,10 +233,10 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
<div className="flex items-center justify-between p-4 border-b border-slate-700/30">
<div>
<h2 className="text-base font-semibold text-white flex items-center gap-2">
<Sparkles className="w-4 h-4 text-blue-400" /> Suggérer des patterns par l'IA
<Sparkles className="w-4 h-4 text-blue-400" /> Suggest patterns with AI
</h2>
<p className="text-xs text-slate-500 mt-0.5">
GPT-4o analyse l'actualité géo, les prix, le calendrier et le <span className="text-blue-400">régime macro actuel</span> pour proposer des patterns cohérents avec le scénario dominant
GPT-4o analyzes geo news, prices, calendar and the <span className="text-blue-400">current macro regime</span> to propose patterns aligned with the dominant scenario
</p>
</div>
<button onClick={onClose} className="text-slate-500 hover:text-white"><X className="w-5 h-5" /></button>
@@ -248,11 +248,11 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
<div className="text-center py-12">
<Sparkles className="w-10 h-10 text-blue-400/40 mx-auto mb-3" />
<p className="text-sm text-slate-400 mb-4">
L'IA va analyser les news géopolitiques actuelles, les mouvements de prix et le calendrier économique pour proposer des patterns pertinents <em>aujourd'hui</em>.
The AI will analyze current geopolitical news, price movements, and the economic calendar to propose patterns relevant <em>today</em>.
</p>
<button onClick={() => suggest()}
className="bg-blue-600 hover:bg-blue-500 text-white px-6 py-2 rounded text-sm font-semibold">
Lancer l'analyse (GPT-4o)
Run analysis (GPT-4o)
</button>
</div>
)}
@@ -260,7 +260,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
{isPending && (
<div className="text-center py-12">
<div className="w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full animate-spin mx-auto mb-3"></div>
<p className="text-sm text-slate-400">GPT-4o analyse le contexte du moment…</p>
<p className="text-sm text-slate-400">GPT-4o analyzing current context</p>
</div>
)}
@@ -277,7 +277,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
<span className={clsx('badge text-xs font-bold', score >= 50 ? 'badge-green' : score >= 25 ? 'badge-yellow' : 'badge-red')}>
{score}/100 IA
</span>
<span className="text-xs text-slate-500">{p.horizon_days}j · {p.expected_move_pct > 0 ? '+' : ''}{p.expected_move_pct}%</span>
<span className="text-xs text-slate-500">{p.horizon_days}d · {p.expected_move_pct > 0 ? '+' : ''}{p.expected_move_pct}%</span>
</div>
</div>
<button
@@ -287,7 +287,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
'bg-emerald-700/30 border border-emerald-700/40 text-emerald-400 cursor-default': saved.has(idx),
'bg-blue-600 hover:bg-blue-500 text-white': !saved.has(idx),
})}>
{saved.has(idx) ? <><Check className="w-3 h-3" /> Sauvegardé</> : <><Save className="w-3 h-3" /> Sauvegarder</>}
{saved.has(idx) ? <><Check className="w-3 h-3" /> Saved</> : <><Save className="w-3 h-3" /> Save</>}
</button>
</div>
@@ -295,7 +295,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
<div className="flex flex-wrap gap-1 mb-2">
{similars.map((s, i) => (
<span key={i} className="inline-flex items-center gap-1 text-[10px] bg-amber-900/30 border border-amber-600/30 text-amber-400 rounded px-1.5 py-0.5">
Similaire à <span className="font-semibold max-w-[140px] truncate">{s.name}</span>
Similar to <span className="font-semibold max-w-[140px] truncate">{s.name}</span>
<span className="font-mono">{Math.round(s.similarity * 100)}%</span>
</span>
))}
@@ -312,7 +312,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
{(p.counter_thesis || p.invalidation_trigger) && (
<div className="mb-2 text-[11px] bg-red-900/10 border border-red-700/20 rounded px-2 py-1.5 space-y-0.5">
{p.counter_thesis && (
<div className="text-slate-300"><span className="text-red-400/80">⚠ Contre-thèse :</span> {p.counter_thesis}</div>
<div className="text-slate-300"><span className="text-red-400/80"> Counter-thesis:</span> {p.counter_thesis}</div>
)}
{p.invalidation_trigger && (
<div className="text-orange-300/70"><span className="text-slate-500">Trigger :</span> {p.invalidation_trigger}{p.invalidation_probability != null ? ` (${Math.round(p.invalidation_probability * 100)}%)` : ''}</div>
@@ -336,7 +336,7 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
{data && suggestions.length === 0 && (
<div className="card text-center py-8 text-slate-500 text-sm">
Aucun pattern suggéré. Réessayer.
No patterns suggested. Try again.
</div>
)}
</div>
@@ -345,18 +345,18 @@ function AiSuggestModal({ onClose, onSaveAll, allPatterns }: { onClose: () => vo
<div className="p-4 border-t border-slate-700/30 flex justify-between items-center">
{suggestions.length > 0 && (
<span className="text-xs text-slate-500">
{saved.size}/{suggestions.length} sauvegardé{saved.size > 1 ? 's' : ''}
{saved.size}/{suggestions.length} saved
</span>
)}
<div className="flex gap-2 ml-auto">
{data && suggestions.length > 0 && (
<button onClick={() => suggest()} disabled={isPending}
className="flex items-center gap-1 border border-slate-700 text-slate-400 hover:text-slate-200 px-3 py-1.5 rounded text-xs">
<RotateCcw className="w-3 h-3" /> Régénérer
<RotateCcw className="w-3 h-3" /> Regenerate
</button>
)}
<button onClick={onClose} className="border border-slate-700 text-slate-400 hover:text-slate-200 px-4 py-1.5 rounded text-xs">
Fermer
Close
</button>
</div>
</div>
@@ -431,18 +431,18 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
{/* AI Suggest from context */}
{aiStatus?.enabled && (
<div className="card border-blue-500/30">
<div className="section-title flex items-center gap-1"><Brain className="w-3 h-3 text-blue-400" /> Générer depuis un contexte (IA)</div>
<div className="section-title flex items-center gap-1"><Brain className="w-3 h-3 text-blue-400" /> Generate from context (AI)</div>
<div className="flex gap-2">
<textarea
value={suggestInput}
onChange={e => setSuggestInput(e.target.value)}
placeholder="Décris le contexte géopolitique... ex: 'Quand la Chine impose des restrictions sur les terres rares, que se passe-t-il sur le marché des semi-conducteurs et des métaux ?'"
placeholder="Describe the geopolitical context... e.g. 'When China imposes restrictions on rare earths, what happens to semiconductor and metals markets?'"
rows={2}
className="flex-1 bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500 resize-none"
/>
<button onClick={suggestFromContext} disabled={suggestLoading || !suggestInput}
className="shrink-0 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-3 rounded text-xs">
{suggestLoading ? '...' : ' Générer'}
{suggestLoading ? '...' : '⚡ Generate'}
</button>
</div>
</div>
@@ -451,22 +451,22 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
{/* Form fields */}
<div className="grid grid-cols-2 gap-3">
<div className="col-span-2">
<label className="text-xs text-slate-500 mb-1 block">Nom du pattern</label>
<label className="text-xs text-slate-500 mb-1 block">Pattern name</label>
<input value={form.name} onChange={e => set('name', e.target.value)}
placeholder="Ex: Chine tensions Taiwan → Semi-conducteurs spike"
placeholder="E.g.: China Taiwan tensions → Semiconductors spike"
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
</div>
<div className="col-span-2">
<label className="text-xs text-slate-500 mb-1 block">Description</label>
<textarea value={form.description} onChange={e => set('description', e.target.value)}
rows={2} placeholder="Mécanisme géopolitique → impact marché..."
rows={2} placeholder="Geopolitical mechanism → market impact..."
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500 resize-none" />
</div>
</div>
{/* Triggers */}
<div>
<label className="text-xs text-slate-500 mb-2 block">Déclencheurs</label>
<label className="text-xs text-slate-500 mb-2 block">Triggers</label>
<div className="flex flex-wrap gap-1.5">
{TRIGGERS.map(t => (
<button key={t} onClick={() => toggleTrigger(t)}
@@ -482,11 +482,11 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
{/* Keywords */}
<div>
<label className="text-xs text-slate-500 mb-1 block">Mots-clés de détection (anglais)</label>
<label className="text-xs text-slate-500 mb-1 block">Detection keywords (English)</label>
<div className="flex gap-2 mb-2">
<input value={kwInput} onChange={e => setKwInput(e.target.value)}
onKeyDown={e => e.key === 'Enter' && addKeyword()}
placeholder="Ajouter un mot-clé..."
placeholder="Add a keyword..."
className="flex-1 bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
<button onClick={addKeyword} className="bg-dark-600 border border-slate-700 hover:border-slate-500 text-slate-300 px-3 rounded text-xs">+</button>
</div>
@@ -504,48 +504,48 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
{/* Metrics */}
<div className="grid grid-cols-4 gap-3">
<div>
<label className="text-xs text-slate-500 mb-1 block">Classe d'actif</label>
<label className="text-xs text-slate-500 mb-1 block">Asset class</label>
<select value={form.asset_class} onChange={e => set('asset_class', e.target.value)}
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500">
{ASSET_CLASSES.map(a => <option key={a}>{a}</option>)}
</select>
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Move attendu (%)</label>
<label className="text-xs text-slate-500 mb-1 block">Expected move (%)</label>
<input type="number" value={form.expected_move_pct} onChange={e => set('expected_move_pct', Number(e.target.value))}
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Probabilité (0-1)</label>
<label className="text-xs text-slate-500 mb-1 block">Probability (0-1)</label>
<input type="number" step="0.05" min="0" max="1" value={form.probability} onChange={e => set('probability', Number(e.target.value))}
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Horizon (jours)</label>
<label className="text-xs text-slate-500 mb-1 block">Horizon (days)</label>
<input type="number" value={form.horizon_days} onChange={e => set('horizon_days', Number(e.target.value))}
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-blue-500" />
</div>
</div>
{/* Contre-thèse & Invalidation */}
{/* Counter-thesis & Invalidation */}
<div className="space-y-2">
<div>
<label className="text-xs text-slate-500 mb-1 block flex items-center gap-1">
<ShieldAlert className="w-3 h-3 text-red-400" /> Contre-thèse (scénario adverse principal)
<ShieldAlert className="w-3 h-3 text-red-400" /> Counter-thesis (main adverse scenario)
</label>
<textarea value={form.counter_thesis || ''} onChange={e => set('counter_thesis', e.target.value)}
rows={2} placeholder="Ex: accord de paix inattendu, données inflation sous 3%, pivot Fed…"
rows={2} placeholder="E.g.: unexpected peace deal, inflation below 3%, Fed pivot…"
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-red-500/50 resize-none" />
</div>
<div className="grid grid-cols-3 gap-3">
<div className="col-span-2">
<label className="text-xs text-slate-500 mb-1 block">Trigger d'invalidation (événement précis &amp; mesurable)</label>
<label className="text-xs text-slate-500 mb-1 block">Invalidation trigger (precise &amp; measurable event)</label>
<input value={form.invalidation_trigger || ''} onChange={e => set('invalidation_trigger', e.target.value)}
placeholder="Ex: prix pétrole < 70$/b 3j consécutifs"
placeholder="E.g.: oil price < $70/b for 3 consecutive days"
className="w-full bg-dark-700 border border-slate-700 rounded px-2 py-1.5 text-sm text-white focus:outline-none focus:border-red-500/50" />
</div>
<div>
<label className="text-xs text-slate-500 mb-1 block">Prob. invalidation (0-1)</label>
<label className="text-xs text-slate-500 mb-1 block">Invalidation prob. (0-1)</label>
<input type="number" step="0.05" min="0" max="1" value={form.invalidation_probability ?? ''}
onChange={e => set('invalidation_probability', e.target.value ? Number(e.target.value) : undefined)}
placeholder="0.20"
@@ -559,19 +559,19 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
<div className="card border-blue-700/40">
<div className="flex items-center justify-between mb-2">
<div className="text-sm font-semibold text-blue-400 flex items-center gap-1">
<Brain className="w-4 h-4" /> Évaluation IA
<Brain className="w-4 h-4" /> AI Evaluation
</div>
<QualityBadge score={aiResult.quality_score || 0} />
</div>
<div className="grid grid-cols-2 gap-3 text-xs">
<div>
<div className="text-slate-500 mb-1 font-semibold">✓ Points forts</div>
<div className="text-slate-500 mb-1 font-semibold"> Strengths</div>
{aiResult.strengths?.map((s: string, i: number) => (
<div key={i} className="text-emerald-400 mb-0.5"> {s}</div>
))}
</div>
<div>
<div className="text-slate-500 mb-1 font-semibold">⚠ Faiblesses</div>
<div className="text-slate-500 mb-1 font-semibold"> Weaknesses</div>
{aiResult.weaknesses?.map((w: string, i: number) => (
<div key={i} className="text-orange-400 mb-0.5"> {w}</div>
))}
@@ -579,7 +579,7 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
</div>
{aiResult.suggested_improvements?.additional_keywords?.length > 0 && (
<div className="mt-2">
<div className="text-xs text-slate-500 mb-1">Mots-clés suggérés par l'IA</div>
<div className="text-xs text-slate-500 mb-1">Keywords suggested by AI</div>
<div className="flex flex-wrap gap-1">
{aiResult.suggested_improvements.additional_keywords.map((kw: string) => (
<button key={kw} onClick={() => set('keywords', [...form.keywords, kw])}
@@ -597,7 +597,7 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
)}
{aiResult.counter_scenarios?.length > 0 && (
<div className="mt-2">
<div className="text-xs text-slate-500 mb-1">Scénarios d'invalidation</div>
<div className="text-xs text-slate-500 mb-1">Invalidation scenarios</div>
{aiResult.counter_scenarios.map((s: string, i: number) => (
<div key={i} className="text-xs text-red-400 mb-0.5"> {s}</div>
))}
@@ -611,15 +611,15 @@ function PatternForm({ initial, onSave, onCancel }: { initial?: any; onSave: (p:
<button onClick={evaluateWithAI} disabled={aiLoading || !form.name}
className="flex items-center gap-1.5 border border-blue-500/50 text-blue-400 hover:bg-blue-900/20 px-3 py-1.5 rounded text-sm transition-colors disabled:opacity-40">
<Brain className="w-3.5 h-3.5" />
{aiLoading ? 'Évaluation IA...' : 'Évaluer avec IA'}
{aiLoading ? 'AI Evaluation...' : 'Evaluate with AI'}
</button>
)}
<button onClick={onCancel} className="flex items-center gap-1.5 border border-slate-700 text-slate-400 hover:text-slate-200 px-3 py-1.5 rounded text-sm">
<RotateCcw className="w-3.5 h-3.5" /> Annuler
<RotateCcw className="w-3.5 h-3.5" /> Cancel
</button>
<button onClick={() => onSave(form)} disabled={!form.name || form.triggers.length === 0}
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-40 text-white px-4 py-1.5 rounded text-sm font-semibold ml-auto">
<Save className="w-3.5 h-3.5" /> Sauvegarder le pattern
<Save className="w-3.5 h-3.5" /> Save pattern
</button>
</div>
</div>
@@ -725,23 +725,23 @@ export default function PatternEditor() {
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-white flex items-center gap-2">
<Zap className="w-5 h-5 text-blue-400" /> Patterns Géopolitiques
<Zap className="w-5 h-5 text-blue-400" /> Geopolitical Patterns
</h1>
<p className="text-xs text-slate-500 mt-0.5">
{patterns?.length ?? 0} patterns ·{' '}
{patterns?.filter((p: any) => p.source === 'custom').length ?? 0} personnalisés
{patterns?.filter((p: any) => p.source === 'custom').length ?? 0} custom
</p>
</div>
<div className="flex gap-2">
{aiStatus?.enabled && (
<button onClick={() => setShowAiSuggest(true)}
className="flex items-center gap-1.5 border border-blue-500/50 text-blue-400 hover:bg-blue-900/20 px-3 py-1.5 rounded text-sm font-semibold transition-colors">
<Sparkles className="w-4 h-4" /> Suggérer par l'IA
<Sparkles className="w-4 h-4" /> Suggest with AI
</button>
)}
<button onClick={() => { setCreating(true); setEditing(null) }}
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 text-white px-3 py-1.5 rounded text-sm font-semibold">
<Plus className="w-4 h-4" /> Créer un pattern
<Plus className="w-4 h-4" /> Create pattern
</button>
</div>
</div>
@@ -749,7 +749,7 @@ export default function PatternEditor() {
{(creating || editing) && (
<div className="card border-blue-500/30">
<div className="text-sm font-semibold text-white mb-4">
{editing ? `Modifier: ${editing.name}` : 'Nouveau pattern géopolitique'}
{editing ? `Edit: ${editing.name}` : 'New geopolitical pattern'}
</div>
<PatternForm
initial={editing}
@@ -763,9 +763,9 @@ export default function PatternEditor() {
<div className="flex flex-wrap items-center justify-between gap-2">
<div className="flex gap-1 bg-dark-700 p-1 rounded">
{[
{ key: 'all', label: `Tous (${patterns?.length ?? 0})` },
{ key: 'builtin', label: `Intégrés (${patterns?.filter((p: any) => p.source === 'builtin').length ?? 0})` },
{ key: 'custom', label: `Mes patterns (${patterns?.filter((p: any) => p.source === 'custom').length ?? 0})` },
{ key: 'all', label: `All (${patterns?.length ?? 0})` },
{ key: 'builtin', label: `Built-in (${patterns?.filter((p: any) => p.source === 'builtin').length ?? 0})` },
{ key: 'custom', label: `My patterns (${patterns?.filter((p: any) => p.source === 'custom').length ?? 0})` },
].map(({ key, label }) => (
<button key={key} onClick={() => setTab(key as any)}
className={clsx('px-3 py-1.5 rounded text-sm', {
@@ -799,9 +799,9 @@ export default function PatternEditor() {
{/* Period filter */}
<div className="flex gap-1 bg-dark-700 p-1 rounded">
{([
{ key: null, label: 'Tout' },
{ key: 7, label: '7j' },
{ key: 30, label: '30j' },
{ key: null, label: 'All' },
{ key: 7, label: '7d' },
{ key: 30, label: '30d' },
] as const).map(({ key, label }) => (
<button key={String(key)} onClick={() => setFilterDays(key)}
className={clsx('px-2 py-1 rounded text-xs transition-colors', {
@@ -834,7 +834,7 @@ export default function PatternEditor() {
'bg-slate-600/40 border-slate-500/60 text-white': filterAsset === 'all',
'border-slate-700/40 text-slate-500 hover:border-slate-500 hover:text-slate-300': filterAsset !== 'all',
})}>
Tous types
All types
</button>
{ASSET_CLASSES.map(ac => (
<button key={ac} onClick={() => setFilterAsset(filterAsset === ac ? 'all' : ac)}
@@ -847,7 +847,7 @@ export default function PatternEditor() {
))}
{(filterAsset !== 'all' || filterDays !== null) && (
<span className="text-xs text-slate-600 ml-1">
{displayPatterns.length} résultat{displayPatterns.length !== 1 ? 's' : ''}
{displayPatterns.length} result{displayPatterns.length !== 1 ? 's' : ''}
</span>
)}
</div>
@@ -874,12 +874,12 @@ export default function PatternEditor() {
<Zap className="w-8 h-8 mx-auto mb-2 opacity-20" />
<div>
{filterAsset !== 'all'
? `Aucun pattern dans "${ASSET_CLASS_LABELS[filterAsset]}"`
? `No patterns in "${ASSET_CLASS_LABELS[filterAsset]}"`
: filterDays !== null
? `Aucun pattern dans les ${filterDays} derniers jours`
: 'Aucun pattern personnalisé'}
? `No patterns in the last ${filterDays} days`
: 'No custom patterns'}
</div>
<div className="text-xs mt-1">Créer un pattern avec l'aide de l'IA</div>
<div className="text-xs mt-1">Create a pattern with AI assistance</div>
</div>
)}
</div>