feat: Complete translation in French
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
FlexSetCWSidetone, FlexSetSidetoneLevel, FlexSetCWFilter, FlexSetFilter,
|
||||
} from '../../wailsjs/go/main/App';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
|
||||
type FlexState = {
|
||||
available: boolean; model?: string;
|
||||
@@ -190,6 +191,7 @@ function Card({ icon: Icon, title, accent, children }: { icon: any; title: strin
|
||||
// onCWSpeed (optional): notified when the operator changes CW speed here, so the
|
||||
// host can keep the WinKeyer (which actually sends the macros) in sync.
|
||||
export function FlexPanel({ onCWSpeed }: { onCWSpeed?: (wpm: number) => void } = {}) {
|
||||
const { t } = useI18n();
|
||||
const [st, setSt] = useState<FlexState>(ZERO);
|
||||
const hold = useRef<Record<string, number>>({});
|
||||
// Peak-hold: keep the highest reading for ~2 s so the jittery VITA-49 meters
|
||||
@@ -264,34 +266,34 @@ export function FlexPanel({ onCWSpeed }: { onCWSpeed?: (wpm: number) => void } =
|
||||
<Radio className="size-6 text-sky-400" />
|
||||
<div className="flex flex-col leading-tight">
|
||||
<span className="text-base font-extrabold tracking-tight">{st.model || 'FlexRadio'}</span>
|
||||
<span className="text-[11px] text-slate-400">SmartSDR remote control</span>
|
||||
<span className="text-[11px] text-slate-400">{t('flxp.smartsdrRemote')}</span>
|
||||
</div>
|
||||
<div className="flex-1" />
|
||||
<span className={cn('inline-flex items-center gap-1.5 px-3 py-1.5 rounded-lg text-sm font-extrabold tracking-wider',
|
||||
!st.available ? 'bg-slate-700 text-slate-300'
|
||||
: st.transmitting ? 'bg-rose-500 text-white shadow-[0_0_16px] shadow-rose-500/60' : 'bg-emerald-500 text-white')}>
|
||||
<span className="size-2 rounded-full bg-current" />
|
||||
{!st.available ? 'OFFLINE' : st.transmitting ? 'TX' : 'RX'}
|
||||
{!st.available ? t('flxp.offline') : st.transmitting ? 'TX' : 'RX'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{off && (
|
||||
<div className="text-center text-sm text-muted-foreground py-6">
|
||||
Waiting for the FlexRadio… (set CAT to FlexRadio and connect)
|
||||
{t('flxp.waiting')}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* TX + RX columns */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
||||
{/* TRANSMIT */}
|
||||
<Card icon={Zap} title="Transmit" accent="#dc2626">
|
||||
<Card icon={Zap} title={t('flxp.transmit')} accent="#dc2626">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-20 shrink-0 text-xs font-medium text-muted-foreground">RF Power</span>
|
||||
<span className="w-20 shrink-0 text-xs font-medium text-muted-foreground">{t('flxp.rfPower')}</span>
|
||||
<Slider value={st.rf_power} disabled={off} accent="#dc2626" onChange={(v) => change('rf_power', v, () => FlexSetPower(v))} />
|
||||
<span className="w-9 text-right text-sm font-mono font-bold tabular-nums">{st.rf_power}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="w-20 shrink-0 text-xs font-medium text-muted-foreground">Tune Pwr</span>
|
||||
<span className="w-20 shrink-0 text-xs font-medium text-muted-foreground">{t('flxp.tunePwr')}</span>
|
||||
<Slider value={st.tune_power} disabled={off} accent="#d97706" onChange={(v) => change('tune_power', v, () => FlexSetTunePower(v))} />
|
||||
<span className="w-9 text-right text-sm font-mono font-bold tabular-nums">{st.tune_power}</span>
|
||||
</div>
|
||||
@@ -311,7 +313,7 @@ export function FlexPanel({ onCWSpeed }: { onCWSpeed?: (wpm: number) => void } =
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<button type="button" disabled={off}
|
||||
title="Split: RX/TX on separate slices. ON creates a TX slice +1 kHz (CW) / +5 kHz (SSB) up, like SmartSDR."
|
||||
title={t('flxp.splitHint')}
|
||||
onClick={() => change('split', !st.split, () => FlexSetSplit(!st.split))}
|
||||
className={cn('px-3 py-1.5 rounded-lg text-sm font-extrabold tracking-wide border-2 transition-all disabled:opacity-30',
|
||||
st.split ? 'bg-sky-600 text-white border-sky-600 shadow-[0_0_12px] shadow-sky-600/50' : 'bg-card text-sky-700 border-sky-400 hover:bg-sky-50')}>
|
||||
@@ -338,7 +340,7 @@ export function FlexPanel({ onCWSpeed }: { onCWSpeed?: (wpm: number) => void } =
|
||||
onToggle={() => change('vox_enable', !st.vox_enable, () => FlexSetVox(!st.vox_enable))}
|
||||
onLevel={(v) => change('vox_level', v, () => FlexSetVoxLevel(v))} />
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-14 shrink-0 text-[11px] font-bold text-muted-foreground pl-0.5">VOX Dly</span>
|
||||
<span className="w-14 shrink-0 text-[11px] font-bold text-muted-foreground pl-0.5">{t('flxp.voxDly')}</span>
|
||||
<Slider value={st.vox_delay} disabled={off || !st.vox_enable} accent="#16a34a"
|
||||
onChange={(v) => change('vox_delay', v, () => FlexSetVoxDelay(v))} />
|
||||
<span className="w-8 text-right text-xs font-mono tabular-nums text-muted-foreground">{st.vox_delay}</span>
|
||||
@@ -356,17 +358,17 @@ export function FlexPanel({ onCWSpeed }: { onCWSpeed?: (wpm: number) => void } =
|
||||
/* CW keyer controls (replace VOX/PROC/MIC when the slice is in CW). */
|
||||
<div className="border-t border-border/60 pt-3 space-y-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-16 shrink-0 text-[11px] font-bold text-muted-foreground">Speed</span>
|
||||
<span className="w-16 shrink-0 text-[11px] font-bold text-muted-foreground">{t('flxp.speed')}</span>
|
||||
<Slider value={st.cw_speed} disabled={off} max={60} accent="#0d9488" onChange={(v) => { change('cw_speed', v, () => FlexSetCWSpeed(v)); onCWSpeed?.(v); }} />
|
||||
<span className="w-12 text-right text-xs font-mono tabular-nums text-muted-foreground">{st.cw_speed} wpm</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-16 shrink-0 text-[11px] font-bold text-muted-foreground">Pitch</span>
|
||||
<span className="w-16 shrink-0 text-[11px] font-bold text-muted-foreground">{t('flxp.pitch')}</span>
|
||||
<Slider value={st.cw_pitch} disabled={off} max={1000} step={10} accent="#7c3aed" onChange={(v) => change('cw_pitch', v, () => FlexSetCWPitch(v))} />
|
||||
<span className="w-12 text-right text-xs font-mono tabular-nums text-muted-foreground">{st.cw_pitch} Hz</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-16 shrink-0 text-[11px] font-bold text-muted-foreground">Delay</span>
|
||||
<span className="w-16 shrink-0 text-[11px] font-bold text-muted-foreground">{t('flxp.delay')}</span>
|
||||
<Slider value={st.cw_break_in_delay} disabled={off} max={1000} step={1} accent="#d97706" onChange={(v) => change('cw_break_in_delay', v, () => FlexSetCWBreakInDelay(v))} />
|
||||
<span className="w-12 text-right text-xs font-mono tabular-nums text-muted-foreground">{st.cw_break_in_delay} ms</span>
|
||||
</div>
|
||||
@@ -378,7 +380,7 @@ export function FlexPanel({ onCWSpeed }: { onCWSpeed?: (wpm: number) => void } =
|
||||
</Card>
|
||||
|
||||
{/* RECEIVE */}
|
||||
<Card icon={AudioLines} title="Receive (active slice)" accent="#0891b2">
|
||||
<Card icon={AudioLines} title={t('flxp.receiveActive')} accent="#0891b2">
|
||||
{((st.ant_list?.length ?? 0) > 0 || (st.tx_ant_list?.length ?? 0) > 0) && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-14 shrink-0 text-[11px] font-bold text-muted-foreground">Ant</span>
|
||||
@@ -406,7 +408,7 @@ export function FlexPanel({ onCWSpeed }: { onCWSpeed?: (wpm: number) => void } =
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-14 shrink-0 text-[11px] font-bold text-muted-foreground">AF</span>
|
||||
<button type="button" disabled={rxOff}
|
||||
title={st.mute ? 'Muted — click to unmute' : 'Mute RX audio'}
|
||||
title={st.mute ? t('flxp.muted') : t('flxp.mute')}
|
||||
onClick={() => change('mute', !st.mute, () => FlexSetMute(!st.mute))}
|
||||
className={cn('shrink-0 rounded p-1 transition-colors disabled:opacity-30',
|
||||
st.mute ? 'bg-red-600 text-white' : 'text-muted-foreground hover:bg-muted')}>
|
||||
@@ -440,7 +442,7 @@ export function FlexPanel({ onCWSpeed }: { onCWSpeed?: (wpm: number) => void } =
|
||||
onToggle={() => change('apf', !st.apf, () => FlexSetAPF(!st.apf))}
|
||||
onLevel={(v) => change('apf_level', v, () => FlexSetAPFLevel(v))} />
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-14 shrink-0 text-[11px] font-bold text-muted-foreground">Filter</span>
|
||||
<span className="w-14 shrink-0 text-[11px] font-bold text-muted-foreground">{t('flxp.filter')}</span>
|
||||
<div className="inline-flex rounded-md border border-border overflow-hidden">
|
||||
{CW_BW.map((bw) => (
|
||||
<button key={bw} type="button" disabled={rxOff}
|
||||
@@ -458,7 +460,7 @@ export function FlexPanel({ onCWSpeed }: { onCWSpeed?: (wpm: number) => void } =
|
||||
{!isCW && (
|
||||
<div className="border-t border-border/60 pt-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="w-14 shrink-0 text-[11px] font-bold text-muted-foreground">Filter</span>
|
||||
<span className="w-14 shrink-0 text-[11px] font-bold text-muted-foreground">{t('flxp.filter')}</span>
|
||||
<div className="inline-flex rounded-md border border-border overflow-hidden">
|
||||
{SSB_BW.map((bw) => (
|
||||
<button key={bw} type="button" disabled={rxOff}
|
||||
@@ -485,7 +487,7 @@ export function FlexPanel({ onCWSpeed }: { onCWSpeed?: (wpm: number) => void } =
|
||||
|
||||
{/* External amplifier (PowerGenius XL) — only when detected. */}
|
||||
{st.amp_available && (
|
||||
<Card icon={Flame} title={`Amplifier${st.amp_model ? ' · ' + st.amp_model : ''}`} accent="#ea580c">
|
||||
<Card icon={Flame} title={`${t('flxp.amplifier')}${st.amp_model ? ' · ' + st.amp_model : ''}`} accent="#ea580c">
|
||||
<div className="flex items-center gap-3">
|
||||
<button type="button" disabled={off}
|
||||
onClick={() => change('amp_operate', !st.amp_operate, () => FlexAmpOperate(!st.amp_operate))}
|
||||
@@ -494,41 +496,41 @@ export function FlexPanel({ onCWSpeed }: { onCWSpeed?: (wpm: number) => void } =
|
||||
{st.amp_operate ? 'OPERATE' : 'STANDBY'}
|
||||
</button>
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{st.amp_operate ? 'Amplifier is in line (transmitting through PA).' : 'Amplifier bypassed (standby).'}
|
||||
{st.amp_operate ? t('flxp.ampInLine') : t('flxp.ampBypassed')}
|
||||
</span>
|
||||
{/* Fan mode — shown when the PowerGenius is configured (Settings →
|
||||
PowerGenius). The dot shows the direct-connection state; the
|
||||
selector is disabled until connected (hover it for the error). */}
|
||||
{(pg.host || pg.connected) && (
|
||||
<label className="flex items-center gap-1.5 text-xs" title={pg.connected ? 'PowerGenius connected' : (pg.last_error || 'PowerGenius offline')}>
|
||||
<label className="flex items-center gap-1.5 text-xs" title={pg.connected ? t('flxp.pgConnected') : (pg.last_error || t('flxp.pgOffline'))}>
|
||||
<span className={cn('size-1.5 rounded-full', pg.connected ? 'bg-emerald-500 shadow-[0_0_6px_rgba(16,185,129,0.8)]' : 'bg-rose-500')} />
|
||||
<span className="text-muted-foreground">Fan</span>
|
||||
<span className="text-muted-foreground">{t('flxp.fan')}</span>
|
||||
<select
|
||||
disabled={!pg.connected}
|
||||
value={(pg.fan_mode || 'CONTEST').toUpperCase()}
|
||||
onChange={(e) => { const v = e.target.value; setPg((s) => ({ ...s, fan_mode: v })); PGXLSetFanMode(v).catch(() => {}); }}
|
||||
className="h-8 rounded-md border border-orange-300 bg-card px-2 text-xs font-semibold text-orange-800 outline-none focus:border-orange-500 disabled:opacity-40"
|
||||
>
|
||||
<option value="STANDARD">Standard</option>
|
||||
<option value="CONTEST">Contest</option>
|
||||
<option value="BROADCAST">Broadcast</option>
|
||||
<option value="STANDARD">{t('flxp.fanStandard')}</option>
|
||||
<option value="CONTEST">{t('flxp.fanContest')}</option>
|
||||
<option value="BROADCAST">{t('flxp.fanBroadcast')}</option>
|
||||
</select>
|
||||
</label>
|
||||
)}
|
||||
<div className="flex-1" />
|
||||
{st.amp_fault && st.amp_fault !== 'NONE' && (
|
||||
<span className="px-2 py-1 rounded bg-rose-100 text-rose-800 text-xs font-bold">FAULT: {st.amp_fault}</span>
|
||||
<span className="px-2 py-1 rounded bg-rose-100 text-rose-800 text-xs font-bold">{t('flxp.fault')}: {st.amp_fault}</span>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{/* Live meters (UDP VITA-49 stream) */}
|
||||
<Card icon={Gauge} title="Meters">
|
||||
<Card icon={Gauge} title={t('flxp.meters')}>
|
||||
{(() => {
|
||||
const meters = st.meters || [];
|
||||
if (off || meters.length === 0) {
|
||||
return <p className="text-[11px] text-muted-foreground text-center py-2">No meters yet — waiting for the radio's UDP stream…</p>;
|
||||
return <p className="text-[11px] text-muted-foreground text-center py-2">{t('flxp.noMeters')}</p>;
|
||||
}
|
||||
const isDbm = (m?: Meter) => !!m && /dbm/i.test(m.unit || '');
|
||||
const dbmToW = (d: number) => Math.pow(10, (d - 30) / 10);
|
||||
@@ -568,7 +570,7 @@ export function FlexPanel({ onCWSpeed }: { onCWSpeed?: (wpm: number) => void } =
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">{cur}</div>
|
||||
{amp.length > 0 && (
|
||||
<div className="border-t border-border/60 pt-2 mt-1">
|
||||
<div className="text-[10px] font-bold tracking-wider text-muted-foreground mb-1.5">AMPLIFIER</div>
|
||||
<div className="text-[10px] font-bold tracking-wider text-muted-foreground mb-1.5">{t('flxp.amplifierHdr')}</div>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
{amp.map((m) => {
|
||||
if (/fwd|pwr/i.test(m.name || '') && isDbm(m)) {
|
||||
|
||||
Reference in New Issue
Block a user