i18n: translate the External services settings tabs

The External-services panel (HRDLOG, eQSL, LoTW, POTA) and the many input
placeholders across all tabs (QRZ/ClubLog included) were still hardcoded
English. Added ~40 es.* keys (EN + FR) and wired every label, placeholder,
hint, checkbox, button and the "coming soon" fallback through t(). Renamed the
tab-map param off `t` so it no longer shadows the i18n function.
This commit is contained in:
2026-07-24 09:45:20 +02:00
parent 7d644d05cc
commit a5cfecbf76
3 changed files with 64 additions and 54 deletions
+46 -52
View File
@@ -3805,20 +3805,20 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
{/* Tab strip */}
<div className="flex flex-wrap gap-1 border-b border-border mb-4">
{TABS.map((t) => (
{TABS.map((tab) => (
<button
key={t.k}
key={tab.k}
type="button"
onClick={() => setExtSvcTab(t.k)}
onClick={() => setExtSvcTab(tab.k)}
className={cn(
'px-3 py-1.5 text-xs font-semibold rounded-t-md border-x border-t -mb-px transition-colors',
extSvcTab === t.k
extSvcTab === tab.k
? 'bg-card border-border text-foreground'
: 'bg-muted/40 border-transparent text-muted-foreground hover:text-foreground',
)}
>
{t.label}
{!t.ready && <span className="ml-1 text-[9px] opacity-60">soon</span>}
{tab.label}
{!tab.ready && <span className="ml-1 text-[9px] opacity-60">{t('es.soon')}</span>}
</button>
))}
</div>
@@ -3830,14 +3830,14 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
<Input
value={qrz.api_key}
onChange={(e) => setQrz({ api_key: e.target.value })}
placeholder="QRZ.com logbook API key (XXXX-XXXX-XXXX-XXXX)"
placeholder={t('es.qrzApiPh')}
className="font-mono text-xs"
/>
<Label className="text-sm">{t('es.forceCall')}</Label>
<Input
value={qrz.force_station_callsign}
onChange={(e) => setQrz({ force_station_callsign: e.target.value.toUpperCase() })}
placeholder="e.g. F4BPO — optional"
placeholder={t('es.forceCallPh')}
className="font-mono text-xs"
/>
</div>
@@ -3886,7 +3886,7 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
type="email"
value={clublog.email}
onChange={(e) => setClublog({ email: e.target.value })}
placeholder="your Club Log account email"
placeholder={t('es.clubEmailPh')}
className="text-xs"
/>
<Label className="text-sm">{t('es.password')}</Label>
@@ -3894,14 +3894,14 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
type="password"
value={clublog.password}
onChange={(e) => setClublog({ password: e.target.value })}
placeholder="Club Log account password"
placeholder={t('es.clubPwPh')}
className="text-xs"
/>
<Label className="text-sm">{t('es.logbookCall')}</Label>
<Input
value={clublog.callsign}
onChange={(e) => setClublog({ callsign: e.target.value.toUpperCase() })}
placeholder="defaults to the active profile's callsign"
placeholder={t('es.callDefaultPh')}
className="font-mono text-xs"
/>
</div>
@@ -3945,24 +3945,24 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
) : extSvcTab === 'hrdlog' ? (
<div className="space-y-4 max-w-2xl">
<div className="grid grid-cols-[170px_1fr] gap-3 items-center">
<Label className="text-sm">Station callsign</Label>
<Label className="text-sm">{t('es.stationCall')}</Label>
<Input
value={hrdlog.callsign}
onChange={(e) => setHrdlog({ callsign: e.target.value.toUpperCase() })}
placeholder="defaults to the active profile's callsign"
placeholder={t('es.callDefaultPh')}
className="font-mono text-xs"
/>
<Label className="text-sm">Upload code</Label>
<Label className="text-sm">{t('es.uploadCode')}</Label>
<Input
type="password"
value={hrdlog.code}
onChange={(e) => setHrdlog({ code: e.target.value })}
placeholder="HRDLog account → My Account → Upload code"
placeholder={t('es.hrdCodePh')}
className="text-xs"
/>
</div>
<div className="text-[10px] text-muted-foreground -mt-1">
Find the upload code on HRDLog.net under <em>My Account</em>. It authorises uploads for this callsign.
{t('es.hrdHint')}
</div>
<div className="border-t border-border/60 pt-3 space-y-3">
@@ -4004,11 +4004,11 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
) : extSvcTab === 'eqsl' ? (
<div className="space-y-4 max-w-2xl">
<div className="grid grid-cols-[170px_1fr] gap-3 items-center">
<Label className="text-sm">Username (callsign)</Label>
<Label className="text-sm">{t('es.usernameCall')}</Label>
<Input
value={eqsl.username}
onChange={(e) => setEqsl({ username: e.target.value.toUpperCase() })}
placeholder="your eQSL.cc login (callsign)"
placeholder={t('es.eqslUserPh')}
className="font-mono text-xs"
/>
<Label className="text-sm">{t('es.password')}</Label>
@@ -4016,19 +4016,19 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
type="password"
value={eqsl.password}
onChange={(e) => setEqsl({ password: e.target.value })}
placeholder="eQSL.cc account password"
placeholder={t('es.eqslPwPh')}
className="text-xs"
/>
<Label className="text-sm">QTH nickname</Label>
<Label className="text-sm">{t('es.qthNick')}</Label>
<Input
value={eqsl.qth_nickname}
onChange={(e) => setEqsl({ qth_nickname: e.target.value })}
placeholder="optional — required only if your eQSL account has several QTHs"
placeholder={t('es.qthNickPh')}
className="text-xs"
/>
</div>
<div className="text-[10px] text-muted-foreground -mt-1">
The QTH nickname tells eQSL which location profile to file the QSO under. Leave blank if you have only one.
{t('es.eqslHint')}
</div>
<div className="border-t border-border/60 pt-3 space-y-3">
@@ -4070,38 +4070,38 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
) : extSvcTab === 'lotw' ? (
<div className="space-y-4 max-w-2xl">
<div className="grid grid-cols-[170px_1fr] gap-3 items-center">
<Label className="text-sm">LoTW user</Label>
<Label className="text-sm">{t('es.lotwUser')}</Label>
<Input
value={lotw.username}
onChange={(e) => setLotw({ username: e.target.value })}
placeholder="LoTW website login (for downloading confirmations)"
placeholder={t('es.lotwUserPh')}
className="font-mono text-xs"
/>
<Label className="text-sm">LoTW password</Label>
<Label className="text-sm">{t('es.lotwPw')}</Label>
<Input
type="password"
value={lotw.password}
onChange={(e) => setLotw({ password: e.target.value })}
placeholder="LoTW website password"
placeholder={t('es.lotwPwPh')}
className="text-xs"
/>
<Label className="text-sm">TQSL path</Label>
<Label className="text-sm">{t('es.tqslPath')}</Label>
<Input
value={lotw.tqsl_path}
onChange={(e) => setLotw({ tqsl_path: e.target.value })}
placeholder="C:\Program Files (x86)\TrustedQSL\tqsl.exe"
className="font-mono text-xs"
/>
<Label className="text-sm">Station location</Label>
<Label className="text-sm">{t('es.stationLoc')}</Label>
<div className="flex items-center gap-2">
<Select value={lotw.station_location || '_'} onValueChange={(v) => setLotw({ station_location: v === '_' ? '' : v })}>
<SelectTrigger className="h-8 flex-1"><SelectValue placeholder="— pick a TQSL location —" /></SelectTrigger>
<SelectTrigger className="h-8 flex-1"><SelectValue placeholder={t('es.pickLoc')} /></SelectTrigger>
<SelectContent>
{stationLocations.length === 0 && <SelectItem value="_" disabled>No TQSL locations found</SelectItem>}
{stationLocations.length === 0 && <SelectItem value="_" disabled>{t('es.noLoc')}</SelectItem>}
{stationLocations.map((n) => <SelectItem key={n} value={n}>{n}</SelectItem>)}
</SelectContent>
</Select>
<Button variant="ghost" size="sm" className="h-8 px-2" onClick={refreshLocations} title="Reload locations from TQSL">
<Button variant="ghost" size="sm" className="h-8 px-2" onClick={refreshLocations} title={t('es.reloadLoc')}>
<ArrowDown className="size-3.5 rotate-90" />
</Button>
</div>
@@ -4110,23 +4110,22 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
<Input
value={lotw.force_station_callsign}
onChange={(e) => setLotw({ force_station_callsign: e.target.value.toUpperCase() })}
placeholder="e.g. F4BPO/P — leave blank to use the QSO's own call"
placeholder={t('es.lotwForcePh')}
className="font-mono uppercase w-64"
/>
<div className="text-[10px] text-muted-foreground mt-1">
Overrides STATION_CALLSIGN at sign time so one certificate can sign several calls
(F4BPO, F4BPO/P, TM2Q). Pick the matching Station Location above.
{t('es.lotwForceHint')}
</div>
</div>
<Label className="text-sm">Key password</Label>
<Label className="text-sm">{t('es.keyPw')}</Label>
<Input
type="password"
value={lotw.key_password}
onChange={(e) => setLotw({ key_password: e.target.value })}
placeholder="only if your certificate key has a password"
placeholder={t('es.keyPwPh')}
className="text-xs"
/>
<Label className="text-sm">Consider as unsent</Label>
<Label className="text-sm">{t('es.considerUnsent')}</Label>
<div>
<div className="flex items-center gap-4">
{(['N', 'R'] as const).map((f) => {
@@ -4143,15 +4142,13 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
setLotw({ upload_flags: next });
}}
/>
{f === 'N' ? 'No (N)' : 'Requested (R)'}
{f === 'N' ? t('es.flagN') : t('es.flagR')}
</label>
);
})}
</div>
<div className="text-[10px] text-muted-foreground mt-1">
At app close, every QSO whose LoTW <em>sent</em> status is one of these is signed and
uploaded in one TQSL batch including QSOs imported from an ADIF. Uploaded QSOs become
<em> Y</em> and won't be re-sent. Must include your default <em>sent</em> status from Confirmations.
{t('es.lotwUnsentHint')}
</div>
</div>
</div>
@@ -4162,14 +4159,14 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
checked={lotw.auto_upload}
onCheckedChange={(c) => setLotw({ auto_upload: !!c, upload_mode: 'on_close' })}
/>
Automatic upload on application close
{t('es.lotwAutoClose')}
</label>
<label className="flex items-center gap-2 text-sm cursor-pointer">
<Checkbox
checked={lotw.write_log}
onCheckedChange={(c) => setLotw({ write_log: !!c })}
/>
Write TQSL diagnostic log (-t)
{t('es.lotwWriteLog')}
</label>
<div className="flex items-center gap-3">
@@ -4204,13 +4201,10 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
) : extSvcTab === 'pota' ? (
<div className="space-y-4 max-w-2xl">
<p className="text-xs text-muted-foreground leading-relaxed">
Update your QSOs with the park reference from your <strong>pota.app hunter log</strong>.
Paste your session token: log in at <span className="font-mono">pota.app</span>, open the browser
DevTools → <strong>Network</strong> tab, click any <span className="font-mono">api.pota.app</span> request,
and copy the full <strong>Authorization</strong> header value. The token expires after a while — re-copy it if the sync fails.
{t('es.potaHint')}
</p>
<div className="grid grid-cols-[170px_1fr] gap-3 items-start">
<Label className="text-sm pt-2">Session token</Label>
<Label className="text-sm pt-2">{t('es.sessionToken')}</Label>
<Textarea
value={potaToken}
onChange={(e) => setPotaToken(e.target.value)}
@@ -4220,10 +4214,10 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
</div>
<div className="flex items-center gap-3">
<Button onClick={savePotaToken} disabled={potaBusy}>
{potaBusy ? <><Loader2 className="size-4 mr-1.5 animate-spin" /> Saving…</> : 'Save token'}
{potaBusy ? <><Loader2 className="size-4 mr-1.5 animate-spin" /> {t('es.saving')}</> : t('es.saveToken')}
</Button>
<span className="text-[11px] text-muted-foreground">
Then run the sync from the <strong>QSL Manager</strong> tab → service <strong>POTA hunter log</strong> (you can see and fix unmatched QSOs there).
{t('es.potaThen')}
</span>
</div>
{potaResult && (
@@ -4237,9 +4231,9 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
<div className="flex flex-col items-center justify-center text-muted-foreground gap-2 py-16">
<Construction className="size-10 opacity-30" />
<div className="text-sm font-semibold text-foreground/70">
{TABS.find((t) => t.k === extSvcTab)?.label} — coming soon
{t('es.comingSoon', { name: TABS.find((tab) => tab.k === extSvcTab)?.label ?? '' })}
</div>
<div className="text-xs">This external service isn't wired up yet.</div>
<div className="text-xs">{t('es.notWired')}</div>
</div>
)}
</>