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:
@@ -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>
|
||||
)}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user