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
+43 -41
View File
@@ -13,6 +13,7 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Badge } from '@/components/ui/badge';
import { QSOEditModal } from '@/components/QSOEditModal';
import { useI18n } from '@/lib/i18n';
import type { QSOForm } from '@/types';
import {
NetList, NetCreate, NetRename, NetDelete, NetOpen, NetClose, NetOpenID,
@@ -66,6 +67,7 @@ type Props = {
};
export function NetControlPanel({ onLogged, countries, bands, modes }: Props) {
const { t } = useI18n();
const [nets, setNets] = useState<Net[]>([]);
const [selId, setSelId] = useState<string>('');
const [openId, setOpenId] = useState<string>('');
@@ -123,7 +125,7 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) {
);
async function newNet() {
const name = window.prompt('New NET name:');
const name = window.prompt(t('ncp.newNetPrompt'));
if (!name?.trim()) return;
try { const n = await NetCreate(name.trim()); await refreshNets(); setSelId(n.id); }
catch (e: any) { setError(String(e?.message ?? e)); }
@@ -131,7 +133,7 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) {
async function renameNet() {
if (!selId) return;
const cur = nets.find((n) => n.id === selId);
const name = window.prompt('Rename NET:', cur?.name ?? '');
const name = window.prompt(t('ncp.renamePrompt'), cur?.name ?? '');
if (!name?.trim()) return;
try { await NetRename(selId, name.trim()); await refreshNets(); }
catch (e: any) { setError(String(e?.message ?? e)); }
@@ -139,7 +141,7 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) {
async function deleteNet() {
if (!selId) return;
const cur = nets.find((n) => n.id === selId);
if (!window.confirm(`Delete NET "${cur?.name}" and its roster? This cannot be undone.`)) return;
if (!window.confirm(t('ncp.deleteConfirm', { name: cur?.name ?? '' }))) return;
try { await NetDelete(selId); setSelId(''); await refreshNets(); }
catch (e: any) { setError(String(e?.message ?? e)); }
}
@@ -147,7 +149,7 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) {
try {
if (isOpen) {
if (active.length > 0 &&
!window.confirm(`${active.length} station(s) still on the air will be dropped WITHOUT logging. Close anyway?`)) return;
!window.confirm(t('ncp.closeConfirm', { n: active.length }))) return;
await NetClose(); setOpenId('');
} else {
await NetOpen(selId); setOpenId(selId); await refreshActive();
@@ -206,7 +208,7 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) {
async function removeSelectedRoster() {
const sel = (rosterGrid.current?.api?.getSelectedRows() ?? []) as Station[];
if (sel.length === 0) return;
if (!window.confirm(`Remove ${sel.length} station(s) from this NET's roster?`)) return;
if (!window.confirm(t('ncp.removeConfirm', { n: sel.length }))) return;
try {
for (const s of sel) await NetRosterRemove(selId, s.callsign);
await refreshRoster(selId);
@@ -214,23 +216,23 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) {
}
const activeCols = useMemo<ColDef<QSOForm>[]>(() => [
{ headerName: 'Callsign', field: 'callsign', width: 110, cellClass: 'font-mono font-semibold' },
{ headerName: 'Name', field: 'name', flex: 1 },
{ headerName: t('ncp.colCallsign'), field: 'callsign', width: 110, cellClass: 'font-mono font-semibold' },
{ headerName: t('ncp.colName'), field: 'name', flex: 1 },
{ headerName: 'QTH', field: 'qth', flex: 1 },
{ headerName: 'Time on', valueGetter: (p) => fmtTimeOn((p.data as any)?.qso_date), width: 90, cellClass: 'font-mono text-[11px]' },
{ headerName: 'Band', field: 'band', width: 70, cellClass: 'font-mono' },
{ headerName: 'Mode', field: 'mode', width: 70, cellClass: 'font-mono' },
{ headerName: t('ncp.colTimeOn'), valueGetter: (p) => fmtTimeOn((p.data as any)?.qso_date), width: 90, cellClass: 'font-mono text-[11px]' },
{ headerName: t('ncp.colBand'), field: 'band', width: 70, cellClass: 'font-mono' },
{ headerName: t('ncp.colMode'), field: 'mode', width: 70, cellClass: 'font-mono' },
{ headerName: 'RST S', field: 'rst_sent', width: 70, cellClass: 'font-mono' },
{ headerName: 'RST R', field: 'rst_rcvd', width: 70, cellClass: 'font-mono' },
{ headerName: 'Comment', field: 'comment', flex: 1.5 },
], []);
{ headerName: t('ncp.colComment'), field: 'comment', flex: 1.5 },
], [t]);
const rosterCols = useMemo<ColDef<Station>[]>(() => [
{ headerName: 'Callsign', field: 'callsign', width: 110, cellClass: 'font-mono font-semibold' },
{ headerName: 'Name', field: 'name', flex: 1 },
{ headerName: t('ncp.colCallsign'), field: 'callsign', width: 110, cellClass: 'font-mono font-semibold' },
{ headerName: t('ncp.colName'), field: 'name', flex: 1 },
{ headerName: 'QTH', field: 'qth', flex: 1 },
{ headerName: 'Country', field: 'country', width: 130 },
], []);
{ headerName: t('ncp.colCountry'), field: 'country', width: 130 },
], [t]);
const defaultColDef = useMemo<ColDef>(() => ({ sortable: true, resizable: true, suppressMovable: false }), []);
@@ -239,31 +241,31 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) {
{/* Toolbar */}
<div className="flex items-center gap-2 px-3 py-2 border-b border-border/60 bg-muted/30">
<Button variant="outline" size="sm" className="h-8" onClick={newNet}>
<Plus className="size-3.5" /> New NET
<Plus className="size-3.5" /> {t('ncp.newNet')}
</Button>
<select
className="h-8 rounded-md border border-input bg-background px-2 text-sm min-w-[180px]"
value={selId}
disabled={isOpen}
onChange={(e) => setSelId(e.target.value)}
title={isOpen ? 'Close the NET to switch' : 'Select a NET'}
title={isOpen ? t('ncp.closeToSwitch') : t('ncp.selectNetTitle')}
>
<option value=""> select a NET </option>
<option value="">{t('ncp.selectNetOption')}</option>
{nets.map((n) => <option key={n.id} value={n.id}>{n.name}</option>)}
</select>
<Button variant={isOpen ? 'destructive' : 'default'} size="sm" className="h-8" disabled={!selId} onClick={toggleOpen}>
<Radio className="size-3.5" /> {isOpen ? 'Close NET' : 'Open NET'}
<Radio className="size-3.5" /> {isOpen ? t('ncp.closeNet') : t('ncp.openNet')}
</Button>
<Button variant="ghost" size="sm" className="h-8" disabled={!selId || isOpen} onClick={renameNet}>Rename</Button>
<Button variant="ghost" size="sm" className="h-8" disabled={!selId || isOpen} onClick={renameNet}>{t('ncp.rename')}</Button>
<Button variant="ghost" size="sm" className="h-8 text-rose-700" disabled={!selId || isOpen} onClick={deleteNet}>
<Trash2 className="size-3.5" /> Delete
<Trash2 className="size-3.5" /> {t('ncp.delete')}
</Button>
<div className="flex-1" />
{isOpen && <Badge className="bg-emerald-600 text-white tracking-wider">NET OPEN</Badge>}
{isOpen && <Badge className="bg-emerald-600 text-white tracking-wider">{t('ncp.netOpenBadge')}</Badge>}
<span className="text-[11px] text-muted-foreground">
On air: <strong className="text-foreground">{active.length}</strong> ·
Roster: <strong className="text-foreground">{roster.length}</strong>
{t('ncp.onAir')} <strong className="text-foreground">{active.length}</strong> ·
{t('ncp.roster')} <strong className="text-foreground">{roster.length}</strong>
</span>
{error && <Badge variant="destructive" className="max-w-[280px] truncate" title={error}>{error}</Badge>}
</div>
@@ -272,8 +274,8 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) {
{/* ACTIVE USERS (left) */}
<div className="flex flex-col min-h-0 flex-1 border-r border-border/60">
<div className="flex items-center gap-2 px-3 py-1.5 bg-red-600 text-white text-[11px] font-semibold uppercase tracking-wider">
On air active QSOs
<span className="ml-auto font-normal normal-case opacity-90">double-click edit all fields · "Log &amp; end" to save</span>
{t('ncp.onAirActive')}
<span className="ml-auto font-normal normal-case opacity-90">{t('ncp.activeHint')}</span>
</div>
<div style={{ flex: 1, minHeight: 0, position: 'relative' }}>
<div style={{ position: 'absolute', inset: 0 }}>
@@ -294,7 +296,7 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) {
<div className="px-3 py-1.5 border-t border-border/60 bg-muted/30 flex items-center gap-2">
<Button variant="ghost" size="sm" className="h-7 text-[11px]"
onClick={() => { const r = activeGrid.current?.api?.getSelectedRows?.()?.[0] as QSOForm; if (r) deactivate(r.id as number); }}>
<MinusCircle className="size-3.5" /> Log &amp; end selected
<MinusCircle className="size-3.5" /> {t('ncp.logEndSelected')}
</Button>
</div>
)}
@@ -303,8 +305,8 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) {
{/* NET USERS / roster (right) */}
<div className="flex flex-col min-h-0 w-[40%] max-w-[560px]">
<div className="flex items-center gap-2 px-3 py-1.5 bg-emerald-700 text-white text-[11px] font-semibold uppercase tracking-wider">
NET users roster
<span className="ml-auto font-normal normal-case opacity-90">double-click put on air</span>
{t('ncp.netUsersRoster')}
<span className="ml-auto font-normal normal-case opacity-90">{t('ncp.rosterHint')}</span>
</div>
<div style={{ flex: 1, minHeight: 0, position: 'relative' }}>
<div style={{ position: 'absolute', inset: 0 }}>
@@ -323,15 +325,15 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) {
</div>
<div className="px-3 py-1.5 border-t border-border/60 bg-muted/30 flex items-center gap-2">
<Button variant="ghost" size="sm" className="h-7 text-[11px]" disabled={!selId} onClick={openAddContact}>
<UserPlus className="size-3.5" /> Add contact
<UserPlus className="size-3.5" /> {t('ncp.addContact')}
</Button>
<Button variant="ghost" size="sm" className="h-7 text-[11px] text-rose-700" disabled={!selId} onClick={removeSelectedRoster}>
<MinusCircle className="size-3.5" /> Remove
<MinusCircle className="size-3.5" /> {t('ncp.remove')}
</Button>
{isOpen && (
<Button variant="ghost" size="sm" className="h-7 text-[11px] ml-auto"
onClick={() => { const r = rosterGrid.current?.api?.getSelectedRows?.()?.[0] as Station; if (r) activate(r.callsign); }}>
<PlusCircle className="size-3.5" /> Put selected on air
<PlusCircle className="size-3.5" /> {t('ncp.putOnAir')}
</Button>
)}
</div>
@@ -356,23 +358,23 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) {
<Dialog open={contactOpen} onOpenChange={setContactOpen}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle>Add contact to NET</DialogTitle>
<DialogDescription>Saved in this NET's roster (reused next time you open it).</DialogDescription>
<DialogTitle>{t('ncp.addContactTitle')}</DialogTitle>
<DialogDescription>{t('ncp.addContactDesc')}</DialogDescription>
</DialogHeader>
<div className="grid gap-3 px-5 py-2">
<div className="flex items-end gap-2">
<div className="flex-1">
<Label className="text-[11px]">Callsign</Label>
<Label className="text-[11px]">{t('ncp.callsign')}</Label>
<Input className="font-mono uppercase" value={contact.callsign ?? ''}
onChange={(e) => setContact((c) => netctl.Station.createFrom({ ...c, callsign: e.target.value.toUpperCase() }))}
onKeyDown={(e) => { if (e.key === 'Enter') lookupContact(); }} />
</div>
<Button variant="outline" size="sm" className="h-9" onClick={lookupContact} disabled={looking || !(contact.callsign ?? '').trim()}>
<Search className="size-3.5" /> {looking ? '' : 'Search'}
<Search className="size-3.5" /> {looking ? '…' : t('ncp.search')}
</Button>
</div>
<div>
<Label className="text-[11px]">Name</Label>
<Label className="text-[11px]">{t('ncp.name')}</Label>
<Input value={contact.name ?? ''} onChange={(e) => setContact((c) => netctl.Station.createFrom({ ...c, name: e.target.value }))} />
</div>
<div>
@@ -380,14 +382,14 @@ export function NetControlPanel({ onLogged, countries, bands, modes }: Props) {
<Input value={contact.qth ?? ''} onChange={(e) => setContact((c) => netctl.Station.createFrom({ ...c, qth: e.target.value }))} />
</div>
<div>
<Label className="text-[11px]">Country</Label>
<Label className="text-[11px]">{t('ncp.country')}</Label>
<Input value={contact.country ?? ''} onChange={(e) => setContact((c) => netctl.Station.createFrom({ ...c, country: e.target.value }))} />
</div>
</div>
<DialogFooter>
<Button variant="ghost" size="sm" onClick={() => setContactOpen(false)}>Cancel</Button>
<Button variant="ghost" size="sm" onClick={() => setContactOpen(false)}>{t('ncp.cancel')}</Button>
<Button size="sm" onClick={saveContact} disabled={!(contact.callsign ?? '').trim()}>
<PlusCircle className="size-3.5" /> Save in NET
<PlusCircle className="size-3.5" /> {t('ncp.saveInNet')}
</Button>
</DialogFooter>
</DialogContent>