i18n: translate the CW Keyer settings panel + drop the serial blurb

The whole "Manipulateur CW" settings panel was hardcoded in English, so it
stayed English even with the app in French. Added ~35 wk.* keys (EN + FR) and
wired every label/checkbox/select/placeholder through t(). Also removed the long
serial-engine explanation paragraph as requested.
This commit is contained in:
2026-07-23 22:14:06 +02:00
parent aa871a07b7
commit 7b0ef0ba97
3 changed files with 79 additions and 54 deletions
+47 -54
View File
@@ -3021,47 +3021,45 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
<div className="space-y-4 max-w-2xl">
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox checked={wk.enabled} onCheckedChange={(c) => setWkField({ enabled: !!c })} />
Enable CW keyer (shows the keyer panel)
{t('wk.enable')}
</label>
<div className="grid grid-cols-4 gap-3 items-end">
<div className="space-y-1">
<Label>Keyer engine</Label>
<Label>{t('wk.engine')}</Label>
{/* Picking the serial engine defaults PTT-keying ON: without RTS held,
the rig drops to RX between words (semi-break-in). */}
<Select value={wk.engine} onValueChange={(v) => setWkField(v === 'serial' ? { engine: v, use_ptt: true } : { engine: v })}>
<SelectTrigger className="h-8"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="winkeyer">WinKeyer (K1EL, serial)</SelectItem>
<SelectItem value="serial">Serial port (DTR=CW / RTS=PTT)</SelectItem>
<SelectItem value="icom">Icom CI-V (rig keyer)</SelectItem>
<SelectItem value="flex">FlexRadio (CWX)</SelectItem>
<SelectItem value="tci" disabled>TCI (coming soon)</SelectItem>
<SelectItem value="winkeyer">{t('wk.engWinkeyer')}</SelectItem>
<SelectItem value="serial">{t('wk.engSerial')}</SelectItem>
<SelectItem value="icom">{t('wk.engIcom')}</SelectItem>
<SelectItem value="flex">{t('wk.engFlex')}</SelectItem>
<SelectItem value="tci" disabled>{t('wk.engTci')}</SelectItem>
</SelectContent>
</Select>
</div>
<label className="flex items-center gap-2 text-sm cursor-pointer col-span-3 pb-1.5">
<Checkbox checked={wk.esc_clears_call} onCheckedChange={(c) => setWkField({ esc_clears_call: !!c })} />
ESC clears the callsign too (otherwise ESC only stops transmission)
{t('wk.escClears')}
</label>
</div>
{wk.engine === 'icom' ? (
<>
<p className="text-xs text-muted-foreground -mt-2">
Icom CI-V keys CW through the rig's own keyer over the existing CAT connection (command 0x17) — it reuses the CAT COM port set in Settings → CAT, so there's nothing else to wire up here. Put the rig in CW mode. Weight, ratio, sidetone, paddle mode are configured on the radio; only the speed is set from here (the rig's KEY SPEED).
{t('wk.icomNote')}
</p>
{(!catCfg.enabled || catCfg.backend !== 'icom') && (
<p className="text-xs font-medium text-warning -mt-1 flex items-start gap-1.5">
<span aria-hidden></span>
<span>
Your CAT backend is set to <strong>{catCfg.enabled ? (catCfg.backend || 'none') : 'disabled'}</strong>. Icom CI-V CW needs the CAT backend set to <strong>Icom</strong> and connected — change it under Settings → CAT interface, otherwise sending CW will fail.
</span>
<span>{t('wk.catWarnIcom', { backend: catCfg.enabled ? (catCfg.backend || 'none') : 'disabled' })}</span>
</p>
)}
<div className="grid grid-cols-4 gap-3">
<div className="space-y-1">
<Label>Speed (WPM)</Label>
<Label>{t('wk.speed')}</Label>
<Input type="number" min={6} max={48} value={wk.wpm} onChange={(e) => setWkField({ wpm: num(e.target.value, 25) })} className="font-mono" />
</div>
</div>
@@ -3069,103 +3067,98 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
) : wk.engine === 'flex' ? (
<>
<p className="text-xs text-muted-foreground -mt-2">
FlexRadio keys CW through the radio's <strong>CWX</strong> keyer over the existing SmartSDR CAT connection no WinKeyer or SmartCAT needed. It reuses the connection set in Settings CAT, so there's nothing else to wire up here. Put a slice in CW mode. Only the speed is set from here; weight, sidetone and break-in are configured on the radio (break-in must be on for CW to actually transmit).
{t('wk.flexNote')}
</p>
{(!catCfg.enabled || catCfg.backend !== 'flex') && (
<p className="text-xs font-medium text-warning -mt-1 flex items-start gap-1.5">
<span aria-hidden></span>
<span>
Your CAT backend is set to <strong>{catCfg.enabled ? (catCfg.backend || 'none') : 'disabled'}</strong>. Flex CWX needs the CAT backend set to <strong>FlexRadio</strong> and connected — change it under Settings → CAT interface, otherwise sending CW will fail.
</span>
<span>{t('wk.catWarnFlex', { backend: catCfg.enabled ? (catCfg.backend || 'none') : 'disabled' })}</span>
</p>
)}
<div className="grid grid-cols-4 gap-3">
<div className="space-y-1">
<Label>Speed (WPM)</Label>
<Label>{t('wk.speed')}</Label>
<Input type="number" min={6} max={48} value={wk.wpm} onChange={(e) => setWkField({ wpm: num(e.target.value, 25) })} className="font-mono" />
</div>
</div>
</>
) : wk.engine === 'serial' ? (
<>
<p className="text-xs text-muted-foreground -mt-2">
OpsLog keys the Morse itself by toggling a serial control line — the "hardware CW keying" a Yaesu <strong>SCU-17</strong> and generic interfaces use (like N1MM: DTR = CW, RTS = PTT). No WinKeyer chip. Put the rig in CW mode and pick the interface's <strong>keying COM port</strong> below (this is the CW/PTT port, <em>not</em> the CAT port). Speed, Farnsworth, lead-in/tail and PTT apply; paddle mode, ratio and sidetone are WinKeyer-only.
</p>
<div className="grid grid-cols-4 gap-3">
<div className="space-y-1 col-span-2">
<Label>Keying COM port</Label>
<Label>{t('wk.keyingPort')}</Label>
<div className="flex items-center gap-2">
<Select value={wk.port || '_'} onValueChange={(v) => setWkField({ port: v === '_' ? '' : v })}>
<SelectTrigger className="h-8 flex-1"><SelectValue placeholder="— COM port —" /></SelectTrigger>
<SelectTrigger className="h-8 flex-1"><SelectValue placeholder={t('wk.comPort')} /></SelectTrigger>
<SelectContent>
{wkPorts.length === 0 && <SelectItem value="_" disabled>No ports found</SelectItem>}
{wkPorts.length === 0 && <SelectItem value="_" disabled>{t('wk.noPorts')}</SelectItem>}
{wkPorts.map((p) => <SelectItem key={p} value={p}>{p}</SelectItem>)}
</SelectContent>
</Select>
<Button variant="ghost" size="sm" className="h-8 px-2" title="Reload ports"
<Button variant="ghost" size="sm" className="h-8 px-2" title={t('wk.reloadPorts')}
onClick={() => ListSerialPorts().then((p) => setWkPorts((p ?? []) as string[])).catch(() => {})}>
<ArrowDown className="size-3.5 rotate-90" />
</Button>
</div>
</div>
<div className="space-y-1">
<Label>CW key line</Label>
<Label>{t('wk.cwKeyLine')}</Label>
<Select value={wk.cw_key_line === 'rts' ? 'rts' : 'dtr'} onValueChange={(v) => setWkField({ cw_key_line: v })}>
<SelectTrigger className="h-8"><SelectValue /></SelectTrigger>
<SelectContent>
<SelectItem value="dtr">DTR = CW, RTS = PTT</SelectItem>
<SelectItem value="rts">RTS = CW, DTR = PTT</SelectItem>
<SelectItem value="dtr">{t('wk.lineDtr')}</SelectItem>
<SelectItem value="rts">{t('wk.lineRts')}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label>Speed (WPM)</Label>
<Label>{t('wk.speed')}</Label>
<Input type="number" min={5} max={99} value={wk.wpm} onChange={(e) => setWkField({ wpm: num(e.target.value, 25) })} className="font-mono" />
</div>
</div>
<div className="grid grid-cols-4 gap-3">
<div className="space-y-1">
<Label>Farnsworth</Label>
<Label>{t('wk.farnsworth')}</Label>
<Input type="number" min={0} max={99} value={wk.farnsworth} onChange={(e) => setWkField({ farnsworth: num(e.target.value, 0) })} className="font-mono" />
</div>
<div className="space-y-1">
<Label>Lead-in (ms)</Label>
<Label>{t('wk.leadIn')}</Label>
<Input type="number" min={0} value={wk.lead_in_ms} onChange={(e) => setWkField({ lead_in_ms: num(e.target.value, 10) })} className="font-mono" />
</div>
<div className="space-y-1">
<Label>Tail (ms)</Label>
<Label>{t('wk.tail')}</Label>
<Input type="number" min={0} value={wk.tail_ms} onChange={(e) => setWkField({ tail_ms: num(e.target.value, 50) })} className="font-mono" />
</div>
<label className="flex items-center gap-2 text-sm cursor-pointer pb-1.5 self-end">
<Checkbox checked={wk.use_ptt} onCheckedChange={(c) => setWkField({ use_ptt: !!c })} /> Key PTT line
<Checkbox checked={wk.use_ptt} onCheckedChange={(c) => setWkField({ use_ptt: !!c })} /> {t('wk.keyPtt')}
</label>
</div>
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox checked={wk.cw_invert} onCheckedChange={(c) => setWkField({ cw_invert: !!c })} />
Invert keying (active-LOW) tick this only if the rig transmits at rest / keys backwards
{t('wk.invert')}
</label>
</>
) : (
<>
<div className="grid grid-cols-4 gap-3">
<div className="space-y-1 col-span-2">
<Label>Serial port</Label>
<Label>{t('wk.serialPort')}</Label>
<div className="flex items-center gap-2">
<Select value={wk.port || '_'} onValueChange={(v) => setWkField({ port: v === '_' ? '' : v })}>
<SelectTrigger className="h-8 flex-1"><SelectValue placeholder="— COM port —" /></SelectTrigger>
<SelectTrigger className="h-8 flex-1"><SelectValue placeholder={t('wk.comPort')} /></SelectTrigger>
<SelectContent>
{wkPorts.length === 0 && <SelectItem value="_" disabled>No ports found</SelectItem>}
{wkPorts.length === 0 && <SelectItem value="_" disabled>{t('wk.noPorts')}</SelectItem>}
{wkPorts.map((p) => <SelectItem key={p} value={p}>{p}</SelectItem>)}
</SelectContent>
</Select>
<Button variant="ghost" size="sm" className="h-8 px-2" title="Reload ports"
<Button variant="ghost" size="sm" className="h-8 px-2" title={t('wk.reloadPorts')}
onClick={() => ListSerialPorts().then((p) => setWkPorts((p ?? []) as string[])).catch(() => {})}>
<ArrowDown className="size-3.5 rotate-90" />
</Button>
</div>
</div>
<div className="space-y-1">
<Label>Baud</Label>
<Label>{t('wk.baud')}</Label>
<Select value={String(wk.baud)} onValueChange={(v) => setWkField({ baud: num(v, 1200) })}>
<SelectTrigger className="h-8"><SelectValue /></SelectTrigger>
<SelectContent>
@@ -3174,38 +3167,38 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
</Select>
</div>
<div className="space-y-1">
<Label>Speed (WPM)</Label>
<Label>{t('wk.speed')}</Label>
<Input type="number" min={5} max={99} value={wk.wpm} onChange={(e) => setWkField({ wpm: num(e.target.value, 25) })} className="font-mono" />
</div>
</div>
<div className="grid grid-cols-4 gap-3">
<div className="space-y-1">
<Label>Weight</Label>
<Label>{t('wk.weight')}</Label>
<Input type="number" min={10} max={90} value={wk.weight} onChange={(e) => setWkField({ weight: num(e.target.value, 50) })} className="font-mono" />
</div>
<div className="space-y-1">
<Label>Lead-in (ms)</Label>
<Label>{t('wk.leadIn')}</Label>
<Input type="number" min={0} value={wk.lead_in_ms} onChange={(e) => setWkField({ lead_in_ms: num(e.target.value, 10) })} className="font-mono" />
</div>
<div className="space-y-1">
<Label>Tail (ms)</Label>
<Label>{t('wk.tail')}</Label>
<Input type="number" min={0} value={wk.tail_ms} onChange={(e) => setWkField({ tail_ms: num(e.target.value, 50) })} className="font-mono" />
</div>
<div className="space-y-1">
<Label>Ratio (33-66)</Label>
<Label>{t('wk.ratio')}</Label>
<Input type="number" min={33} max={66} value={wk.ratio} onChange={(e) => setWkField({ ratio: num(e.target.value, 50) })} className="font-mono" />
</div>
<div className="space-y-1">
<Label>Farnsworth</Label>
<Label>{t('wk.farnsworth')}</Label>
<Input type="number" min={0} max={99} value={wk.farnsworth} onChange={(e) => setWkField({ farnsworth: num(e.target.value, 0) })} className="font-mono" />
</div>
<div className="space-y-1">
<Label>Sidetone (Hz)</Label>
<Label>{t('wk.sidetone')}</Label>
<Input type="number" min={0} value={wk.sidetone_hz} onChange={(e) => setWkField({ sidetone_hz: num(e.target.value, 600) })} className="font-mono" />
</div>
<div className="space-y-1 col-span-2">
<Label>Paddle mode</Label>
<Label>{t('wk.paddleMode')}</Label>
<Select value={wk.mode} onValueChange={(v) => setWkField({ mode: v })}>
<SelectTrigger className="h-8"><SelectValue /></SelectTrigger>
<SelectContent>
@@ -3220,16 +3213,16 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
<div className="flex flex-wrap gap-x-5 gap-y-2">
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox checked={wk.swap} onCheckedChange={(c) => setWkField({ swap: !!c })} /> Swap paddles
<Checkbox checked={wk.swap} onCheckedChange={(c) => setWkField({ swap: !!c })} /> {t('wk.swapPaddles')}
</label>
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox checked={wk.autospace} onCheckedChange={(c) => setWkField({ autospace: !!c })} /> Auto-space
<Checkbox checked={wk.autospace} onCheckedChange={(c) => setWkField({ autospace: !!c })} /> {t('wk.autospace')}
</label>
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox checked={wk.use_ptt} onCheckedChange={(c) => setWkField({ use_ptt: !!c })} /> Key PTT
<Checkbox checked={wk.use_ptt} onCheckedChange={(c) => setWkField({ use_ptt: !!c })} /> {t('wk.keyPttShort')}
</label>
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox checked={wk.serial_echo} onCheckedChange={(c) => setWkField({ serial_echo: !!c })} /> Serial echo
<Checkbox checked={wk.serial_echo} onCheckedChange={(c) => setWkField({ serial_echo: !!c })} /> {t('wk.serialEcho')}
</label>
</div>
</>
@@ -3237,9 +3230,9 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
{/* Macro editor */}
<div className="border-t border-border/60 pt-3">
<Label className="text-sm font-medium">CW message macros (F1)</Label>
<Label className="text-sm font-medium">{t('wk.macroTitle')}</Label>
<p className="text-[11px] text-muted-foreground mb-2">
Use variables: <span className="font-mono">&lt;MY_CALL&gt; &lt;CALL&gt; &lt;STX&gt; &lt;STRX&gt; &lt;MY_NAME&gt; &lt;HIS_NAME&gt; &lt;MY_QTH&gt; &lt;GRID&gt; &lt;CONT_TX&gt; &lt;n&gt;</span> (cut numbers: 9N, 0T). <span className="font-mono">*</span>=my call, <span className="font-mono">!</span>=his call. <span className="font-mono">&lt;LOGQSO&gt;</span> = log the QSO when the macro is sent (e.g. <span className="font-mono">73 TU &lt;LOGQSO&gt;</span>).
{t('wk.macroVars')} <span className="font-mono">&lt;MY_CALL&gt; &lt;CALL&gt; &lt;STX&gt; &lt;STRX&gt; &lt;MY_NAME&gt; &lt;HIS_NAME&gt; &lt;MY_QTH&gt; &lt;GRID&gt; &lt;CONT_TX&gt; &lt;n&gt;</span> {t('wk.macroCut')} <span className="font-mono">*</span>={t('wk.macroMyCall')}, <span className="font-mono">!</span>={t('wk.macroHisCall')}. <span className="font-mono">&lt;LOGQSO&gt;</span> = {t('wk.macroLog')} (<span className="font-mono">73 TU &lt;LOGQSO&gt;</span>).
</p>
<div className="space-y-1.5 max-h-[34vh] overflow-y-auto pr-1">
{Array.from({ length: 12 }).map((_, i) => {
@@ -3247,7 +3240,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
return (
<div key={i} className="flex items-center gap-2">
<span className="font-mono text-[10px] text-primary font-semibold w-6 shrink-0">F{i + 1}</span>
<Input className="h-8 w-28 shrink-0 text-xs" placeholder="Label" value={m.label} onChange={(e) => setMacro(i, { label: e.target.value })} />
<Input className="h-8 w-28 shrink-0 text-xs" placeholder={t('wk.label')} value={m.label} onChange={(e) => setMacro(i, { label: e.target.value })} />
<Input className="h-8 flex-1 font-mono text-xs" placeholder="CQ CQ DE <MY_CALL> K" value={m.text} onChange={(e) => setMacro(i, { text: e.target.value })} />
</div>
);