feat: Complete translation in French

This commit is contained in:
2026-07-05 10:44:40 +02:00
parent 3a6afc28ac
commit 2d742be7df
32 changed files with 1841 additions and 1421 deletions
+82 -78
View File
@@ -10,6 +10,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Combobox } from '@/components/ui/combobox';
import { cn } from '@/lib/utils';
import { useI18n } from '@/lib/i18n';
import {
GetAwardDefs, SaveAwardDefs, ResetAwardDefs, AwardFields,
GetAwardReferenceMeta, UpdateAwardReferenceList,
@@ -102,6 +103,7 @@ function Field2({ label, children }: { label: string; children: React.ReactNode
// DxccFilter — pick the entities an award is scoped to by country name (like the
// QSO editor), resolving each to its ADIF DXCC number. Stored as number[].
function DxccFilter({ value, onChange, countries }: { value: number[]; onChange: (v: number[]) => void; countries: string[] }) {
const { t } = useI18n();
const [names, setNames] = useState<Record<number, string>>({});
useEffect(() => {
let live = true;
@@ -124,7 +126,7 @@ function DxccFilter({ value, onChange, countries }: { value: number[]; onChange:
}
return (
<div className="space-y-1.5">
<Combobox value="" options={countries} placeholder="Add country…" onChange={addCountry} className="h-8 w-full" />
<Combobox value="" options={countries} placeholder={t('awed.addCountry')} onChange={addCountry} className="h-8 w-full" />
{value.length > 0 && (
<div className="flex flex-wrap gap-1">
{value.map((n) => (
@@ -141,6 +143,7 @@ function DxccFilter({ value, onChange, countries }: { value: number[]; onChange:
}
export function AwardEditor({ open, onClose, onSaved }: Props) {
const { t } = useI18n();
const [defs, setDefs] = useState<AwardDef[]>([]);
const [fields, setFields] = useState<string[]>([]);
const [meta, setMeta] = useState<Record<string, RefMeta>>({});
@@ -208,7 +211,7 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
setErr('');
try {
const p = await ExportAwards();
if (p) setErr(`Awards exported to:\n${p}`);
if (p) setErr(t('awed.exportedTo', { path: p }));
} catch (e: any) { setErr(String(e?.message ?? e)); }
}
// Import an award bundle: definitions are upserted by code, reference lists
@@ -221,7 +224,7 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
const [d] = await Promise.all([GetAwardDefs(), loadMeta()]);
setDefs((d ?? []) as any); setSel(0);
onSaved();
setErr(`Imported ${r.awards} award(s) and ${r.references} reference(s).`);
setErr(t('awed.importedMsg', { awards: r.awards, references: r.references }));
} catch (e: any) { setErr(String(e?.message ?? e)); }
}
async function updateList(code: string) {
@@ -244,7 +247,7 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
<Dialog open={open} onOpenChange={(o) => { if (!o) onClose(); }}>
<DialogContent className="max-w-5xl max-h-[92vh] grid grid-rows-[auto_1fr_auto] gap-0 p-0">
<DialogHeader className="px-5 py-3 border-b">
<DialogTitle>Award management</DialogTitle>
<DialogTitle>{t('awed.awardManagement')}</DialogTitle>
</DialogHeader>
<div className="grid grid-cols-[220px_1fr] min-h-0 overflow-hidden">
@@ -253,7 +256,7 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
<div className="p-2 border-b">
<div className="relative">
<Search className="absolute left-2 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground" />
<Input className="h-7 pl-7 text-xs" placeholder="Search awards…" value={search} onChange={(e) => setSearch(e.target.value)} />
<Input className="h-7 pl-7 text-xs" placeholder={t('awed.searchAwards')} value={search} onChange={(e) => setSearch(e.target.value)} />
</div>
</div>
<div className="flex-1 overflow-auto">
@@ -268,22 +271,22 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
))}
</div>
<Button variant="ghost" size="sm" className="m-2 h-7 justify-start" onClick={addAward}>
<Plus className="size-3.5 mr-1" /> New award
<Plus className="size-3.5 mr-1" /> {t('awed.newAward')}
</Button>
</div>
{/* Right: tabbed editor for selected award */}
<div className="flex flex-col min-h-0 overflow-hidden">
{err && <div onClick={() => setErr('')} title="Click to dismiss" className="mx-4 mt-3 text-xs text-destructive bg-destructive/10 border border-destructive/30 rounded px-3 py-1.5 whitespace-pre-line break-all cursor-pointer">{err}</div>}
{err && <div onClick={() => setErr('')} title={t('awed.clickToDismiss')} className="mx-4 mt-3 text-xs text-destructive bg-destructive/10 border border-destructive/30 rounded px-3 py-1.5 whitespace-pre-line break-all cursor-pointer">{err}</div>}
{!cur ? (
<div className="flex-1 grid place-items-center text-sm text-muted-foreground">Select or create an award.</div>
<div className="flex-1 grid place-items-center text-sm text-muted-foreground">{t('awed.selectOrCreate')}</div>
) : (
<Tabs defaultValue="info" className="flex flex-col min-h-0 overflow-hidden">
<TabsList className="px-3 justify-start">
<TabsTrigger value="info">Award info</TabsTrigger>
<TabsTrigger value="type">Award type</TabsTrigger>
<TabsTrigger value="conf">Confirmation</TabsTrigger>
<TabsTrigger value="refs">References</TabsTrigger>
<TabsTrigger value="info">{t('awed.tabInfo')}</TabsTrigger>
<TabsTrigger value="type">{t('awed.tabType')}</TabsTrigger>
<TabsTrigger value="conf">{t('awed.tabConfirmation')}</TabsTrigger>
<TabsTrigger value="refs">{t('awed.tabReferences')}</TabsTrigger>
</TabsList>
<div className="flex-1 overflow-auto p-4">
@@ -291,29 +294,29 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
<TabsContent value="info" className="mt-0 space-y-2.5">
<div className="flex items-center gap-2">
<Input className="h-8 w-28 font-mono font-semibold" value={cur.code} onChange={(e) => patch({ code: e.target.value })} placeholder="CODE" />
<Input className="h-8 flex-1" value={cur.name} onChange={(e) => patch({ name: e.target.value })} placeholder="Award name" />
<label className="flex items-center gap-1.5 text-xs cursor-pointer"><Checkbox checked={cur.valid !== false} onCheckedChange={(c) => patch({ valid: !!c })} /> Valid</label>
<button className="text-muted-foreground hover:text-destructive" title="Delete award" onClick={() => removeAward(sel)}><Trash2 className="size-4" /></button>
<Input className="h-8 flex-1" value={cur.name} onChange={(e) => patch({ name: e.target.value })} placeholder={t('awed.awardName')} />
<label className="flex items-center gap-1.5 text-xs cursor-pointer"><Checkbox checked={cur.valid !== false} onCheckedChange={(c) => patch({ valid: !!c })} /> {t('awed.valid')}</label>
<button className="text-muted-foreground hover:text-destructive" title={t('awed.deleteAward')} onClick={() => removeAward(sel)}><Trash2 className="size-4" /></button>
</div>
<Field2 label="Description"><Input className="h-8" value={cur.description ?? ''} onChange={(e) => patch({ description: e.target.value })} /></Field2>
<Field2 label="Award URL"><Input className="h-8" value={cur.url ?? ''} onChange={(e) => patch({ url: e.target.value })} /></Field2>
<Field2 label="Reference URL"><Input className="h-8" value={cur.ref_url ?? ''} onChange={(e) => patch({ ref_url: e.target.value })} placeholder="https://…/<REF>" /></Field2>
<Field2 label={t('awed.description')}><Input className="h-8" value={cur.description ?? ''} onChange={(e) => patch({ description: e.target.value })} /></Field2>
<Field2 label={t('awed.awardUrl')}><Input className="h-8" value={cur.url ?? ''} onChange={(e) => patch({ url: e.target.value })} /></Field2>
<Field2 label={t('awed.referenceUrl')}><Input className="h-8" value={cur.ref_url ?? ''} onChange={(e) => patch({ ref_url: e.target.value })} placeholder="https://…/<REF>" /></Field2>
<div className="grid grid-cols-2 gap-3">
<Field2 label="Valid from"><Input type="date" className="h-8" value={cur.valid_from ?? ''} onChange={(e) => patch({ valid_from: e.target.value })} /></Field2>
<Field2 label="Valid to"><Input type="date" className="h-8" value={cur.valid_to ?? ''} onChange={(e) => patch({ valid_to: e.target.value })} /></Field2>
<Field2 label={t('awed.validFrom')}><Input type="date" className="h-8" value={cur.valid_from ?? ''} onChange={(e) => patch({ valid_from: e.target.value })} /></Field2>
<Field2 label={t('awed.validTo')}><Input type="date" className="h-8" value={cur.valid_to ?? ''} onChange={(e) => patch({ valid_to: e.target.value })} /></Field2>
</div>
<div className="grid grid-cols-[120px_1fr] gap-2">
<Label className="text-xs text-muted-foreground pt-1.5">DXCC filter</Label>
<Label className="text-xs text-muted-foreground pt-1.5">{t('awed.dxccFilter')}</Label>
<DxccFilter value={cur.dxcc_filter ?? []} onChange={(v) => patch({ dxcc_filter: v })} countries={countries} />
</div>
<div className="space-y-1"><Label className="text-xs text-muted-foreground">Valid bands (empty = all)</Label><Chips all={BANDS} value={cur.valid_bands ?? []} onToggle={(v) => toggleIn('valid_bands', v)} /></div>
<div className="space-y-1"><Label className="text-xs text-muted-foreground">Emission (empty = all)</Label><Chips all={EMISSIONS} value={cur.emission ?? []} onToggle={(v) => toggleIn('emission', v)} /></div>
<div className="space-y-1"><Label className="text-xs text-muted-foreground">Valid modes (empty = all)</Label><Chips all={MODES} value={cur.valid_modes ?? []} onToggle={(v) => toggleIn('valid_modes', v)} /></div>
<div className="space-y-1"><Label className="text-xs text-muted-foreground">{t('awed.validBands')}</Label><Chips all={BANDS} value={cur.valid_bands ?? []} onToggle={(v) => toggleIn('valid_bands', v)} /></div>
<div className="space-y-1"><Label className="text-xs text-muted-foreground">{t('awed.emission')}</Label><Chips all={EMISSIONS} value={cur.emission ?? []} onToggle={(v) => toggleIn('emission', v)} /></div>
<div className="space-y-1"><Label className="text-xs text-muted-foreground">{t('awed.validModes')}</Label><Chips all={MODES} value={cur.valid_modes ?? []} onToggle={(v) => toggleIn('valid_modes', v)} /></div>
</TabsContent>
{/* ── Award type ── */}
<TabsContent value="type" className="mt-0 space-y-2.5">
<Field2 label="Award type">
<Field2 label={t('awed.awardType')}>
<Select value={cur.type || 'QSOFIELDS'} onValueChange={(v) => patch({ type: v })}>
<SelectTrigger className="h-8 text-xs w-48"><SelectValue /></SelectTrigger>
<SelectContent>{
@@ -324,18 +327,18 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
}</SelectContent>
</Select>
</Field2>
<label className="flex items-center gap-2 text-xs cursor-pointer"><Checkbox checked={!!cur.multi} onCheckedChange={(c) => patch({ multi: !!c })} /> Allow multiple references on a single QSO</label>
<label className="flex items-center gap-2 text-xs cursor-pointer"><Checkbox checked={!!cur.dynamic} onCheckedChange={(c) => patch({ dynamic: !!c })} /> Dynamic references (not predefined any value counts, like POTA)</label>
<label className="flex items-center gap-2 text-xs cursor-pointer"><Checkbox checked={!!cur.multi} onCheckedChange={(c) => patch({ multi: !!c })} /> {t('awed.allowMultiple')}</label>
<label className="flex items-center gap-2 text-xs cursor-pointer"><Checkbox checked={!!cur.dynamic} onCheckedChange={(c) => patch({ dynamic: !!c })} /> {t('awed.dynamicRefs')}</label>
<div className="border-t pt-2.5 mt-1 space-y-2.5">
<p className="text-[11px] text-muted-foreground">QSO parameters (used by QSOFIELDS / REFERENCE types)</p>
<Field2 label="Search in field">
<p className="text-[11px] text-muted-foreground">{t('awed.qsoParams')}</p>
<Field2 label={t('awed.searchInField')}>
<Select value={cur.field} onValueChange={(v) => patch({ field: v })}>
<SelectTrigger className="h-8 text-xs w-56"><SelectValue /></SelectTrigger>
<SelectContent className="max-h-72">{fields.map((f) => <SelectItem key={f} value={f}>{f}</SelectItem>)}</SelectContent>
</Select>
</Field2>
<Field2 label="Match by">
<Field2 label={t('awed.matchBy')}>
<div className="flex items-center gap-3 text-xs">
{['code', 'description', 'pattern'].map((m) => (
<label key={m} className="flex items-center gap-1.5 cursor-pointer">
@@ -344,21 +347,21 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
))}
</div>
</Field2>
<label className="flex items-center gap-2 text-xs cursor-pointer pl-[128px]"><Checkbox checked={!!cur.exact_match} onCheckedChange={(c) => patch({ exact_match: !!c })} /> Exact match (else search reference inside the field)</label>
<Field2 label="Pattern (regex)"><Input className="h-8 font-mono text-xs" value={cur.pattern} onChange={(e) => patch({ pattern: e.target.value })} placeholder="group 1 = reference (for match-by pattern / dynamic)" /></Field2>
<label className="flex items-center gap-2 text-xs cursor-pointer pl-[128px]"><Checkbox checked={!!cur.exact_match} onCheckedChange={(c) => patch({ exact_match: !!c })} /> {t('awed.exactMatch')}</label>
<Field2 label={t('awed.patternRegex')}><Input className="h-8 font-mono text-xs" value={cur.pattern} onChange={(e) => patch({ pattern: e.target.value })} placeholder={t('awed.patternPlaceholder')} /></Field2>
<div className="grid grid-cols-2 gap-3">
<Field2 label="Leading string"><Input className="h-8 font-mono text-xs" value={cur.leading_str ?? ''} onChange={(e) => patch({ leading_str: e.target.value })} /></Field2>
<Field2 label="Trailing string"><Input className="h-8 font-mono text-xs" value={cur.trailing_str ?? ''} onChange={(e) => patch({ trailing_str: e.target.value })} /></Field2>
<Field2 label={t('awed.leadingString')}><Input className="h-8 font-mono text-xs" value={cur.leading_str ?? ''} onChange={(e) => patch({ leading_str: e.target.value })} /></Field2>
<Field2 label={t('awed.trailingString')}><Input className="h-8 font-mono text-xs" value={cur.trailing_str ?? ''} onChange={(e) => patch({ trailing_str: e.target.value })} /></Field2>
</div>
{/* Additional OR searches: a QSO earns a reference if the
primary rule OR any of these match. */}
<div className="border-t pt-2.5 space-y-2">
<div className="flex items-center justify-between">
<p className="text-[11px] text-muted-foreground">Additional searches <span className="font-semibold">(OR)</span> also match the reference if any of these hit</p>
<p className="text-[11px] text-muted-foreground">{t('awed.additionalSearches')} <span className="font-semibold">(OR)</span> {t('awed.orAlsoMatch')}</p>
<Button size="sm" variant="outline" className="h-7"
onClick={() => patch({ or_rules: [...(cur.or_rules ?? []), { field: cur.field || 'note', match_by: 'pattern', pattern: '', prefix: '' }] })}>
<Plus className="size-3.5" /> Add OR
<Plus className="size-3.5" /> {t('awed.addOr')}
</Button>
</div>
{(cur.or_rules ?? []).map((r, ri) => {
@@ -367,7 +370,7 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
return (
<div key={ri} className="rounded-md border border-border bg-muted/20 p-2 space-y-2">
<div className="flex items-center gap-2">
<span className="text-[11px] text-muted-foreground">OR search in</span>
<span className="text-[11px] text-muted-foreground">{t('awed.orSearchIn')}</span>
<Select value={r.field} onValueChange={(v) => upd({ field: v })}>
<SelectTrigger className="h-7 text-xs w-44"><SelectValue /></SelectTrigger>
<SelectContent className="max-h-72">{fields.map((f) => <SelectItem key={f} value={f}>{f}</SelectItem>)}</SelectContent>
@@ -379,12 +382,12 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
</label>
))}
</div>
<label className="flex items-center gap-1.5 text-[11px] cursor-pointer"><Checkbox checked={!!r.exact_match} onCheckedChange={(c) => upd({ exact_match: !!c })} /> exact</label>
<button className="ml-auto text-destructive hover:opacity-70" onClick={del} title="Remove this OR search"><Trash2 className="size-4" /></button>
<label className="flex items-center gap-1.5 text-[11px] cursor-pointer"><Checkbox checked={!!r.exact_match} onCheckedChange={(c) => upd({ exact_match: !!c })} /> {t('awed.exact')}</label>
<button className="ml-auto text-destructive hover:opacity-70" onClick={del} title={t('awed.removeOrSearch')}><Trash2 className="size-4" /></button>
</div>
<div className="grid grid-cols-[1fr_120px] gap-2">
<Input className="h-7 font-mono text-xs" value={r.pattern ?? ''} onChange={(e) => upd({ pattern: e.target.value })} placeholder="regex — group 1 = reference (e.g. \b(\d{2})\d{3}\b for postal → dept)" />
<Input className="h-7 font-mono text-xs" value={r.prefix ?? ''} onChange={(e) => upd({ prefix: e.target.value })} placeholder="prefix (D)" title="Prepended to each found reference, e.g. 74 → D74" />
<Input className="h-7 font-mono text-xs" value={r.pattern ?? ''} onChange={(e) => upd({ pattern: e.target.value })} placeholder={t('awed.orPatternPlaceholder')} />
<Input className="h-7 font-mono text-xs" value={r.prefix ?? ''} onChange={(e) => upd({ prefix: e.target.value })} placeholder={t('awed.prefixPlaceholder')} title={t('awed.prefixTitle')} />
</div>
</div>
);
@@ -397,20 +400,20 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
<TabsContent value="conf" className="mt-0 space-y-4">
<div className="grid grid-cols-2 gap-6">
<div className="space-y-1.5">
<Label className="text-xs font-semibold">Confirmation (worked confirmed)</Label>
<Label className="text-xs font-semibold">{t('awed.confirmationLabel')}</Label>
{CONFIRM_SRC.map((c) => (
<label key={c.id} className="flex items-center gap-2 text-xs cursor-pointer"><Checkbox checked={(cur.confirm ?? []).includes(c.id)} onCheckedChange={() => toggleIn('confirm', c.id)} /> {c.label}</label>
))}
</div>
<div className="space-y-1.5">
<Label className="text-xs font-semibold">Validation (confirmed validated)</Label>
<Label className="text-xs font-semibold">{t('awed.validationLabel')}</Label>
{CONFIRM_SRC.map((c) => (
<label key={c.id} className="flex items-center gap-2 text-xs cursor-pointer"><Checkbox checked={(cur.validate ?? []).includes(c.id)} onCheckedChange={() => toggleIn('validate', c.id)} /> {c.label}</label>
))}
</div>
</div>
<Field2 label="Grant codes"><Input className="h-8" value={cur.grant_codes ?? ''} onChange={(e) => patch({ grant_codes: e.target.value })} /></Field2>
<label className="flex items-center gap-2 text-xs cursor-pointer"><Checkbox checked={!!cur.export_credit_granted} onCheckedChange={(c) => patch({ export_credit_granted: !!c })} /> Export award in ADIF credit_granted field</label>
<Field2 label={t('awed.grantCodes')}><Input className="h-8" value={cur.grant_codes ?? ''} onChange={(e) => patch({ grant_codes: e.target.value })} /></Field2>
<label className="flex items-center gap-2 text-xs cursor-pointer"><Checkbox checked={!!cur.export_credit_granted} onCheckedChange={(c) => patch({ export_credit_granted: !!c })} /> {t('awed.exportCreditGranted')}</label>
</TabsContent>
{/* ── References ── */}
@@ -428,16 +431,16 @@ export function AwardEditor({ open, onClose, onSaved }: Props) {
</div>
<DialogFooter className="px-5 py-3 border-t !flex-row">
<Button variant="ghost" onClick={reset}><RotateCcw className="size-3.5 mr-1" /> Reset to defaults</Button>
<Button variant="outline" onClick={exportAwards} title="Export all award definitions + reference lists to a JSON backup">
<Download className="size-3.5 mr-1" /> Export
<Button variant="ghost" onClick={reset}><RotateCcw className="size-3.5 mr-1" /> {t('awed.resetDefaults')}</Button>
<Button variant="outline" onClick={exportAwards} title={t('awed.exportTitle')}>
<Download className="size-3.5 mr-1" /> {t('awed.export')}
</Button>
<Button variant="outline" onClick={importAwards} title="Import an award bundle (definitions + reference lists)">
<Upload className="size-3.5 mr-1" /> Import
<Button variant="outline" onClick={importAwards} title={t('awed.importTitle')}>
<Upload className="size-3.5 mr-1" /> {t('awed.import')}
</Button>
<div className="flex-1" />
<Button variant="outline" onClick={onClose}>Cancel</Button>
<Button onClick={save}><Save className="size-3.5 mr-1" /> Save</Button>
<Button variant="outline" onClick={onClose}>{t('awed.cancel')}</Button>
<Button onClick={save}><Save className="size-3.5 mr-1" /> {t('awed.save')}</Button>
</DialogFooter>
</DialogContent>
</Dialog>
@@ -451,6 +454,7 @@ function ReferencesPanel({ code, presets, meta, onUpdateOnline, updating, onChan
code: string; presets: Preset[]; meta?: RefMeta;
onUpdateOnline: () => void; updating: boolean; onChanged: () => void; setErr: (s: string) => void;
}) {
const { t } = useI18n();
const [refs, setRefs] = useState<AwardRef[]>([]);
const [q, setQ] = useState('');
const [selCode, setSelCode] = useState<string | null>(null);
@@ -486,7 +490,7 @@ function ReferencesPanel({ code, presets, meta, onUpdateOnline, updating, onChan
}, [code, q, large]);
async function populateBuiltin() {
try { const n = await PopulateBuiltinReferences(code); load(); onChanged(); setErr(`Populated ${n} built-in references.`); }
try { const n = await PopulateBuiltinReferences(code); load(); onChanged(); setErr(t('awed.populatedMsg', { n })); }
catch (e: any) { setErr(String(e?.message ?? e)); }
}
@@ -505,7 +509,7 @@ function ReferencesPanel({ code, presets, meta, onUpdateOnline, updating, onChan
catch (e: any) { setErr(String(e?.message ?? e)); }
}
async function addRef() {
const c = prompt('New reference code:')?.trim().toUpperCase();
const c = prompt(t('awed.newRefCodePrompt'))?.trim().toUpperCase();
if (!c) return;
const r: AwardRef = { code: c, name: '', dxcc: 0, group: '', subgrp: '', valid: true };
await saveRef(r); setSelCode(c);
@@ -520,7 +524,7 @@ function ReferencesPanel({ code, presets, meta, onUpdateOnline, updating, onChan
catch (e: any) { setErr(String(e?.message ?? e)); }
}
async function importBulk() {
try { const n = await ImportAwardReferencesText(code, bulk); setBulk(''); setShowBulk(false); load(); onChanged(); setErr(`Imported ${n} references.`); }
try { const n = await ImportAwardReferencesText(code, bulk); setBulk(''); setShowBulk(false); load(); onChanged(); setErr(t('awed.importedRefsMsg', { n })); }
catch (e: any) { setErr(String(e?.message ?? e)); }
}
@@ -528,46 +532,46 @@ function ReferencesPanel({ code, presets, meta, onUpdateOnline, updating, onChan
<div className="space-y-2">
{/* Toolbar */}
<div className="flex items-center gap-2 flex-wrap">
<span className="text-xs text-muted-foreground">Reference count: <span className="font-mono text-foreground">{total.toLocaleString()}</span></span>
<span className="text-xs text-muted-foreground">{t('awed.referenceCount')} <span className="font-mono text-foreground">{total.toLocaleString()}</span></span>
<div className="flex-1" />
<Select value="" onValueChange={applyPreset}>
<SelectTrigger className="h-7 w-44 text-xs"><SelectValue placeholder="Apply preset…" /></SelectTrigger>
<SelectTrigger className="h-7 w-44 text-xs"><SelectValue placeholder={t('awed.applyPreset')} /></SelectTrigger>
<SelectContent>{presets.map((p) => <SelectItem key={p.key} value={p.key}>{p.name}</SelectItem>)}</SelectContent>
</Select>
<Button variant="outline" size="sm" className="h-7" onClick={() => setShowBulk((s) => !s)}>Paste / CSV</Button>
<Button variant="outline" size="sm" className="h-7" onClick={() => setShowBulk((s) => !s)}>{t('awed.pasteCsv')}</Button>
{hasBuiltin && (
<Button variant="outline" size="sm" className="h-7" onClick={populateBuiltin} title="Replace with the shipped built-in list (DXCC entities, French departments, …)">
Populate built-in
<Button variant="outline" size="sm" className="h-7" onClick={populateBuiltin} title={t('awed.populateBuiltinTitle')}>
{t('awed.populateBuiltin')}
</Button>
)}
{meta?.can_update && (
<Button variant="outline" size="sm" className="h-7" disabled={updating} onClick={onUpdateOnline}>
{updating ? <Loader2 className="size-3.5 mr-1 animate-spin" /> : <Download className="size-3.5 mr-1" />} Update online
{updating ? <Loader2 className="size-3.5 mr-1 animate-spin" /> : <Download className="size-3.5 mr-1" />} {t('awed.updateOnline')}
</Button>
)}
<Button variant="outline" size="sm" className="h-7" onClick={addRef}><Plus className="size-3.5 mr-1" /> Add</Button>
<Button variant="outline" size="sm" className="h-7" onClick={addRef}><Plus className="size-3.5 mr-1" /> {t('awed.add')}</Button>
</div>
{showBulk && (
<div className="space-y-1.5 border rounded p-2 bg-muted/30">
<p className="text-[11px] text-muted-foreground">One reference per line: <span className="font-mono">CODE,Description,Group,Subgroup,DXCC</span> (comma/semicolon/tab). Replaces the whole list.</p>
<p className="text-[11px] text-muted-foreground">{t('awed.onePerLine')} <span className="font-mono">CODE,Description,Group,Subgroup,DXCC</span> {t('awed.replacesList')}</p>
<Textarea rows={6} className="font-mono text-xs" value={bulk} onChange={(e) => setBulk(e.target.value)} placeholder={'ON,Ontario,,,1\nQC,Quebec,,,1'} />
<div className="flex justify-end gap-2"><Button variant="ghost" size="sm" className="h-7" onClick={() => setShowBulk(false)}>Cancel</Button><Button size="sm" className="h-7" onClick={importBulk}>Import</Button></div>
<div className="flex justify-end gap-2"><Button variant="ghost" size="sm" className="h-7" onClick={() => setShowBulk(false)}>{t('awed.cancel')}</Button><Button size="sm" className="h-7" onClick={importBulk}>{t('awed.import2')}</Button></div>
</div>
)}
<div className="grid grid-cols-[200px_1fr] gap-3">
{/* List */}
<div className="border rounded flex flex-col min-h-0 max-h-[46vh]">
<div className="p-1.5 border-b"><Input className="h-7 text-xs" placeholder="Search…" value={q} onChange={(e) => setQ(e.target.value)} /></div>
<div className="p-1.5 border-b"><Input className="h-7 text-xs" placeholder={t('awed.search')} value={q} onChange={(e) => setQ(e.target.value)} /></div>
<div className="flex-1 overflow-auto">
{busy && <div className="px-2 py-1.5 text-[11px] text-muted-foreground flex items-center gap-1.5"><Loader2 className="size-3 animate-spin" /> Searching</div>}
{busy && <div className="px-2 py-1.5 text-[11px] text-muted-foreground flex items-center gap-1.5"><Loader2 className="size-3 animate-spin" /> {t('awed.searching')}</div>}
{!busy && large && q.trim().length < 2 && (
<div className="m-2 rounded border border-amber-300 bg-amber-50 px-2 py-1.5 text-[11px] text-amber-800">
Too many items ({total.toLocaleString()}). Please refine search (type 2+ characters).
{t('awed.tooManyItems', { total: total.toLocaleString() })}
</div>
)}
{!busy && filtered.length === 0 && !(large && q.trim().length < 2) && <div className="px-2 py-1.5 text-[11px] text-muted-foreground">No references.</div>}
{!busy && filtered.length === 0 && !(large && q.trim().length < 2) && <div className="px-2 py-1.5 text-[11px] text-muted-foreground">{t('awed.noReferences')}</div>}
{filtered.map((r) => (
<button key={r.code} onClick={() => setSelCode(r.code)}
className={cn('flex w-full items-baseline gap-2 px-2 py-1 text-left text-xs border-b border-border/30', r.code === selCode ? 'bg-accent' : 'hover:bg-accent/50', !r.valid && 'opacity-50')}>
@@ -581,37 +585,37 @@ function ReferencesPanel({ code, presets, meta, onUpdateOnline, updating, onChan
{/* Per-reference editor */}
<div className="border rounded p-3">
{!sel ? (
<div className="grid place-items-center h-full text-xs text-muted-foreground">Select a reference, or Add / import a list.</div>
<div className="grid place-items-center h-full text-xs text-muted-foreground">{t('awed.selectReference')}</div>
) : (
<div className="space-y-2">
<div className="flex items-center gap-2">
<Input className="h-8 w-28 font-mono font-semibold" value={sel.code} readOnly />
<label className="flex items-center gap-1.5 text-xs cursor-pointer"><Checkbox checked={sel.valid} onCheckedChange={(c) => patchSel({ valid: !!c })} /> Valid</label>
<label className="flex items-center gap-1.5 text-xs cursor-pointer"><Checkbox checked={sel.valid} onCheckedChange={(c) => patchSel({ valid: !!c })} /> {t('awed.valid')}</label>
<div className="flex-1" />
<button className="text-muted-foreground hover:text-destructive" onClick={() => delRef(sel.code)}><Trash2 className="size-4" /></button>
</div>
<Field2 label="Description"><Input className="h-8" value={sel.name ?? ''} onChange={(e) => patchSel({ name: e.target.value })} /></Field2>
<Field2 label={t('awed.description')}><Input className="h-8" value={sel.name ?? ''} onChange={(e) => patchSel({ name: e.target.value })} /></Field2>
<div className="grid grid-cols-2 gap-3">
<Field2 label="Group"><Input className="h-8" value={sel.group ?? ''} onChange={(e) => patchSel({ group: e.target.value })} /></Field2>
<Field2 label="Subgroup"><Input className="h-8" value={sel.subgrp ?? ''} onChange={(e) => patchSel({ subgrp: e.target.value })} /></Field2>
<Field2 label={t('awed.group')}><Input className="h-8" value={sel.group ?? ''} onChange={(e) => patchSel({ group: e.target.value })} /></Field2>
<Field2 label={t('awed.subgroup')}><Input className="h-8" value={sel.subgrp ?? ''} onChange={(e) => patchSel({ subgrp: e.target.value })} /></Field2>
</div>
<Field2 label="DXCC"><Input type="number" className="h-8 w-32 font-mono" value={sel.dxcc || ''} onChange={(e) => patchSel({ dxcc: parseInt(e.target.value, 10) || 0 })} /></Field2>
<Field2 label="Pattern (regex)"><Input className="h-8 font-mono text-xs" value={sel.pattern ?? ''} onChange={(e) => patchSel({ pattern: e.target.value })} placeholder="optional per-reference regex" /></Field2>
<Field2 label={t('awed.patternRegex')}><Input className="h-8 font-mono text-xs" value={sel.pattern ?? ''} onChange={(e) => patchSel({ pattern: e.target.value })} placeholder={t('awed.perRefRegex')} /></Field2>
<div className="grid grid-cols-3 gap-3">
<div className="flex flex-col gap-1 min-w-0">
<Label className="text-xs text-muted-foreground">Score</Label>
<Label className="text-xs text-muted-foreground">{t('awed.score')}</Label>
<Input type="number" className="h-8 font-mono w-full" value={sel.score ?? 0} onChange={(e) => patchSel({ score: parseInt(e.target.value, 10) || 0 })} />
</div>
<div className="flex flex-col gap-1 min-w-0">
<Label className="text-xs text-muted-foreground">Bonus</Label>
<Label className="text-xs text-muted-foreground">{t('awed.bonus')}</Label>
<Input type="number" className="h-8 font-mono w-full" value={sel.bonus ?? 0} onChange={(e) => patchSel({ bonus: parseInt(e.target.value, 10) || 0 })} />
</div>
<div className="flex flex-col gap-1 min-w-0">
<Label className="text-xs text-muted-foreground">Grid</Label>
<Label className="text-xs text-muted-foreground">{t('awed.grid')}</Label>
<Input className="h-8 font-mono w-full" value={sel.gridsquare ?? ''} onChange={(e) => patchSel({ gridsquare: e.target.value })} />
</div>
</div>
<div className="flex justify-end pt-1"><Button size="sm" className="h-7" onClick={() => sel && saveRef(sel)}><Save className="size-3.5 mr-1" /> Save reference</Button></div>
<div className="flex justify-end pt-1"><Button size="sm" className="h-7" onClick={() => sel && saveRef(sel)}><Save className="size-3.5 mr-1" /> {t('awed.saveReference')}</Button></div>
</div>
)}
</div>