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:
@@ -33,25 +33,25 @@ const BUCKET_ICONS: Record<string, string> = {
|
||||
}
|
||||
|
||||
export const CATEGORIES = [
|
||||
{ key: 'all', label: 'Tous' },
|
||||
{ key: 'energy', label: '⛽ Énergie' },
|
||||
{ key: 'metals', label: '🥇 Métaux' },
|
||||
{ key: 'all', label: 'All' },
|
||||
{ key: 'energy', label: '⛽ Energy' },
|
||||
{ key: 'metals', label: '🥇 Metals' },
|
||||
{ key: 'agriculture', label: '🌾 Agri' },
|
||||
{ key: 'indices', label: '📊 Indices' },
|
||||
{ key: 'equities', label: '📈 Actions' },
|
||||
{ key: 'equities', label: '📈 Equities' },
|
||||
{ key: 'forex', label: '💱 Forex' },
|
||||
]
|
||||
|
||||
export const THEMATIC_CATEGORIES = [
|
||||
{ key: 'all', label: 'Toutes' },
|
||||
{ key: 'géopolitique', label: '⚔️ Géopo' },
|
||||
{ key: 'all', label: 'All' },
|
||||
{ key: 'géopolitique', label: '⚔️ Geopo' },
|
||||
{ key: 'macro_monétaire', label: '🏦 Macro' },
|
||||
{ key: 'technique', label: '📐 Technique' },
|
||||
{ key: 'technique', label: '📐 Technical' },
|
||||
{ key: 'commodités_supply',label: '🛢️ Supply' },
|
||||
{ key: 'risk_off', label: '🌩️ Risk-off' },
|
||||
{ key: 'flux_saisonnier', label: '📅 Flux' },
|
||||
{ key: 'géo_économique', label: '🌐 Géo-éco' },
|
||||
{ key: 'crédit_stress', label: '💳 Crédit' },
|
||||
{ key: 'flux_saisonnier', label: '📅 Flows' },
|
||||
{ key: 'géo_économique', label: '🌐 Geo-eco' },
|
||||
{ key: 'crédit_stress', label: '💳 Credit' },
|
||||
]
|
||||
|
||||
const SIGNAL_DIR: Record<string, { label: string; color: string }> = {
|
||||
@@ -80,12 +80,12 @@ export interface TradeItem {
|
||||
}
|
||||
|
||||
const BIAS_DISPLAY: Record<string, { label: string; color: string }> = {
|
||||
'bullish+': { label: '★★ Compatible', color: '#10b981' },
|
||||
'bullish+': { label: '★★ Aligned', color: '#10b981' },
|
||||
'bullish': { label: '★ Compatible', color: '#34d399' },
|
||||
'neutral': { label: '→ Neutre', color: '#64748b' },
|
||||
'bearish': { label: '✗ Défavorable', color: '#f97316' },
|
||||
'neutral': { label: '→ Neutral', color: '#64748b' },
|
||||
'bearish': { label: '✗ Unfavorable', color: '#f97316' },
|
||||
'bearish+': { label: '✗✗ Contra', color: '#ef4444' },
|
||||
'defensive':{ label: '⚠ Défensif', color: '#f59e0b' },
|
||||
'defensive':{ label: '⚠ Defensive', color: '#f59e0b' },
|
||||
}
|
||||
|
||||
// ── IBKR helpers ──────────────────────────────────────────────────────────────
|
||||
@@ -234,33 +234,33 @@ function IBKRTicket({ strategy, underlying, strikeGuidance, expiryDays, entryPri
|
||||
return (
|
||||
<div className="bg-blue-950/20 border border-blue-700/30 rounded p-3 space-y-2.5">
|
||||
<div className="text-[10px] font-bold text-blue-300 uppercase tracking-wide flex items-center gap-1.5">
|
||||
<Terminal className="w-3 h-3" /> Ticket IBKR
|
||||
{!entryPrice && <span className="ml-auto text-slate-600 font-normal normal-case">Prix non disponible — vérifier dans IBKR</span>}
|
||||
<Terminal className="w-3 h-3" /> IBKR Ticket
|
||||
{!entryPrice && <span className="ml-auto text-slate-600 font-normal normal-case">Price unavailable — check in IBKR</span>}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3 text-[10px]">
|
||||
<div>
|
||||
<div className="text-slate-600 mb-0.5">Sous-jacent</div>
|
||||
<div className="text-slate-600 mb-0.5">Underlying</div>
|
||||
<div className="text-white font-mono font-bold text-sm">{underlying}</div>
|
||||
<div className="text-slate-600 text-[9px]">Security Type: OPT</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-slate-600 mb-0.5">Stratégie</div>
|
||||
<div className="text-slate-600 mb-0.5">Strategy</div>
|
||||
<div className="text-slate-200 font-semibold">{strategy}</div>
|
||||
{entryPrice && <div className="text-slate-600 text-[9px]">Sous-jacent: ${entryPrice.toFixed(2)}</div>}
|
||||
{entryPrice && <div className="text-slate-600 text-[9px]">Underlying: ${entryPrice.toFixed(2)}</div>}
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-slate-600 mb-0.5">Expiration</div>
|
||||
<div className="text-slate-600 mb-0.5">Expiry</div>
|
||||
<div className="text-slate-200 font-mono">
|
||||
{expiryDate ? format(expiryDate, 'd MMM yyyy', { locale: fr }) : expiryDays ? `~${expiryDays}j` : '—'}
|
||||
{expiryDate ? format(expiryDate, 'd MMM yyyy', { locale: fr }) : expiryDays ? `~${expiryDays}d` : '—'}
|
||||
</div>
|
||||
<div className="text-slate-600 text-[9px]">{expiryDays ? `${expiryDays}j DTE` : ''}</div>
|
||||
<div className="text-slate-600 text-[9px]">{expiryDays ? `${expiryDays}d DTE` : ''}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{legs.map((leg, i) => (
|
||||
<div key={i} className="flex items-center gap-2 bg-slate-900/70 rounded px-2.5 py-1.5 font-mono text-[10px]">
|
||||
<span className={clsx('w-8 font-bold', leg.action === 'BUY' ? 'text-emerald-400' : 'text-red-400')}>{leg.action}</span>
|
||||
<span className="text-slate-400">1 contrat</span>
|
||||
<span className="text-slate-400">1 contract</span>
|
||||
<span className={clsx('font-bold w-10 text-center', leg.type === 'CALL' ? 'text-blue-300' : leg.type === 'PUT' ? 'text-orange-300' : 'text-slate-300')}>{leg.type}</span>
|
||||
<span className="text-slate-600">Strike</span>
|
||||
<span className="text-yellow-300 font-bold">{fmtStrike(leg.strike)}</span>
|
||||
@@ -270,17 +270,17 @@ function IBKRTicket({ strategy, underlying, strikeGuidance, expiryDays, entryPri
|
||||
</div>
|
||||
<div className="grid grid-cols-3 gap-3 text-[10px]">
|
||||
<div>
|
||||
<div className="text-slate-600 mb-0.5">Type d'ordre</div>
|
||||
<div className="text-slate-600 mb-0.5">Order type</div>
|
||||
<div className="text-slate-300 font-semibold">LIMIT</div>
|
||||
<div className="text-slate-600 text-[9px]">Prix = prime dans IBKR</div>
|
||||
<div className="text-slate-600 text-[9px]">Price = premium in IBKR</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-slate-600 mb-0.5">Budget max</div>
|
||||
<div className="text-slate-300 font-mono">{maxLoss ? `${Math.abs(maxLoss).toFixed(0)}€` : '~1 000€'}</div>
|
||||
<div className="text-slate-600 text-[9px]">Perte max estimée</div>
|
||||
<div className="text-slate-600 mb-0.5">Max budget</div>
|
||||
<div className="text-slate-300 font-mono">{maxLoss ? `${Math.abs(maxLoss).toFixed(0)}€` : '~1,000€'}</div>
|
||||
<div className="text-slate-600 text-[9px]">Estimated max loss</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-slate-600 mb-0.5">Cible</div>
|
||||
<div className="text-slate-600 mb-0.5">Target</div>
|
||||
<div className="text-emerald-400 font-mono">{targetGain ? `+${targetGain.toFixed(0)}€` : '—'}</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -373,7 +373,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<span className="ml-2 shrink-0 text-xs text-slate-500 bg-dark-600 border border-slate-700/40 rounded px-1.5 py-0.5 whitespace-nowrap">à scorer</span>
|
||||
<span className="ml-2 shrink-0 text-xs text-slate-500 bg-dark-600 border border-slate-700/40 rounded px-1.5 py-0.5 whitespace-nowrap">to score</span>
|
||||
)}
|
||||
</div>
|
||||
{convergenceCount > 0 && (
|
||||
@@ -404,7 +404,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: {
|
||||
<><span className="text-slate-700">·</span>
|
||||
{matchedProfile
|
||||
? <span className="font-semibold" style={{ color: matchedProfile.color }}>● {matchedProfile.name}</span>
|
||||
: <span className="text-red-400/80">✗ {gainPct === 0 ? 'Gain non défini' : 'Aucun profil'}</span>
|
||||
: <span className="text-red-400/80">✗ {gainPct === 0 ? 'Gain undefined' : 'No profile'}</span>
|
||||
}</>
|
||||
)}
|
||||
</div>
|
||||
@@ -435,7 +435,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: {
|
||||
<button onClick={() => setExpanded(!expanded)}
|
||||
className="flex items-center gap-1 text-xs text-slate-500 hover:text-slate-300 mb-1.5 transition-colors">
|
||||
{expanded ? <ChevronUp className="w-3 h-3" /> : <ChevronDown className="w-3 h-3" />}
|
||||
{expanded ? 'Masquer la justification' : '↳ Justification du score'}
|
||||
{expanded ? 'Hide breakdown' : '↳ Score breakdown'}
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className="mb-2">
|
||||
@@ -463,7 +463,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: {
|
||||
{addedInfo && (
|
||||
<div className="flex items-center gap-1.5 mb-2 text-[10px] text-emerald-400 bg-emerald-900/20 border border-emerald-700/30 rounded px-2 py-1">
|
||||
<CheckCircle2 className="w-3 h-3 shrink-0" />
|
||||
<span>Ajouté le {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })}</span>
|
||||
<span>Added on {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })}</span>
|
||||
</div>
|
||||
)}
|
||||
<button onClick={() => onAdd(item)}
|
||||
@@ -473,7 +473,7 @@ export function TradeCard({ item, onAdd, macroInfo, addedInfo, profiles }: {
|
||||
: 'text-blue-400 hover:text-white hover:bg-blue-600 border-blue-500/30 hover:border-blue-500'
|
||||
)}>
|
||||
<Plus className="w-3 h-3" />
|
||||
{addedInfo ? 'Ajouter à nouveau' : 'Ajouter au portefeuille'}
|
||||
{addedInfo ? 'Add again' : 'Add to portfolio'}
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
@@ -560,7 +560,7 @@ export function TradeRow({ item, onAdd, macroInfo, addedInfo, profiles, rank }:
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : <span className="text-slate-600 text-[10px]">à scorer</span>}
|
||||
) : <span className="text-slate-600 text-[10px]">to score</span>}
|
||||
</td>
|
||||
<td className="px-2 py-2 w-20">
|
||||
{effectiveConviction !== null && (
|
||||
@@ -646,7 +646,7 @@ export function TradeRow({ item, onAdd, macroInfo, addedInfo, profiles, rank }:
|
||||
{addedInfo && (
|
||||
<div className="flex items-center gap-1.5 text-[10px] text-emerald-400 bg-emerald-900/20 border border-emerald-700/30 rounded px-2 py-1">
|
||||
<CheckCircle2 className="w-3 h-3 shrink-0" />
|
||||
Ajouté le {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })}
|
||||
Added on {format(new Date(addedInfo.entry_date), "d MMM yyyy", { locale: fr })}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
@@ -657,7 +657,7 @@ export function TradeRow({ item, onAdd, macroInfo, addedInfo, profiles, rank }:
|
||||
: 'text-blue-400 hover:text-white hover:bg-blue-600 border-blue-500/30 hover:border-blue-500'
|
||||
)}>
|
||||
<Plus className="w-3 h-3" />
|
||||
{addedInfo ? 'Ajouter à nouveau' : 'Ajouter au portefeuille'}
|
||||
{addedInfo ? 'Add again' : 'Add to portfolio'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -826,7 +826,7 @@ export function TradeIdeasTab() {
|
||||
}, {
|
||||
onSuccess: () => {
|
||||
refetchPositions()
|
||||
setToast({ title: 'Ajouté au portefeuille', sub: `${t.strategy ?? ''} ${t.underlying ?? ''} · ${item.patternName}`.trim() })
|
||||
setToast({ title: 'Added to portfolio', sub: `${t.strategy ?? ''} ${t.underlying ?? ''} · ${item.patternName}`.trim() })
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -844,13 +844,13 @@ export function TradeIdeasTab() {
|
||||
<div className="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h2 className="section-title flex items-center gap-1 mb-0">
|
||||
<Target className="w-3 h-3" /> Idées de trade
|
||||
<Target className="w-3 h-3" /> Trade Ideas
|
||||
</h2>
|
||||
{scoredAt && (
|
||||
<span className="text-xs text-slate-600">— scoré le {format(new Date(scoredAt), "d MMM à HH:mm", { locale: fr })}</span>
|
||||
<span className="text-xs text-slate-600">— scored on {format(new Date(scoredAt), "d MMM HH:mm", { locale: fr })}</span>
|
||||
)}
|
||||
{topScored.length > 0 && (
|
||||
<span className="text-xs text-emerald-500">{topScored.length} scoré{topScored.length > 1 ? 's' : ''} · {allUnscored.length} à scorer</span>
|
||||
<span className="text-xs text-emerald-500">{topScored.length} scored · {allUnscored.length} to score</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
@@ -886,7 +886,7 @@ export function TradeIdeasTab() {
|
||||
'bg-slate-600 text-white': topN === n,
|
||||
'text-slate-400 hover:text-slate-200': topN !== n,
|
||||
})}>
|
||||
{n === 0 ? 'Tous' : `Top ${n}`}
|
||||
{n === 0 ? 'All' : `Top ${n}`}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
@@ -904,10 +904,10 @@ export function TradeIdeasTab() {
|
||||
<button onClick={() => scorePatterns({})}
|
||||
disabled={scoring}
|
||||
className="flex items-center gap-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 text-white px-3 py-1.5 rounded text-xs font-semibold transition-all">
|
||||
{scoring ? <><RefreshCw className="w-3 h-3 animate-spin" /> Scoring GPT-4o...</> : <><Brain className="w-3 h-3" /> Scorer les patterns</>}
|
||||
{scoring ? <><RefreshCw className="w-3 h-3 animate-spin" /> Scoring GPT-4o...</> : <><Brain className="w-3 h-3" /> Score patterns</>}
|
||||
</button>
|
||||
) : (
|
||||
<span className="text-xs text-slate-600 border border-slate-700/30 rounded px-2 py-1">Clé OpenAI requise</span>
|
||||
<span className="text-xs text-slate-600 border border-slate-700/30 rounded px-2 py-1">OpenAI key required</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -927,16 +927,16 @@ export function TradeIdeasTab() {
|
||||
<tr className="border-b border-slate-700/60 text-slate-600 text-[10px] uppercase tracking-wide">
|
||||
<th className="pl-3 py-2 text-left w-6">#</th>
|
||||
<th className="px-2 py-2 text-left">Pattern / Ticker</th>
|
||||
<th className="px-2 py-2 text-left">Stratégie</th>
|
||||
<th className="px-2 py-2 text-left">Strategy</th>
|
||||
<th className="px-2 py-2 text-right">Score</th>
|
||||
<th className="px-2 py-2 w-20"></th>
|
||||
<th className="px-2 py-2 text-right">EV</th>
|
||||
<th className="px-2 py-2 text-right">Gain</th>
|
||||
<th className="px-2 py-2 text-right">Max/Cible</th>
|
||||
<th className="px-2 py-2">Profil</th>
|
||||
<th className="px-2 py-2 text-right">Move</th>
|
||||
<th className="px-2 py-2 text-right">Max/Target</th>
|
||||
<th className="px-2 py-2">Profile</th>
|
||||
<th className="px-2 py-2">Macro</th>
|
||||
<th className="px-2 py-2 text-right">Entrée</th>
|
||||
<th className="px-2 py-2 text-right">Durée</th>
|
||||
<th className="px-2 py-2 text-right">Entry</th>
|
||||
<th className="px-2 py-2 text-right">Duration</th>
|
||||
<th className="pr-3 py-2 w-6"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -956,7 +956,7 @@ export function TradeIdeasTab() {
|
||||
{topScored.length > 0 && (
|
||||
<div className="flex items-center gap-2 text-xs text-slate-600 mb-3">
|
||||
<span className="border-b border-slate-700/30 flex-1"></span>
|
||||
{allUnscored.length} trade{allUnscored.length > 1 ? 's' : ''} à scorer
|
||||
{allUnscored.length} trade{allUnscored.length > 1 ? 's' : ''} to score
|
||||
<span className="border-b border-slate-700/30 flex-1"></span>
|
||||
</div>
|
||||
)}
|
||||
@@ -982,7 +982,7 @@ export function TradeIdeasTab() {
|
||||
|
||||
{allPatterns.length === 0 && (
|
||||
<div className="card text-center py-8 text-slate-500 text-sm">
|
||||
Démarrer le backend — patterns en cours de chargement…
|
||||
Start the backend — patterns loading…
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -8,23 +8,23 @@ import clsx from 'clsx'
|
||||
|
||||
const nav = [
|
||||
{ to: '/', icon: LayoutDashboard, label: 'Cockpit' },
|
||||
{ to: '/geo', icon: Globe, label: 'Radar Géopolitique' },
|
||||
{ to: '/markets', icon: BarChart2, label: 'Marchés & Prix' },
|
||||
{ to: '/macro', icon: Activity, label: 'Régime Macro' },
|
||||
{ to: '/geo', icon: Globe, label: 'Geopolitical Radar' },
|
||||
{ to: '/markets', icon: BarChart2, label: 'Markets & Prices' },
|
||||
{ to: '/macro', icon: Activity, label: 'Macro Regime' },
|
||||
{ to: '/options', icon: TrendingUp, label: 'Options Lab' },
|
||||
{ to: '/patterns', icon: Zap, label: 'Patterns' },
|
||||
{ to: '/portfolio', icon: DollarSign, label: 'Portefeuille' },
|
||||
{ to: '/journal', icon: BookOpen, label: 'Journal de Bord' },
|
||||
{ to: '/rapport', icon: FileBarChart, label: 'Rapport de Cycle' },
|
||||
{ to: '/super-contexte', icon: Brain, label: 'Super Contexte' },
|
||||
{ to: '/portfolio', icon: DollarSign, label: 'Portfolio' },
|
||||
{ to: '/journal', icon: BookOpen, label: 'Trading Journal' },
|
||||
{ to: '/rapport', icon: FileBarChart, label: 'Cycle Report' },
|
||||
{ to: '/super-contexte', icon: Brain, label: 'Super Context' },
|
||||
{ to: '/analytics', icon: FlaskConical, label: 'Analytics' },
|
||||
{ to: '/analytics-advanced', icon: Microscope, label: 'Analytics Avancées' },
|
||||
{ to: '/analytics-advanced', icon: Microscope, label: 'Advanced Analytics' },
|
||||
{ to: '/risk', icon: ShieldAlert, label: 'Risk Dashboard' },
|
||||
{ to: '/var', icon: Gauge, label: 'VaR Analyse' },
|
||||
{ to: '/position-history', icon: GitCompare, label: 'Historique Positions' },
|
||||
{ to: '/var', icon: Gauge, label: 'VaR Analysis' },
|
||||
{ to: '/position-history', icon: GitCompare, label: 'Position History' },
|
||||
{ to: '/backtest', icon: History, label: 'Backtest' },
|
||||
{ to: '/calendar', icon: Calendar, label: 'Calendrier' },
|
||||
{ to: '/logs', icon: ScrollText, label: 'Logs Système' },
|
||||
{ to: '/calendar', icon: Calendar, label: 'Calendar' },
|
||||
{ to: '/logs', icon: ScrollText, label: 'System Logs' },
|
||||
{ to: '/config', icon: Settings, label: 'Configuration' },
|
||||
]
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function Sidebar() {
|
||||
{/* Geo risk indicator */}
|
||||
{riskScore && (
|
||||
<div className={clsx('mx-3 mt-3 px-3 py-2 rounded border text-xs', riskColors[riskScore.level])}>
|
||||
<div className="font-semibold uppercase tracking-wider">Risque Géo</div>
|
||||
<div className="font-semibold uppercase tracking-wider">Geo Risk</div>
|
||||
<div className="text-lg font-bold">{riskScore.score}/100</div>
|
||||
<div className="capitalize">{riskScore.level}</div>
|
||||
</div>
|
||||
@@ -65,7 +65,7 @@ export default function Sidebar() {
|
||||
{/* Portfolio mini summary */}
|
||||
{summary && (summary.open_positions > 0 || summary.unrealized_pnl !== 0) && (
|
||||
<div className="mx-3 mt-2 px-3 py-2 rounded border border-slate-700/30 bg-dark-700/50 text-xs">
|
||||
<div className="text-slate-500 uppercase tracking-wider text-xs mb-1">Portefeuille</div>
|
||||
<div className="text-slate-500 uppercase tracking-wider text-xs mb-1">Portfolio</div>
|
||||
<div className="flex justify-between">
|
||||
<span className="text-slate-400">{summary.open_positions} positions</span>
|
||||
<span className={clsx('font-bold', summary.unrealized_pnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
|
||||
@@ -97,7 +97,7 @@ export default function Sidebar() {
|
||||
'bg-dark-700 text-slate-600': !aiStatus?.enabled,
|
||||
})}>
|
||||
<BrainCircuit className="w-3.5 h-3.5" />
|
||||
<span>{aiStatus?.enabled ? 'IA GPT-4o active' : 'IA non configurée'}</span>
|
||||
<span>{aiStatus?.enabled ? 'AI GPT-4o active' : 'AI not configured'}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user