Skip to content

Commit 4336dc4

Browse files
belinea4071claude
andcommitted
Single source of truth for dashboard translations (#60)
Create dashboard/translations.json (138 keys × 6 languages) as the single translation file for all dashboard UI text. Both JS cards and Python generator read from this same file. - sem-localize.js: fetches translations.json at startup instead of inline translation tables - dashboard_generator.py: reads translations.json, builds reverse lookup (English → translated), replaces all title/name/primary/ subtitle/label strings in the YAML template - __init__.py: auto-installs translations.json to www/ alongside cards Covers ALL dashboard strings: section titles, mushroom card names, status text, metric labels, device modes, schedule labels. Result: German user sees "Nachtladen", "Prognose-Reduktion", "Ladeziel Nacht (kWh)", "Überschusssteuerung" etc. across the entire dashboard — from a single translations.json file. Refs #60 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 0897f98 commit 4336dc4

File tree

4 files changed

+991
-278
lines changed

4 files changed

+991
-278
lines changed

__init__.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1133,6 +1133,15 @@ def _copy_cards() -> list:
11331133
if not os.path.exists(dst) or os.path.getmtime(src) > os.path.getmtime(dst):
11341134
shutil.copy2(src, dst)
11351135
cards.append(fname)
1136+
# Also copy translations.json for sem-localize.js (#60)
1137+
dashboard_dir = os.path.dirname(card_src_dir)
1138+
translations_src = os.path.join(dashboard_dir, "translations.json")
1139+
translations_dst = os.path.join(os.path.dirname(card_www_dir), "translations.json")
1140+
if os.path.exists(translations_src):
1141+
if not os.path.exists(translations_dst) or os.path.getmtime(translations_src) > os.path.getmtime(translations_dst):
1142+
os.makedirs(os.path.dirname(translations_dst), exist_ok=True)
1143+
shutil.copy2(translations_src, translations_dst)
1144+
cards.append("translations.json")
11361145
return cards
11371146

11381147
updated = await hass.async_add_executor_job(_copy_cards)

dashboard/card/sem-localize.js

Lines changed: 34 additions & 215 deletions
Original file line numberDiff line numberDiff line change
@@ -1,232 +1,51 @@
11
/**
2-
* SEM Localization — shared translation tables for all SEM dashboard cards
2+
* SEM Localization — loads dashboard translations from translations.json
33
*
4-
* Usage in cards:
5-
* const _t = (key) => semLocalize(key, this._hass?.language);
6-
* this.shadowRoot.querySelector('.label').textContent = _t('charging');
4+
* Single source of truth: /dashboard/translations.json
5+
* Used by all SEM cards via semLocalize(key, lang)
6+
* Also loaded by dashboard_generator.py for YAML template translation
77
*/
88

9-
const SEM_TRANSLATIONS = {
10-
en: {
11-
// Status
12-
charging: 'Charging', discharging: 'Discharging', idle: 'Idle',
13-
connected: 'Connected', disconnected: 'Disconnected',
14-
importing: 'Import', exporting: 'Export', grid: 'Grid',
9+
let SEM_TRANSLATIONS = null;
10+
let _loadPromise = null;
1511

16-
// Tab headers
17-
home: 'Home', energy: 'Energy', battery: 'Battery',
18-
ev_charging: 'EV Charging', control: 'Control', costs: 'Costs', system: 'System',
19-
20-
// Tab subtitles
21-
home_sub: 'Energy overview', energy_sub: 'Production & consumption',
22-
battery_sub: 'Storage & health', ev_sub: 'Vehicle & sessions',
23-
control_sub: 'Devices & scheduling', costs_sub: 'Savings & tariffs',
24-
system_sub: 'Health & diagnostics',
25-
26-
// Metric labels
27-
solar: 'Solar', autarky: 'Autarky', today: 'Today', soc: 'SOC',
28-
power: 'Power', health: 'Health', cycles: 'Cycles', temperature: 'Temperature',
29-
status: 'Status', session: 'Session', current: 'Current',
30-
solar_share: 'Solar share', strategy: 'Strategy', peak: 'Peak',
31-
devices: 'Devices', active: 'Active', cost: 'Cost', saved: 'Saved', net: 'Net',
32-
score: 'Score', co2: 'CO₂', self_use: 'Self-use',
33-
34-
// Battery card
35-
charge_today: 'Charge today', discharge_today: 'Discharge today',
36-
savings_today: 'Savings today',
37-
38-
// EV card
39-
session_cost: 'Session cost', no_vehicle: 'No vehicle connected',
40-
41-
// Schedule card
42-
tariff: 'Tariff', night: 'Night', surplus: 'Surplus', ev: 'EV',
43-
ht: 'HT', nt: 'NT',
44-
45-
// Device control modes
46-
mode: 'Mode', off: 'Off', peak_only: 'Peak Only',
47-
surplus_mode: 'Surplus', critical: 'Critical',
48-
49-
// Units
50-
kwh: 'kWh', w: 'W', kw: 'kW',
51-
},
52-
de: {
53-
charging: 'Laden', discharging: 'Entladen', idle: 'Leerlauf',
54-
connected: 'Verbunden', disconnected: 'Getrennt',
55-
importing: 'Import', exporting: 'Export', grid: 'Netz',
56-
57-
home: 'Übersicht', energy: 'Energie', battery: 'Batterie',
58-
ev_charging: 'EV-Laden', control: 'Steuerung', costs: 'Kosten', system: 'System',
59-
60-
home_sub: 'Energieübersicht', energy_sub: 'Erzeugung & Verbrauch',
61-
battery_sub: 'Speicher & Gesundheit', ev_sub: 'Fahrzeug & Sitzungen',
62-
control_sub: 'Geräte & Planung', costs_sub: 'Ersparnis & Tarife',
63-
system_sub: 'Zustand & Diagnose',
64-
65-
solar: 'Solar', autarky: 'Autarkie', today: 'Heute', soc: 'SOC',
66-
power: 'Leistung', health: 'Zustand', cycles: 'Zyklen', temperature: 'Temperatur',
67-
status: 'Status', session: 'Sitzung', current: 'Strom',
68-
solar_share: 'Solaranteil', strategy: 'Strategie', peak: 'Spitze',
69-
devices: 'Geräte', active: 'Aktiv', cost: 'Kosten', saved: 'Gespart', net: 'Netto',
70-
score: 'Bewertung', co2: 'CO₂', self_use: 'Eigenverbrauch',
71-
72-
charge_today: 'Ladung heute', discharge_today: 'Entladung heute',
73-
savings_today: 'Ersparnis heute',
74-
75-
session_cost: 'Sitzungskosten', no_vehicle: 'Kein Fahrzeug verbunden',
76-
77-
tariff: 'Tarif', night: 'Nacht', surplus: 'Überschuss', ev: 'EV',
78-
ht: 'HT', nt: 'NT',
79-
80-
mode: 'Modus', off: 'Aus', peak_only: 'Nur Spitze',
81-
surplus_mode: 'Überschuss', critical: 'Kritisch',
82-
83-
kwh: 'kWh', w: 'W', kw: 'kW',
84-
},
85-
fr: {
86-
charging: 'En charge', discharging: 'Décharge', idle: 'Inactif',
87-
connected: 'Connecté', disconnected: 'Déconnecté',
88-
importing: 'Import', exporting: 'Export', grid: 'Réseau',
89-
90-
home: 'Accueil', energy: 'Énergie', battery: 'Batterie',
91-
ev_charging: 'Charge VE', control: 'Contrôle', costs: 'Coûts', system: 'Système',
92-
93-
home_sub: "Vue d'ensemble", energy_sub: 'Production & consommation',
94-
battery_sub: 'Stockage & santé', ev_sub: 'Véhicule & sessions',
95-
control_sub: 'Appareils & planification', costs_sub: 'Économies & tarifs',
96-
system_sub: 'Santé & diagnostic',
97-
98-
solar: 'Solaire', autarky: 'Autarcie', today: "Aujourd'hui", soc: 'SOC',
99-
power: 'Puissance', health: 'Santé', cycles: 'Cycles', temperature: 'Température',
100-
status: 'État', session: 'Session', current: 'Courant',
101-
solar_share: 'Part solaire', strategy: 'Stratégie', peak: 'Pointe',
102-
devices: 'Appareils', active: 'Actifs', cost: 'Coût', saved: 'Économisé', net: 'Net',
103-
score: 'Score', co2: 'CO₂', self_use: 'Autoconsommation',
104-
105-
charge_today: "Charge aujourd'hui", discharge_today: "Décharge aujourd'hui",
106-
savings_today: "Économies aujourd'hui",
107-
108-
session_cost: 'Coût session', no_vehicle: 'Aucun véhicule connecté',
109-
110-
tariff: 'Tarif', night: 'Nuit', surplus: 'Surplus', ev: 'VE',
111-
ht: 'HP', nt: 'HC',
112-
113-
mode: 'Mode', off: 'Arrêt', peak_only: 'Pointe seule',
114-
surplus_mode: 'Surplus', critical: 'Critique',
115-
116-
kwh: 'kWh', w: 'W', kw: 'kW',
117-
},
118-
es: {
119-
charging: 'Cargando', discharging: 'Descargando', idle: 'Inactivo',
120-
connected: 'Conectado', disconnected: 'Desconectado',
121-
importing: 'Importación', exporting: 'Exportación', grid: 'Red',
122-
123-
home: 'Inicio', energy: 'Energía', battery: 'Batería',
124-
ev_charging: 'Carga VE', control: 'Control', costs: 'Costes', system: 'Sistema',
125-
126-
home_sub: 'Vista general', energy_sub: 'Producción y consumo',
127-
battery_sub: 'Almacenamiento y salud', ev_sub: 'Vehículo y sesiones',
128-
control_sub: 'Dispositivos y planificación', costs_sub: 'Ahorro y tarifas',
129-
system_sub: 'Salud y diagnóstico',
130-
131-
solar: 'Solar', autarky: 'Autarquía', today: 'Hoy', soc: 'SOC',
132-
power: 'Potencia', health: 'Salud', cycles: 'Ciclos', temperature: 'Temperatura',
133-
status: 'Estado', session: 'Sesión', current: 'Corriente',
134-
solar_share: 'Cuota solar', strategy: 'Estrategia', peak: 'Pico',
135-
devices: 'Dispositivos', active: 'Activos', cost: 'Coste', saved: 'Ahorrado', net: 'Neto',
136-
score: 'Puntuación', co2: 'CO₂', self_use: 'Autoconsumo',
137-
138-
charge_today: 'Carga hoy', discharge_today: 'Descarga hoy',
139-
savings_today: 'Ahorro hoy',
140-
141-
session_cost: 'Coste sesión', no_vehicle: 'Ningún vehículo conectado',
142-
143-
tariff: 'Tarifa', night: 'Noche', surplus: 'Excedente', ev: 'VE',
144-
ht: 'HP', nt: 'HV',
145-
146-
mode: 'Modo', off: 'Apagado', peak_only: 'Solo pico',
147-
surplus_mode: 'Excedente', critical: 'Crítico',
148-
149-
kwh: 'kWh', w: 'W', kw: 'kW',
150-
},
151-
it: {
152-
charging: 'In carica', discharging: 'Scarica', idle: 'Inattivo',
153-
connected: 'Connesso', disconnected: 'Disconnesso',
154-
importing: 'Importazione', exporting: 'Esportazione', grid: 'Rete',
155-
156-
home: 'Home', energy: 'Energia', battery: 'Batteria',
157-
ev_charging: 'Carica VE', control: 'Controllo', costs: 'Costi', system: 'Sistema',
158-
159-
home_sub: "Panoramica energia", energy_sub: 'Produzione e consumo',
160-
battery_sub: 'Accumulo e salute', ev_sub: 'Veicolo e sessioni',
161-
control_sub: 'Dispositivi e pianificazione', costs_sub: 'Risparmio e tariffe',
162-
system_sub: 'Salute e diagnostica',
163-
164-
solar: 'Solare', autarky: 'Autarchia', today: 'Oggi', soc: 'SOC',
165-
power: 'Potenza', health: 'Salute', cycles: 'Cicli', temperature: 'Temperatura',
166-
status: 'Stato', session: 'Sessione', current: 'Corrente',
167-
solar_share: 'Quota solare', strategy: 'Strategia', peak: 'Picco',
168-
devices: 'Dispositivi', active: 'Attivi', cost: 'Costo', saved: 'Risparmiato', net: 'Netto',
169-
score: 'Punteggio', co2: 'CO₂', self_use: 'Autoconsumo',
170-
171-
charge_today: 'Carica oggi', discharge_today: 'Scarica oggi',
172-
savings_today: 'Risparmio oggi',
173-
174-
session_cost: 'Costo sessione', no_vehicle: 'Nessun veicolo connesso',
175-
176-
tariff: 'Tariffa', night: 'Notte', surplus: 'Eccedenza', ev: 'VE',
177-
ht: 'FP', nt: 'FV',
178-
179-
mode: 'Modalità', off: 'Spento', peak_only: 'Solo picco',
180-
surplus_mode: 'Eccedenza', critical: 'Critico',
181-
182-
kwh: 'kWh', w: 'W', kw: 'kW',
183-
},
184-
nl: {
185-
charging: 'Laden', discharging: 'Ontladen', idle: 'Inactief',
186-
connected: 'Verbonden', disconnected: 'Ontkoppeld',
187-
importing: 'Import', exporting: 'Export', grid: 'Net',
188-
189-
home: 'Home', energy: 'Energie', battery: 'Batterij',
190-
ev_charging: 'EV laden', control: 'Bediening', costs: 'Kosten', system: 'Systeem',
191-
192-
home_sub: 'Energieoverzicht', energy_sub: 'Productie & verbruik',
193-
battery_sub: 'Opslag & gezondheid', ev_sub: 'Voertuig & sessies',
194-
control_sub: 'Apparaten & planning', costs_sub: 'Besparing & tarieven',
195-
system_sub: 'Gezondheid & diagnose',
196-
197-
solar: 'Zon', autarky: 'Autarkie', today: 'Vandaag', soc: 'SOC',
198-
power: 'Vermogen', health: 'Gezondheid', cycles: 'Cycli', temperature: 'Temperatuur',
199-
status: 'Status', session: 'Sessie', current: 'Stroom',
200-
solar_share: 'Zonneaandeel', strategy: 'Strategie', peak: 'Piek',
201-
devices: 'Apparaten', active: 'Actief', cost: 'Kosten', saved: 'Bespaard', net: 'Netto',
202-
score: 'Score', co2: 'CO₂', self_use: 'Eigenverbruik',
203-
204-
charge_today: 'Laden vandaag', discharge_today: 'Ontladen vandaag',
205-
savings_today: 'Besparing vandaag',
206-
207-
session_cost: 'Sessiekosten', no_vehicle: 'Geen voertuig verbonden',
208-
209-
tariff: 'Tarief', night: 'Nacht', surplus: 'Overschot', ev: 'EV',
210-
ht: 'HT', nt: 'LT',
211-
212-
mode: 'Modus', off: 'Uit', peak_only: 'Alleen piek',
213-
surplus_mode: 'Overschot', critical: 'Kritiek',
12+
/**
13+
* Load translations from the JSON file (async, cached).
14+
*/
15+
function _loadTranslations() {
16+
if (_loadPromise) return _loadPromise;
17+
_loadPromise = fetch('/local/custom_components/solar_energy_management/dashboard/translations.json')
18+
.then(r => r.json())
19+
.then(data => {
20+
SEM_TRANSLATIONS = data;
21+
window.SEM_TRANSLATIONS = data;
22+
return data;
23+
})
24+
.catch(() => {
25+
// Fallback: minimal English if fetch fails
26+
SEM_TRANSLATIONS = { en: {} };
27+
window.SEM_TRANSLATIONS = SEM_TRANSLATIONS;
28+
return SEM_TRANSLATIONS;
29+
});
30+
return _loadPromise;
31+
}
21432

215-
kwh: 'kWh', w: 'W', kw: 'kW',
216-
},
217-
};
33+
// Start loading immediately
34+
_loadTranslations();
21835

21936
/**
22037
* Get translated string for the given key and language.
22138
* Falls back to English if key not found in target language.
39+
* Returns the key itself if no translation loaded yet.
22240
*/
22341
function semLocalize(key, lang) {
224-
const t = SEM_TRANSLATIONS[lang] || SEM_TRANSLATIONS.en;
225-
return t[key] || SEM_TRANSLATIONS.en[key] || key;
42+
if (!SEM_TRANSLATIONS) return key;
43+
const t = SEM_TRANSLATIONS[lang] || SEM_TRANSLATIONS.en || {};
44+
return t[key] || (SEM_TRANSLATIONS.en || {})[key] || key;
22645
}
22746

228-
// Export globally for all SEM cards
47+
// Export globally
22948
if (typeof window !== 'undefined') {
230-
window.SEM_TRANSLATIONS = SEM_TRANSLATIONS;
23149
window.semLocalize = semLocalize;
50+
window._semLoadTranslations = _loadTranslations;
23251
}

0 commit comments

Comments
 (0)