Add React i18n for translate proposal. First, settings dialog translate

pull/27/head
SebFox2011 2 days ago
parent 5f913c2fe9
commit 3b925f5058

@ -16,7 +16,10 @@
"cors": "^2.8.5",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"i18next": "^25.8.0",
"i18next-browser-languagedetector": "^8.2.0",
"node-fetch": "^2.7.0",
"react-i18next": "^16.5.4",
"satellite.js": "^5.0.0",
"ws": "^8.14.2"
},

@ -4,6 +4,7 @@
*/
import React, { useState, useEffect } from 'react';
import { calculateGridSquare } from '../utils/geo.js';
import { useTranslation, Trans } from 'react-i18next';
export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
const [callsign, setCallsign] = useState(config?.callsign || '');
@ -13,6 +14,7 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
const [theme, setTheme] = useState(config?.theme || 'dark');
const [layout, setLayout] = useState(config?.layout || 'modern');
const [dxClusterSource, setDxClusterSource] = useState(config?.dxClusterSource || 'dxspider-proxy');
const { t } = useTranslation();
useEffect(() => {
if (config) {
@ -83,11 +85,11 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
},
(error) => {
console.error('Geolocation error:', error);
alert('Unable to get location. Please enter manually.');
alert(t('station.settings.useLocation.error1'));
}
);
} else {
alert('Geolocation not supported by your browser.');
alert(t('station.settings.useLocation.error2'));
}
};
@ -106,16 +108,22 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
if (!isOpen) return null;
const Code = ({ children }) => (
<code style={{ background: 'var(--bg-tertiary)', padding: '2px 4px', borderRadius: '3px' }}>
{children}
</code>
);
const themeDescriptions = {
dark: '→ Modern dark theme (default)',
light: '→ Light theme for daytime use',
legacy: '→ Green terminal CRT style',
retro: '→ 90s Windows retro style'
dark: t('station.settings.theme.dark.describe'),
light: t('station.settings.theme.light.describe'),
legacy: t('station.settings.theme.legacy.describe'),
retro: t('station.settings.theme.retro.describe')
};
const layoutDescriptions = {
modern: '→ Modern responsive grid layout',
classic: '→ Original HamClock-style layout'
modern: t('station.settings.layout.modern.describe'),
classic: t('station.settings.layout.classic.describe')
};
return (
@ -136,7 +144,7 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
border: '2px solid var(--accent-amber)',
borderRadius: '12px',
padding: '24px',
width: '420px',
width: '480px',
maxHeight: '90vh',
overflowY: 'auto'
}}>
@ -148,7 +156,7 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
fontFamily: 'Orbitron, monospace',
fontSize: '20px'
}}>
Station Settings
{t('station.settings.title')}
</h2>
{/* First-time setup banner */}
@ -162,14 +170,13 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
fontSize: '13px'
}}>
<div style={{ color: 'var(--accent-amber)', fontWeight: '700', marginBottom: '6px' }}>
👋 Welcome to OpenHamClock!
{t("station.settings.welcome")}
</div>
<div style={{ color: 'var(--text-secondary)', lineHeight: 1.5 }}>
Please enter your callsign and grid square to get started.
Your settings will be saved in your browser.
{t("station.settings.describe")}
</div>
<div style={{ color: 'var(--text-muted)', fontSize: '11px', marginTop: '8px' }}>
💡 Tip: For permanent config, copy <code style={{ background: 'var(--bg-tertiary)', padding: '2px 4px', borderRadius: '3px' }}>.env.example</code> to <code style={{ background: 'var(--bg-tertiary)', padding: '2px 4px', borderRadius: '3px' }}>.env</code> and set CALLSIGN and LOCATOR
<Trans i18nKey="station.settings.tip.env" components={{ envExample: <Code />, env: <Code /> }} />
</div>
</div>
)}
@ -177,7 +184,7 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
{/* Callsign */}
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '6px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1px' }}>
Your Callsign
{t('station.settings.callsign')}
</label>
<input
type="text"
@ -201,7 +208,7 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
{/* Grid Square */}
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '6px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1px' }}>
Grid Square (or enter Lat/Lon below)
{t('station.settings.locator')}
</label>
<input
type="text"
@ -228,7 +235,7 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '12px', marginBottom: '12px' }}>
<div>
<label style={{ display: 'block', marginBottom: '6px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase' }}>
Latitude
{t('station.settings.latitude')}
</label>
<input
type="number"
@ -250,7 +257,7 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
</div>
<div>
<label style={{ display: 'block', marginBottom: '6px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase' }}>
Longitude
{t('station.settings.longitude')}
</label>
<input
type="number"
@ -287,31 +294,31 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
marginBottom: '20px'
}}
>
📍 Use My Current Location
{t('station.settings.useLocation')}
</button>
{/* Theme */}
<div style={{ marginBottom: '8px' }}>
<label style={{ display: 'block', marginBottom: '8px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1px' }}>
Theme
{t('station.settings.theme')}
</label>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '8px' }}>
{['dark', 'light', 'legacy', 'retro'].map((t) => (
{['dark', 'light', 'legacy', 'retro'].map((theme) => (
<button
key={t}
onClick={() => setTheme(t)}
key={theme}
onClick={() => setTheme(theme)}
style={{
padding: '10px',
background: theme === t ? 'var(--accent-amber)' : 'var(--bg-tertiary)',
border: `1px solid ${theme === t ? 'var(--accent-amber)' : 'var(--border-color)'}`,
background: theme === theme ? 'var(--accent-amber)' : 'var(--bg-tertiary)',
border: `1px solid ${theme === theme ? 'var(--accent-amber)' : 'var(--border-color)'}`,
borderRadius: '6px',
color: theme === t ? '#000' : 'var(--text-secondary)',
color: theme === theme ? '#000' : 'var(--text-secondary)',
fontSize: '12px',
cursor: 'pointer',
fontWeight: theme === t ? '600' : '400'
fontWeight: theme === theme ? '600' : '400'
}}
>
{t === 'dark' ? '🌙' : t === 'light' ? '☀️' : t === 'legacy' ? '💻' : '🪟'} {t.charAt(0).toUpperCase() + t.slice(1)}
{theme === 'dark' ? '🌙' : theme === 'light' ? '☀️' : theme === 'legacy' ? '💻' : '🪟'} {t('station.settings.theme.' + theme)}
</button>
))}
</div>
@ -323,7 +330,7 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
{/* Layout */}
<div style={{ marginBottom: '8px' }}>
<label style={{ display: 'block', marginBottom: '8px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1px' }}>
Layout
{t('station.settings.layout')}
</label>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(2, 1fr)', gap: '8px' }}>
{['modern', 'classic'].map((l) => (
@ -341,7 +348,7 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
fontWeight: layout === l ? '600' : '400'
}}
>
{l === 'modern' ? '🖥️' : '📺'} {l.charAt(0).toUpperCase() + l.slice(1)}
{l === 'modern' ? '🖥️' : '📺'} {t('station.settings.layout.' + l)}
</button>
))}
</div>
@ -353,7 +360,7 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
{/* DX Cluster Source */}
<div style={{ marginBottom: '20px' }}>
<label style={{ display: 'block', marginBottom: '8px', color: 'var(--text-muted)', fontSize: '11px', textTransform: 'uppercase', letterSpacing: '1px' }}>
DX Cluster Source
{t('station.settings.dx.title')}
</label>
<select
value={dxClusterSource}
@ -370,13 +377,13 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
cursor: 'pointer'
}}
>
<option value="dxspider-proxy"> DX Spider Proxy (Recommended)</option>
<option value="hamqth">HamQTH Cluster</option>
<option value="dxwatch">DXWatch</option>
<option value="auto">Auto (try all sources)</option>
<option value="dxspider-proxy">{t('station.settings.dx.option1')}</option>
<option value="hamqth">{t('station.settings.dx.option2')}</option>
<option value="dxwatch">{t('station.settings.dx.option3')}</option>
<option value="auto">{t('station.settings.dx.option4')}</option>
</select>
<div style={{ fontSize: '11px', color: 'var(--text-muted)', marginTop: '6px' }}>
Real-time DX Spider feed via our dedicated proxy service
{t('station.settings.dx.describe')}
</div>
</div>
@ -394,7 +401,7 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
cursor: 'pointer'
}}
>
Cancel
{t('cancel')}
</button>
<button
onClick={handleSave}
@ -409,12 +416,12 @@ export const SettingsPanel = ({ isOpen, onClose, config, onSave }) => {
cursor: 'pointer'
}}
>
Save Settings
{t('station.settings.button.save')}
</button>
</div>
<div style={{ textAlign: 'center', marginTop: '16px', fontSize: '11px', color: 'var(--text-muted)' }}>
Settings are saved in your browser
{t('station.settings.button.save.confirm')}
</div>
</div>
</div>

@ -0,0 +1,40 @@
{
"cancel": "Cancel",
"station.settings.altitude": "Altitude (m)",
"station.settings.antenna": "Antenna",
"station.settings.button.save": "Save Settings",
"station.settings.button.save.confirm": "Settings are saved in your browser",
"station.settings.callsign": "Your Callsign",
"station.settings.describe": "Please enter your callsign and grid square to get started. Your settings will be saved in your browser.",
"station.settings.dx.describe": "→ Real-time DX Spider feed via our dedicated proxy service",
"station.settings.dx.option1": "⭐ DX Spider Proxy (Recommended)",
"station.settings.dx.option2": "HamQTH Cluster",
"station.settings.dx.option3": "DXWatch",
"station.settings.dx.option4": "Auto (try all sources)",
"station.settings.dx.title": "DX Cluster Source",
"station.settings.layout": "Layout",
"station.settings.layout.classic": "Classic",
"station.settings.layout.classic.describe": "→ Original HamClock-style layout",
"station.settings.layout.modern": "Modern",
"station.settings.layout.modern.describe": "→ Modern responsive grid layout",
"station.settings.latitude": "Latitude",
"station.settings.locator": "Grid Square (or enter Lat/Lon below)",
"station.settings.longitude": "Longitude",
"station.settings.power": "Power (W)",
"station.settings.theme": "THEME",
"station.settings.theme.dark": "Dark",
"station.settings.theme.dark.describe": "→ Modern dark theme (default)",
"station.settings.theme.legacy": "Legacy",
"station.settings.theme.legacy.describe": "→ Green CRT terminal style",
"station.settings.theme.light": "Light",
"station.settings.theme.light.describe": "→ Light theme for daytime use",
"station.settings.theme.retro": "Retro",
"station.settings.theme.retro.describe": "→ 90s Windows retro style",
"station.settings.timezone": "Timezone",
"station.settings.title": "Station Settings",
"station.settings.tip.env": "💡 Tip: For permanent config, copy <envExample>.env.example</envExample> to <env>.env</env> and set CALLSIGN and LOCATOR",
"station.settings.useLocation": "📍 Use My Current Location",
"station.settings.useLocation.error1": "Unable to get location. Please enter manually.",
"station.settings.useLocation.error2": "Geolocation is not supported by your browser.",
"station.settings.welcome": "👋 Welcome to OpenHamClock!"
}

@ -0,0 +1,40 @@
{
"Cancel": "Annuler",
"station.settings.altitude": "Altitude (m)",
"station.settings.antenna": "Antenne",
"station.settings.button.save": "Enregistrer les paramètres",
"station.settings.button.save.confirm": "Les paramètres sont enregistrés dans votre navigateur",
"station.settings.callsign": "Indicatif d'appel",
"station.settings.describe": "Veuillez entrer votre indicatif d'appel et votre carré de grille pour commencer. Vos paramètres seront enregistrés dans votre navigateur.",
"station.settings.dx.describe": "→ Flux en temps réel de DX Spider via notre service proxy dédié",
"station.settings.dx.option1": "⭐ Proxy DX Spider (Recommandé)",
"station.settings.dx.option2": "Cluster HamQTH",
"station.settings.dx.option3": "DXWatch",
"station.settings.dx.option4": "Auto (essayer toutes les sources)",
"station.settings.dx.title": "Source du cluster DX",
"station.settings.layout": "Disposition",
"station.settings.layout.classic": "Classique",
"station.settings.layout.classic.describe": "→ Disposition de style HamClock original",
"station.settings.layout.modern": "Moderne",
"station.settings.layout.modern.describe": "→ Disposition en grille réactive moderne",
"station.settings.latitude": "Latitude",
"station.settings.locator": "Carré de grille (ou entrez Lat/Lon ci-dessous)",
"station.settings.longitude": "Longitude",
"station.settings.power": "Puissance (W)",
"station.settings.theme": "THÈME",
"station.settings.theme.dark": "Sombre",
"station.settings.theme.dark.describe": "→ Thème sombre moderne (par défaut)",
"station.settings.theme.legacy": "Classique",
"station.settings.theme.legacy.describe": "→ Style CRT terminal vert",
"station.settings.theme.light": "Clair",
"station.settings.theme.light.describe": "→ Thème clair pour une utilisation diurne",
"station.settings.theme.retro": "Rétro",
"station.settings.theme.retro.describe": "→ Style rétro Windows des années 90",
"station.settings.timezone": "Fuseau horaire",
"station.settings.title": "⚙ Paramètres de la station",
"station.settings.tip.env": "💡 Astuce : Pour une configuration permanente, copiez <envExample>.env.example</envExample> vers <env>.env</env> et définissez Indicatif d'appel et Carré de grille",
"station.settings.useLocation": "📍 Utiliser ma position actuelle",
"station.settings.useLocation.error1": "Impossible d'obtenir la position. Veuillez entrer manuellement.",
"station.settings.useLocation.error2": "La géolocalisation n'est pas prise en charge par votre navigateur.",
"station.settings.welcome": "👋 Bienvenue sur OpenHamClock !"
}

@ -0,0 +1,31 @@
import i18n from 'i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
import { initReactI18next } from 'react-i18next';
import translationFR from './fr.json';
import translationEN from './en.json';
export const resources = {
fr: { translation: translationFR },
en: { translation: translationEN }
} ;
i18n
.use(LanguageDetector) // Automatically detects the user's language
.use(initReactI18next)
.init({
fallbackLng: 'en',
resources: {
fr: {
translation: translationFR
},
en: {
translation: translationEN
}
},
interpolation: {
escapeValue: false
}
});
export default i18n;

@ -2,6 +2,7 @@ import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import './styles/main.css';
import './lang/i18n';
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>

Loading…
Cancel
Save

Powered by TurnKey Linux.