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:
@@ -25,9 +25,9 @@ function ivRankBg(rank: number | null | undefined): string {
|
||||
|
||||
function ivSignalLabel(rank: number | null | undefined) {
|
||||
if (rank == null) return null
|
||||
if (rank >= 80) return { text: 'Vendre de la vol', icon: <TrendingDown className="w-3 h-3" />, cls: 'text-red-400' }
|
||||
if (rank < 20) return { text: 'Acheter de la vol', icon: <TrendingUp className="w-3 h-3" />, cls: 'text-blue-400' }
|
||||
return { text: 'Neutre', icon: <Minus className="w-3 h-3" />, cls: 'text-slate-500' }
|
||||
if (rank >= 80) return { text: 'Sell vol', icon: <TrendingDown className="w-3 h-3" />, cls: 'text-red-400' }
|
||||
if (rank < 20) return { text: 'Buy vol', icon: <TrendingUp className="w-3 h-3" />, cls: 'text-blue-400' }
|
||||
return { text: 'Neutral', icon: <Minus className="w-3 h-3" />, cls: 'text-slate-500' }
|
||||
}
|
||||
|
||||
function StructureBadge({ structure }: { structure: string | null | undefined }) {
|
||||
@@ -93,7 +93,7 @@ function TickerDetail({ ticker }: { ticker: string }) {
|
||||
<div>
|
||||
<div className="text-[9px] text-slate-500 uppercase tracking-wide font-semibold mb-2">Term Structure</div>
|
||||
<div className="space-y-1.5">
|
||||
{([['30j', ts.iv_30d], ['60j', ts.iv_60d], ['90j', ts.iv_90d], ['180j', ts.iv_180d]] as [string, number | undefined][]).map(([label, iv]) =>
|
||||
{([['30d', ts.iv_30d], ['60d', ts.iv_60d], ['90d', ts.iv_90d], ['180d', ts.iv_180d]] as [string, number | undefined][]).map(([label, iv]) =>
|
||||
iv ? (
|
||||
<div key={label} className="flex items-center gap-2">
|
||||
<span className="text-[9px] text-slate-600 w-7 shrink-0">{label}</span>
|
||||
@@ -130,7 +130,7 @@ function TickerDetail({ ticker }: { ticker: string }) {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[9px] text-slate-700 italic">Données insuffisantes</div>
|
||||
<div className="text-[9px] text-slate-700 italic">Insufficient data</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -150,7 +150,7 @@ function TickerDetail({ ticker }: { ticker: string }) {
|
||||
{flow.gamma_bias && <div className="text-[9px] text-amber-500/70">{flow.gamma_bias}</div>}
|
||||
{flow.unusual_strikes?.length > 0 && (
|
||||
<div className="mt-1 pt-1 border-t border-slate-700/30">
|
||||
<div className="text-[8px] text-slate-600 mb-0.5">Strikes inhabituels</div>
|
||||
<div className="text-[8px] text-slate-600 mb-0.5">Unusual strikes</div>
|
||||
{flow.unusual_strikes.slice(0, 2).map((s: any) => (
|
||||
<div key={s.strike} className="text-[8px] font-mono text-slate-500">
|
||||
{s.strike} {s.type.toUpperCase()} OI={s.total_oi.toLocaleString()}
|
||||
@@ -161,19 +161,19 @@ function TickerDetail({ ticker }: { ticker: string }) {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-[9px] text-slate-700 italic">Pas d'OI disponible</div>
|
||||
<div className="text-[9px] text-slate-700 italic">No OI available</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* History sparkline */}
|
||||
{history.length > 4 && (
|
||||
<div className="col-span-3 flex items-center gap-3 pt-1 border-t border-slate-700/20">
|
||||
<span className="text-[9px] text-slate-600 shrink-0">IV 90j</span>
|
||||
<span className="text-[9px] text-slate-600 shrink-0">IV 90d</span>
|
||||
<IvSparkline history={history} />
|
||||
<div className="text-[9px] text-slate-600">
|
||||
min <span className="text-slate-400">{snap.iv_min_52w_pct}%</span>
|
||||
{' · '}max <span className="text-slate-400">{snap.iv_max_52w_pct}%</span>
|
||||
{' · '}{snap.history_days}j d'historique
|
||||
{' · '}{snap.history_days}d of history
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -203,7 +203,7 @@ function WatchlistRow({ item }: { item: any }) {
|
||||
<span className={clsx('text-sm font-bold font-mono', ivRankColor(item.iv_rank))}>
|
||||
{item.iv_current_pct != null ? `${item.iv_source === 'history' ? '~' : ''}${item.iv_current_pct}%` : '—'}
|
||||
</span>
|
||||
<div className="text-[8px] text-slate-600">{item.iv_source === 'history' ? 'IV estimée' : 'IV actuelle'}</div>
|
||||
<div className="text-[8px] text-slate-600">{item.iv_source === 'history' ? 'Estimated IV' : 'Current IV'}</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0 space-y-1">
|
||||
@@ -230,7 +230,7 @@ function WatchlistRow({ item }: { item: any }) {
|
||||
</div>
|
||||
)}
|
||||
{item.history_days > 0 && (
|
||||
<div className="text-[8px] text-slate-500">{item.history_days}j historique</div>
|
||||
<div className="text-[8px] text-slate-500">{item.history_days}d history</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -286,15 +286,15 @@ function WatchlistManager() {
|
||||
<div className="card space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<List className="w-4 h-4 text-slate-400" />
|
||||
<span className="text-sm font-semibold text-slate-300">Gestion de la Watchlist IV</span>
|
||||
<span className="ml-auto text-xs text-slate-600">{active.length} tickers actifs</span>
|
||||
<span className="text-sm font-semibold text-slate-300">IV Watchlist Manager</span>
|
||||
<span className="ml-auto text-xs text-slate-600">{active.length} active tickers</span>
|
||||
</div>
|
||||
|
||||
{/* Add ticker */}
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
className="flex-1 bg-slate-800 border border-slate-700 rounded px-3 py-1.5 text-xs text-slate-300 placeholder:text-slate-600"
|
||||
placeholder="Ajouter ticker (ex: AAPL, FXI, EEM…)"
|
||||
placeholder="Add ticker (e.g. AAPL, FXI, EEM…)"
|
||||
value={input}
|
||||
onChange={e => setInput(e.target.value.toUpperCase())}
|
||||
onKeyDown={e => e.key === 'Enter' && handleAdd()}
|
||||
@@ -305,10 +305,10 @@ function WatchlistManager() {
|
||||
className="flex items-center gap-1.5 px-3 py-1.5 text-xs bg-blue-600/20 hover:bg-blue-600/30 border border-blue-600/40 text-blue-400 rounded disabled:opacity-40 transition-all"
|
||||
>
|
||||
<Plus className="w-3.5 h-3.5" />
|
||||
{adding ? 'Ajout...' : 'Ajouter'}
|
||||
{adding ? 'Adding...' : 'Add'}
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-[10px] text-slate-600">Le système bootstrappe automatiquement 1 an d'historique IV pour chaque nouveau ticker ajouté.</p>
|
||||
<p className="text-[10px] text-slate-600">The system automatically bootstraps 1 year of IV history for each new ticker added.</p>
|
||||
|
||||
{/* Active tickers */}
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
@@ -331,12 +331,12 @@ function WatchlistManager() {
|
||||
<div className="text-[9px] text-slate-700 flex gap-3">
|
||||
<span className="text-slate-800 border-b border-slate-700">■</span> Builtins
|
||||
<span className="text-blue-700 border-b border-blue-700">■</span> Auto (cycle)
|
||||
<span className="text-emerald-700 border-b border-emerald-700">■</span> Manuel
|
||||
<span className="text-emerald-700 border-b border-emerald-700">■</span> Manual
|
||||
</div>
|
||||
|
||||
{inactive.length > 0 && (
|
||||
<button onClick={() => setShowInactive(!showInactive)} className="text-[10px] text-slate-600 hover:text-slate-400">
|
||||
{showInactive ? 'Masquer' : `Voir ${inactive.length} ticker(s) désactivés`}
|
||||
{showInactive ? 'Hide' : `Show ${inactive.length} disabled ticker(s)`}
|
||||
</button>
|
||||
)}
|
||||
{showInactive && inactive.map(e => (
|
||||
@@ -365,10 +365,10 @@ export default function OptionsLab() {
|
||||
setBootstrapMsg(null)
|
||||
try {
|
||||
await api.post('/options-vol/bootstrap-history')
|
||||
setBootstrapMsg('Bootstrap lancé (~60s) — actualise dans 1 minute pour voir les IV Rank mis à jour.')
|
||||
setBootstrapMsg('Bootstrap started (~60s) — refresh in 1 minute to see updated IV Ranks.')
|
||||
setTimeout(() => refetch(), 70_000)
|
||||
} catch {
|
||||
setBootstrapMsg('Erreur lors du bootstrap.')
|
||||
setBootstrapMsg('Error during bootstrap.')
|
||||
} finally {
|
||||
setBootstrapping(false)
|
||||
}
|
||||
@@ -381,14 +381,14 @@ export default function OptionsLab() {
|
||||
<div className="p-6 space-y-5">
|
||||
{needsBootstrap && !bootstrapMsg && (
|
||||
<div className="flex items-center justify-between gap-3 px-4 py-3 rounded border border-amber-700/40 bg-amber-900/10 text-xs text-amber-300">
|
||||
<span>⚠️ Historique IV insuffisant — IV Rank bloqué à 50%. Bootstrappe 1 an de données réalisées pour calibrer le Rank.</span>
|
||||
<span>⚠️ Insufficient IV history — IV Rank stuck at 50%. Bootstrap 1 year of realized data to calibrate the Rank.</span>
|
||||
<button
|
||||
onClick={handleBootstrap}
|
||||
disabled={bootstrapping}
|
||||
className="flex items-center gap-1.5 text-xs bg-amber-700/30 hover:bg-amber-700/50 border border-amber-600/50 text-amber-200 px-3 py-1.5 rounded transition-all whitespace-nowrap disabled:opacity-50"
|
||||
>
|
||||
<Database className={clsx('w-3.5 h-3.5', bootstrapping && 'animate-pulse')} />
|
||||
{bootstrapping ? 'Chargement...' : 'Initialiser historique'}
|
||||
{bootstrapping ? 'Loading...' : 'Initialize history'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -413,7 +413,7 @@ export default function OptionsLab() {
|
||||
className="flex items-center gap-1.5 text-xs border border-slate-600 text-slate-400 hover:text-slate-200 hover:border-slate-500 px-3 py-1.5 rounded transition-all disabled:opacity-50"
|
||||
>
|
||||
<RefreshCw className={clsx('w-3.5 h-3.5', isFetching && 'animate-spin')} />
|
||||
{isFetching ? 'Chargement...' : 'Actualiser'}
|
||||
{isFetching ? 'Loading...' : 'Refresh'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -422,22 +422,22 @@ export default function OptionsLab() {
|
||||
<div className="card bg-red-900/10 border-red-700/20 text-xs">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<TrendingDown className="w-3.5 h-3.5 text-red-400" />
|
||||
<span className="font-semibold text-red-400">IV Rank > 80% — Vendre de la vol</span>
|
||||
<span className="font-semibold text-red-400">IV Rank > 80% — Sell vol</span>
|
||||
</div>
|
||||
<div className="text-slate-500 leading-snug">Credit spreads, iron condors, covered calls. La prime est élevée → avantage au vendeur.</div>
|
||||
<div className="text-slate-500 leading-snug">Credit spreads, iron condors, covered calls. Premium is high → seller has the edge.</div>
|
||||
</div>
|
||||
<div className="card bg-blue-900/10 border-blue-700/20 text-xs">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<TrendingUp className="w-3.5 h-3.5 text-blue-400" />
|
||||
<span className="font-semibold text-blue-400">IV Rank < 20% — Acheter de la convexité</span>
|
||||
<span className="font-semibold text-blue-400">IV Rank < 20% — Buy convexity</span>
|
||||
</div>
|
||||
<div className="text-slate-500 leading-snug">Long calls, long puts, straddles, strangles. La prime est cheap → bon moment pour acheter du gamma.</div>
|
||||
<div className="text-slate-500 leading-snug">Long calls, long puts, straddles, strangles. Premium is cheap → good time to buy gamma.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-[10px] text-slate-600 -mt-2 px-1">
|
||||
<strong className="text-slate-500">Contango</strong> = IV courte < longue (marché calme, bon pour vendre du court terme) ·
|
||||
<strong className="text-slate-500"> Backwardation</strong> = IV courte > longue (stress, bon pour acheter de la protection courte) ·
|
||||
<strong className="text-slate-500"> Skew +</strong> = puts plus chers que calls (biais baissier institutionnel)
|
||||
<strong className="text-slate-500">Contango</strong> = short IV < long IV (calm market, good for selling short term) ·
|
||||
<strong className="text-slate-500"> Backwardation</strong> = short IV > long IV (stress, good for buying short protection) ·
|
||||
<strong className="text-slate-500"> Skew +</strong> = puts more expensive than calls (institutional bearish bias)
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
@@ -447,14 +447,14 @@ export default function OptionsLab() {
|
||||
) : items.length === 0 ? (
|
||||
<div className="card text-center py-12 text-slate-600">
|
||||
<Activity className="w-8 h-8 mx-auto mb-3 opacity-30" />
|
||||
<div className="font-semibold mb-1 text-slate-500">Aucune donnée IV disponible</div>
|
||||
<div className="font-semibold mb-1 text-slate-500">No IV data available</div>
|
||||
<div className="text-xs text-slate-600">
|
||||
Les ETFs US sont requis (SPY, QQQ, GLD...).
|
||||
<br />L'IV Rank nécessite de l'historique — les données s'accumulent à chaque actualisation.
|
||||
US ETFs are required (SPY, QQQ, GLD...).
|
||||
<br />IV Rank requires history — data accumulates with each refresh.
|
||||
</div>
|
||||
<button onClick={() => refetch()} disabled={isFetching}
|
||||
className="mt-4 text-xs bg-blue-600 hover:bg-blue-500 text-white px-4 py-1.5 rounded disabled:opacity-50">
|
||||
Lancer la collecte
|
||||
Start collection
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
@@ -462,7 +462,7 @@ export default function OptionsLab() {
|
||||
{sellVol.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-red-400 uppercase tracking-wide mb-2 flex items-center gap-1.5">
|
||||
<TrendingDown className="w-3.5 h-3.5" /> Vendre de la vol — IVR > 80% ({sellVol.length})
|
||||
<TrendingDown className="w-3.5 h-3.5" /> Sell vol — IVR > 80% ({sellVol.length})
|
||||
</div>
|
||||
<div className="space-y-1.5">{sellVol.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
|
||||
</div>
|
||||
@@ -470,7 +470,7 @@ export default function OptionsLab() {
|
||||
{buyVol.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-blue-400 uppercase tracking-wide mb-2 flex items-center gap-1.5">
|
||||
<TrendingUp className="w-3.5 h-3.5" /> Acheter de la vol — IVR < 20% ({buyVol.length})
|
||||
<TrendingUp className="w-3.5 h-3.5" /> Buy vol — IVR < 20% ({buyVol.length})
|
||||
</div>
|
||||
<div className="space-y-1.5">{buyVol.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
|
||||
</div>
|
||||
@@ -478,14 +478,14 @@ export default function OptionsLab() {
|
||||
{neutral.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs font-semibold text-slate-500 uppercase tracking-wide mb-2 flex items-center gap-1.5">
|
||||
<Minus className="w-3.5 h-3.5" /> Zone neutre — IVR 20–80% ({neutral.length})
|
||||
<Minus className="w-3.5 h-3.5" /> Neutral zone — IVR 20–80% ({neutral.length})
|
||||
</div>
|
||||
<div className="space-y-1.5">{neutral.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
|
||||
</div>
|
||||
)}
|
||||
{noData.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 uppercase tracking-wide mb-2">Sans historique ({noData.length})</div>
|
||||
<div className="text-xs text-slate-500 uppercase tracking-wide mb-2">No history ({noData.length})</div>
|
||||
<div className="space-y-1.5">{noData.map(item => <WatchlistRow key={item.ticker} item={item} />)}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user