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>
)}
</>
+16
View File
@@ -271,6 +271,14 @@ const en: Dict = {
'cat.ubOk': 'Connected — the antenna responded with a status frame.',
// External services (repeated labels)
'es.autoUpload': 'Automatic upload on new QSO', 'es.uploadTiming': 'Upload timing', 'es.immediate': 'Immediate', 'es.delayed': 'Delayed (12 min, lets you fix mistakes)', 'es.onClose': 'On app close (batch)', 'es.testConn': 'Test connection', 'es.testing': 'Testing…', 'es.password': 'Password', 'es.showPass': 'Show password', 'es.hidePass': 'Hide password', 'es.apiKey': 'API key', 'es.forceCall': 'Force station callsign', 'es.accountEmail': 'Account email', 'es.logbookCall': 'Logbook callsign',
// External-services placeholders + HRDLOG / eQSL / LoTW / POTA tabs
'es.qrzApiPh': 'QRZ.com logbook API key (XXXX-XXXX-XXXX-XXXX)', 'es.forceCallPh': 'e.g. F4BPO — optional', 'es.callDefaultPh': "defaults to the active profile's callsign",
'es.clubEmailPh': 'your Club Log account email', 'es.clubPwPh': 'Club Log account password',
'es.stationCall': 'Station callsign', 'es.uploadCode': 'Upload code', 'es.hrdCodePh': 'HRDLog account → My Account → Upload code', 'es.hrdHint': 'Find the upload code on HRDLog.net under My Account. It authorises uploads for this callsign.',
'es.usernameCall': 'Username (callsign)', 'es.eqslUserPh': 'your eQSL.cc login (callsign)', 'es.eqslPwPh': 'eQSL.cc account password', 'es.qthNick': 'QTH nickname', 'es.qthNickPh': 'optional — required only if your eQSL account has several QTHs', 'es.eqslHint': 'The QTH nickname tells eQSL which location profile to file the QSO under. Leave blank if you have only one.',
'es.lotwUser': 'LoTW user', 'es.lotwUserPh': 'LoTW website login (for downloading confirmations)', 'es.lotwPw': 'LoTW password', 'es.lotwPwPh': 'LoTW website password', 'es.tqslPath': 'TQSL path', 'es.stationLoc': 'Station location', 'es.pickLoc': '— pick a TQSL location —', 'es.noLoc': 'No TQSL locations found', 'es.reloadLoc': 'Reload locations from TQSL', 'es.lotwForcePh': "e.g. F4BPO/P — leave blank to use the QSO's own call", 'es.lotwForceHint': 'Overrides STATION_CALLSIGN at sign time so one certificate can sign several calls (F4BPO, F4BPO/P, TM2Q). Pick the matching Station Location above.', 'es.keyPw': 'Key password', 'es.keyPwPh': 'only if your certificate key has a password', 'es.considerUnsent': 'Consider as unsent', 'es.flagN': 'No (N)', 'es.flagR': 'Requested (R)', 'es.lotwUnsentHint': "At app close, every QSO whose LoTW sent status is one of these is signed and uploaded in one TQSL batch — including QSOs imported from an ADIF. Uploaded QSOs become Y and won't be re-sent. Must include your default sent status from Confirmations.", 'es.lotwAutoClose': 'Automatic upload on application close', 'es.lotwWriteLog': 'Write TQSL diagnostic log (-t)',
'es.potaHint': 'Update your QSOs with the park reference from your pota.app hunter log. Paste your session token: log in at pota.app, open the browser DevTools → Network tab, click any api.pota.app request, and copy the full Authorization header value. The token expires after a while — re-copy it if the sync fails.', 'es.sessionToken': 'Session token', 'es.saving': 'Saving…', 'es.saveToken': 'Save token', 'es.potaThen': 'Then run the sync from the QSL Manager tab → service POTA hunter log (you can see and fix unmatched QSOs there).',
'es.soon': 'soon', 'es.comingSoon': '{name} — coming soon', 'es.notWired': "This external service isn't wired up yet.",
'lotw.usersTitle': 'LoTW user list', 'lotw.usersHint': "Downloads ARRL's public list of LoTW users + their last-upload date, to show a colour-coded LoTW badge next to a callsign (green < 1 week · amber 14 weeks · red > 30 days).", 'lotw.usersDownload': 'Download LoTW user list', 'lotw.usersDownloading': 'Downloading…', 'lotw.usersLoaded': '{n} users loaded · updated {date}', 'lotw.usersNone': 'Not downloaded yet — the badge stays hidden until you do.',
'hw.connecting': 'Connecting…', 'hw.testConn': 'Test connection', 'hw.sending': 'Sending…', 'hw.testRotator': 'Test (point to 0°)',
// Profiles panel
@@ -613,6 +621,14 @@ const fr: Dict = {
'cat.rotatorOk': "Paquet envoyé — l'antenne devrait tourner vers 0° (nord). Sinon, vérifie l'hôte/port PstRotator et que l'écouteur UDP de PstRotator est activé.",
'cat.ubOk': "Connecté — l'antenne a répondu avec une trame de statut.",
'es.autoUpload': 'Envoi automatique à chaque nouveau QSO', 'es.uploadTiming': "Moment de l'envoi", 'es.immediate': 'Immédiat', 'es.delayed': 'Différé (12 min, permet de corriger les erreurs)', 'es.onClose': "À la fermeture (par lot)", 'es.testConn': 'Tester la connexion', 'es.testing': 'Test…', 'es.password': 'Mot de passe', 'es.showPass': 'Afficher le mot de passe', 'es.hidePass': 'Masquer le mot de passe', 'es.apiKey': 'Clé API', 'es.forceCall': "Forcer l'indicatif de station", 'es.accountEmail': 'E-mail du compte', 'es.logbookCall': 'Indicatif du carnet',
// Placeholders services externes + onglets HRDLOG / eQSL / LoTW / POTA
'es.qrzApiPh': 'Clé API du logbook QRZ.com (XXXX-XXXX-XXXX-XXXX)', 'es.forceCallPh': 'p. ex. F4BPO — optionnel', 'es.callDefaultPh': "par défaut : l'indicatif du profil actif",
'es.clubEmailPh': 'e-mail de ton compte Club Log', 'es.clubPwPh': 'mot de passe du compte Club Log',
'es.stationCall': 'Indicatif de station', 'es.uploadCode': "Code d'upload", 'es.hrdCodePh': 'Compte HRDLog → My Account → Upload code', 'es.hrdHint': "Trouve le code d'upload sur HRDLog.net dans « My Account ». Il autorise les envois pour cet indicatif.",
'es.usernameCall': 'Identifiant (indicatif)', 'es.eqslUserPh': 'ton identifiant eQSL.cc (indicatif)', 'es.eqslPwPh': 'mot de passe du compte eQSL.cc', 'es.qthNick': 'Surnom QTH', 'es.qthNickPh': 'optionnel — requis seulement si ton compte eQSL a plusieurs QTH', 'es.eqslHint': "Le surnom QTH indique à eQSL sous quel profil de lieu classer le QSO. Laisse vide si tu n'en as qu'un.",
'es.lotwUser': 'Utilisateur LoTW', 'es.lotwUserPh': 'identifiant du site LoTW (pour télécharger les confirmations)', 'es.lotwPw': 'Mot de passe LoTW', 'es.lotwPwPh': 'mot de passe du site LoTW', 'es.tqslPath': 'Chemin TQSL', 'es.stationLoc': 'Station location', 'es.pickLoc': '— choisir une Station Location TQSL —', 'es.noLoc': 'Aucune Station Location TQSL trouvée', 'es.reloadLoc': 'Recharger les locations depuis TQSL', 'es.lotwForcePh': "p. ex. F4BPO/P — laisse vide pour utiliser l'indicatif du QSO", 'es.lotwForceHint': "Remplace STATION_CALLSIGN à la signature pour qu'un même certificat signe plusieurs indicatifs (F4BPO, F4BPO/P, TM2Q). Choisis la Station Location correspondante ci-dessus.", 'es.keyPw': 'Mot de passe de la clé', 'es.keyPwPh': 'seulement si la clé de ton certificat a un mot de passe', 'es.considerUnsent': 'Considérer comme non envoyé', 'es.flagN': 'Non (N)', 'es.flagR': 'Demandé (R)', 'es.lotwUnsentHint': "À la fermeture, chaque QSO dont le statut LoTW « envoyé » est l'un de ceux-ci est signé et envoyé dans un même lot TQSL — y compris les QSO importés depuis un ADIF. Les QSO envoyés passent à Y et ne sont pas renvoyés. Doit inclure ton statut « envoyé » par défaut défini dans Confirmations.", 'es.lotwAutoClose': "Envoi automatique à la fermeture de l'application", 'es.lotwWriteLog': 'Écrire le journal de diagnostic TQSL (-t)',
'es.potaHint': "Met à jour tes QSO avec la référence du parc depuis ton carnet chasseur pota.app. Colle ton jeton de session : connecte-toi sur pota.app, ouvre les DevTools du navigateur → onglet Network, clique sur une requête api.pota.app, et copie toute la valeur de l'en-tête Authorization. Le jeton expire au bout d'un moment — recopie-le si la synchro échoue.", 'es.sessionToken': 'Jeton de session', 'es.saving': 'Enregistrement…', 'es.saveToken': 'Enregistrer le jeton', 'es.potaThen': "Puis lance la synchro depuis l'onglet Gestionnaire QSL → service POTA hunter log (tu peux y voir et corriger les QSO non appariés).",
'es.soon': 'bientôt', 'es.comingSoon': '{name} — bientôt', 'es.notWired': "Ce service externe n'est pas encore branché.",
'lotw.usersTitle': 'Liste des utilisateurs LoTW', 'lotw.usersHint': "Télécharge la liste publique ARRL des utilisateurs LoTW + leur date de dernier upload, pour afficher un badge LoTW coloré à côté d'un indicatif (vert < 1 semaine · ambre 14 semaines · rouge > 30 j).", 'lotw.usersDownload': 'Télécharger la liste LoTW', 'lotw.usersDownloading': 'Téléchargement…', 'lotw.usersLoaded': '{n} utilisateurs chargés · maj {date}', 'lotw.usersNone': 'Pas encore téléchargée — le badge reste caché tant que ce nest pas fait.',
'hw.connecting': 'Connexion…', 'hw.testConn': 'Tester la connexion', 'hw.sending': 'Envoi…', 'hw.testRotator': 'Tester (pointer vers 0°)',
'prof.deleteConfirm': 'Supprimer le profil « {name} » ? Tous ses réglages seront perdus.', 'prof.dupPrompt': 'Nom du nouveau profil (copie de « {name} ») :', 'prof.dupSuffix': '{name} Copie', 'prof.newPrompt': 'Nom du nouveau profil :', 'prof.newDefault': 'Nouveau profil',