0.4.0: interfaccia in italiano e inglese, scelta nelle impostazioni
L'inglese è la lingua sorgente delle stringhe; l'italiano sta nel catalogo translations/harbour-aira-it.ts (99 stringhe), che lupdate aggiorna e lrelease compila in build. Di serie la lingua del sistema, con ripiego sull'inglese. - nuova classe Translator: risolve la lingua prima di createView e allinea QLocale::setDefault, così anche i numeri seguono la lingua (23,0 °C in italiano, 23.0 °C in inglese) — le temperature passano da formatValue, non più dal toFixed() di JavaScript che metteva sempre il punto - Qt 5.6 non ha QQmlEngine::retranslate(): al cambio di lingua la radice QML viene ricaricata (setSource ricrea il componente anche a url invariata) - le etichette di stato in airajson.cpp usano QCoreApplication::translate per esteso: lupdate non riconosce una funzione wrapper e le avrebbe perse - selettore della lingua in Impostazioni (Sistema / English / Italiano) - test: 41/41, carica il .qm vero come fa main.cpp e verifica entrambe le lingue, il ripiego e i formati numerici - anteprime bilingui (docs/anteprima/ inglese, it/ italiano), README e PIANO aggiornati, spec RPM con i cataloghi in %files
This commit is contained in:
+185
-49
@@ -110,52 +110,172 @@ def button(d: ImageDraw.ImageDraw, y: int, text: str, primary: bool = False) ->
|
||||
return y + h + 16
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Testi delle anteprime. L'app e' in inglese (lingua sorgente) e italiano, quindi
|
||||
# le anteprime esistono nelle due lingue: quelle di docs/anteprima/ sono in
|
||||
# inglese, quelle di docs/anteprima/it/ in italiano.
|
||||
TEXTS = {
|
||||
"en": {
|
||||
"app": "Aira",
|
||||
"house_temperature": "House temperature",
|
||||
"heating": "Heating",
|
||||
"target": "target",
|
||||
"new_house_target": "New house target",
|
||||
"set_house": "Set house to {v} °C",
|
||||
"dhw_section": "Domestic hot water",
|
||||
"dhw_target": "DHW target",
|
||||
"target_label": "Target",
|
||||
"set_acs": "Set DHW target",
|
||||
"start_boost": "Start DHW boost",
|
||||
"stop_boost": "Stop DHW boost",
|
||||
"plant": "Plant",
|
||||
"outdoor": "Outdoor",
|
||||
"indoor_pump": "Indoor (pump)",
|
||||
"pull_to_refresh": "Pull down to refresh",
|
||||
"settings": "Settings",
|
||||
"language": "Language",
|
||||
"system": "System",
|
||||
"bridge": "Bridge",
|
||||
"address_label": "Bridge address",
|
||||
"token_label": "Bridge token",
|
||||
"settings_note": "The address and the token stay in the local\n"
|
||||
"configuration of the app, read-only for others.",
|
||||
"test_connection": "Test connection",
|
||||
"connection_ok": "Bridge reachable",
|
||||
"save": "Save",
|
||||
"cover_target": "target",
|
||||
"cover_acs": "DHW",
|
||||
"cover_outdoor": "outdoor",
|
||||
"cover_footer": "house temperature",
|
||||
"other_commands": "Other commands",
|
||||
"current_state": "Current state",
|
||||
"away": "Away mode",
|
||||
"night_mode": "Night mode",
|
||||
"forced_heating": "Forced heating",
|
||||
"hot_water_active": "Hot water active",
|
||||
"pump": "Pump",
|
||||
"stopped": "Stopped",
|
||||
"allowed_modes": "Allowed modes",
|
||||
"on": "yes",
|
||||
"off": "no",
|
||||
"turn_away_on": "Turn away mode on",
|
||||
"turn_away_off": "Turn away mode off",
|
||||
"noise": "Noise",
|
||||
"night_one_hour": "Night mode for one hour",
|
||||
"force_heating": "Force heating",
|
||||
"confirm_hint": "(asks for confirmation first)",
|
||||
},
|
||||
"it": {
|
||||
"app": "Aira",
|
||||
"house_temperature": "Temperatura in casa",
|
||||
"heating": "Riscaldamento",
|
||||
"target": "obiettivo",
|
||||
"new_house_target": "Nuovo obiettivo casa",
|
||||
"set_house": "Imposta casa a {v} °C",
|
||||
"dhw_section": "Acqua calda sanitaria",
|
||||
"dhw_target": "Obiettivo acqua calda",
|
||||
"target_label": "Obiettivo ACS",
|
||||
"set_acs": "Imposta obiettivo ACS",
|
||||
"start_boost": "Avvia boost ACS",
|
||||
"stop_boost": "Ferma boost ACS",
|
||||
"plant": "Impianto",
|
||||
"outdoor": "Esterna",
|
||||
"indoor_pump": "Interna (pompa)",
|
||||
"pull_to_refresh": "Trascina verso il basso per aggiornare",
|
||||
"settings": "Impostazioni",
|
||||
"language": "Lingua",
|
||||
"system": "Sistema",
|
||||
"bridge": "Bridge",
|
||||
"address_label": "Indirizzo del bridge",
|
||||
"token_label": "Token del bridge",
|
||||
"settings_note": "Indirizzo e token restano nella configurazione\n"
|
||||
"locale dell'app, in sola lettura per gli altri.",
|
||||
"test_connection": "Prova connessione",
|
||||
"connection_ok": "Bridge raggiungibile",
|
||||
"save": "Salva",
|
||||
"cover_target": "obiettivo",
|
||||
"cover_acs": "ACS",
|
||||
"cover_outdoor": "esterna",
|
||||
"cover_footer": "temperatura in casa",
|
||||
"other_commands": "Altri comandi",
|
||||
"current_state": "Stato attuale",
|
||||
"away": "Assenza",
|
||||
"night_mode": "Night mode",
|
||||
"forced_heating": "Riscaldamento forzato",
|
||||
"hot_water_active": "Acqua calda attiva",
|
||||
"pump": "Pompa",
|
||||
"stopped": "Ferma",
|
||||
"allowed_modes": "Modalità consentite",
|
||||
"on": "sì",
|
||||
"off": "no",
|
||||
"turn_away_on": "Attiva assenza",
|
||||
"turn_away_off": "Disattiva assenza",
|
||||
"noise": "Rumore",
|
||||
"night_one_hour": "Night mode per un'ora",
|
||||
"force_heating": "Forza riscaldamento",
|
||||
"confirm_hint": "(chiede conferma prima di partire)",
|
||||
},
|
||||
}
|
||||
|
||||
LANG = "en"
|
||||
|
||||
|
||||
def T(key: str) -> str:
|
||||
return TEXTS[LANG][key]
|
||||
|
||||
|
||||
def dec(value: float) -> str:
|
||||
"""Numero a un decimale con il separatore della lingua, come fa l'app."""
|
||||
out = f"{value:.1f}"
|
||||
return out.replace(".", ",") if LANG == "it" else out
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
def main_screen() -> Image.Image:
|
||||
img = background()
|
||||
d = ImageDraw.Draw(img)
|
||||
status_bar(d)
|
||||
page_header(d, "Aira")
|
||||
page_header(d, T("app"))
|
||||
|
||||
y = 156
|
||||
# ---- casa: il dato principale, in cima ------------------------------- #
|
||||
c = "Temperatura in casa"
|
||||
c = T("house_temperature")
|
||||
d.text(((W - d.textlength(c, font=font(24))) / 2, y), c, font=font(24), fill=SECONDARY)
|
||||
y += 40
|
||||
big = "23,0 °C"
|
||||
big = dec(23.0) + " °C"
|
||||
d.text(((W - d.textlength(big, font=font(92, bold=True))) / 2, y),
|
||||
big, font=font(92, bold=True), fill=PRIMARY)
|
||||
y += 116
|
||||
progress(d, y, (23.0 - 5) / (30 - 5))
|
||||
y += 22
|
||||
t = "obiettivo 19,0 °C · Riscaldamento"
|
||||
t = f'{T("target")} {dec(19.0)} °C · {T("heating")}'
|
||||
d.text(((W - d.textlength(t, font=font(24))) / 2, y), t, font=font(24), fill=SECONDARY)
|
||||
y += 46
|
||||
y = slider(d, y, (19.0 - 5) / (30 - 5), "19,0 °C", "Nuovo obiettivo casa")
|
||||
y = button(d, y, "Imposta casa a 19,0 °C", primary=True)
|
||||
y = slider(d, y, (19.0 - 5) / (30 - 5), dec(19.0) + " °C", T("new_house_target"))
|
||||
y = button(d, y, T("set_house").format(v=dec(19.0)), primary=True)
|
||||
|
||||
# ---- acqua calda sanitaria ------------------------------------------- #
|
||||
y = section_header(d, y + 24, "Acqua calda sanitaria")
|
||||
acs = "51,0 °C"
|
||||
y = section_header(d, y + 24, T("dhw_section"))
|
||||
acs = dec(51.0) + " °C"
|
||||
d.text(((W - d.textlength(acs, font=font(56, bold=True))) / 2, y),
|
||||
acs, font=font(56, bold=True), fill=PRIMARY)
|
||||
y += 76
|
||||
progress(d, y, (51.0 - 20) / (65 - 20))
|
||||
y += 22
|
||||
y = label_pair(d, y, "Obiettivo acqua calda", "55,0 °C")
|
||||
y = slider(d, y + 10, (55.0 - 45) / (60 - 45), "55 °C")
|
||||
y = button(d, y, "Imposta obiettivo ACS", primary=True)
|
||||
y = button(d, y, "Avvia boost ACS")
|
||||
y = button(d, y, "Ferma boost ACS")
|
||||
y = label_pair(d, y, T("dhw_target"), dec(55.0) + " °C")
|
||||
y = slider(d, y + 10, (55.0 - 45) / (60 - 45), "55 °C", T("target_label"))
|
||||
y = button(d, y, T("set_acs"), primary=True)
|
||||
y = button(d, y, T("start_boost"))
|
||||
y = button(d, y, T("stop_boost"))
|
||||
|
||||
# ---- stato impianto (scorre sotto) ----------------------------------- #
|
||||
y = section_header(d, y + 24, "Impianto")
|
||||
y = label_pair(d, y, "Esterna", "18,0 °C")
|
||||
y = label_pair(d, y, "Interna (pompa)", "22,5 °C")
|
||||
y = section_header(d, y + 24, T("plant"))
|
||||
y = label_pair(d, y, T("outdoor"), dec(18.0) + " °C")
|
||||
y = label_pair(d, y, T("indoor_pump"), dec(22.5) + " °C")
|
||||
|
||||
d.text(((W - d.textlength("Trascina verso il basso per aggiornare",
|
||||
font=font(22))) / 2, y + 8),
|
||||
"Trascina verso il basso per aggiornare", font=font(22), fill=SECONDARY)
|
||||
hint = T("pull_to_refresh")
|
||||
d.text(((W - d.textlength(hint, font=font(22))) / 2, y + 8),
|
||||
hint, font=font(22), fill=SECONDARY)
|
||||
return img
|
||||
|
||||
|
||||
@@ -163,11 +283,17 @@ def settings_screen() -> Image.Image:
|
||||
img = background()
|
||||
d = ImageDraw.Draw(img)
|
||||
status_bar(d)
|
||||
page_header(d, "Impostazioni")
|
||||
page_header(d, T("settings"))
|
||||
|
||||
y = 172
|
||||
for lab, val in (("Indirizzo del bridge", "192.168.3.21:8790"),
|
||||
("Token del bridge", "••••••••••••••••••••••••••••")):
|
||||
# ---- lingua ----------------------------------------------------------- #
|
||||
y = section_header(d, 156, T("language"))
|
||||
y = label_pair(d, y, T("language"), T("system"))
|
||||
y += 16
|
||||
|
||||
# ---- bridge ----------------------------------------------------------- #
|
||||
y = section_header(d, y, T("bridge"))
|
||||
for lab, val in ((T("address_label"), "192.168.3.21:8790"),
|
||||
(T("token_label"), "••••••••••••••••••••••••••••")):
|
||||
d.text((M, y), lab, font=font(24), fill=SECONDARY)
|
||||
y += 36
|
||||
d.text((M, y), val, font=font(30), fill=PRIMARY)
|
||||
@@ -175,14 +301,12 @@ def settings_screen() -> Image.Image:
|
||||
d.line([(M, y), (W - M, y)], fill=BTN_EDGE, width=2)
|
||||
y += 44
|
||||
|
||||
d.multiline_text((M, y), "Indirizzo e token restano nella configurazione\n"
|
||||
"locale dell'app, in sola lettura per gli altri.",
|
||||
font=font(22), fill=SECONDARY, spacing=10)
|
||||
d.multiline_text((M, y), T("settings_note"), font=font(22), fill=SECONDARY, spacing=10)
|
||||
y += 104
|
||||
y = button(d, y, "Prova connessione", primary=True)
|
||||
d.text((M, y), "Connessione riuscita (bridge 0.1.0)", font=font(24), fill=OK)
|
||||
y = button(d, y, T("test_connection"), primary=True)
|
||||
d.text((M, y), T("connection_ok"), font=font(24), fill=OK)
|
||||
y += 58
|
||||
y = button(d, y, "Salva")
|
||||
y = button(d, y, T("save"))
|
||||
return img
|
||||
|
||||
|
||||
@@ -199,20 +323,22 @@ def cover_screen() -> Image.Image:
|
||||
# copertina
|
||||
card = [96, 430, W - 96, 430 + 470]
|
||||
d.rounded_rectangle(card, radius=34, fill=(14, 18, 24), outline=ACCENT, width=2)
|
||||
d.text((card[0] + 34, card[1] + 30), "Aira", font=font(40, bold=True), fill=PRIMARY)
|
||||
d.text((card[0] + 34, card[1] + 30), T("app"), font=font(40, bold=True), fill=PRIMARY)
|
||||
d.line([(card[0] + 34, card[1] + 100), (card[2] - 34, card[1] + 100)],
|
||||
fill=BTN_EDGE, width=2)
|
||||
y = card[1] + 132
|
||||
big = "23,0 °C"
|
||||
big = dec(23.0) + " °C"
|
||||
d.text(((W - d.textlength(big, font=font(76, bold=True))) / 2, y),
|
||||
big, font=font(76, bold=True), fill=PRIMARY)
|
||||
y += 108
|
||||
for lab, val in (("obiettivo", "19,0 °C"), ("ACS", "51,0 °C"), ("esterna", "18,0 °C")):
|
||||
for lab, val in ((T("cover_target"), dec(19.0) + " °C"),
|
||||
(T("cover_acs"), dec(51.0) + " °C"),
|
||||
(T("cover_outdoor"), dec(18.0) + " °C")):
|
||||
d.text((card[0] + 34, y), lab, font=font(24), fill=SECONDARY)
|
||||
vw = d.textlength(val, font=font(24))
|
||||
d.text((card[2] - 34 - vw, y), val, font=font(24), fill=PRIMARY)
|
||||
y += 52
|
||||
d.text((card[0] + 34, card[3] - 60), "temperatura in casa", font=font(22), fill=SECONDARY)
|
||||
d.text((card[0] + 34, card[3] - 60), T("cover_footer"), font=font(22), fill=SECONDARY)
|
||||
return img
|
||||
|
||||
|
||||
@@ -220,32 +346,42 @@ def commands_screen() -> Image.Image:
|
||||
img = background()
|
||||
d = ImageDraw.Draw(img)
|
||||
status_bar(d)
|
||||
page_header(d, "Altri comandi")
|
||||
page_header(d, T("other_commands"))
|
||||
|
||||
y = section_header(d, 156, "Stato attuale")
|
||||
for lab, val in (("Assenza", "no"), ("Night mode", "no"),
|
||||
("Riscaldamento forzato", "no"), ("Acqua calda attiva", "sì"),
|
||||
("Pompa", "Ferma"), ("Modalità consentite", "Riscaldamento")):
|
||||
y = section_header(d, 156, T("current_state"))
|
||||
for lab, val in ((T("away"), T("off")), (T("night_mode"), T("off")),
|
||||
(T("forced_heating"), T("off")), (T("hot_water_active"), T("on")),
|
||||
(T("pump"), T("stopped")), (T("allowed_modes"), T("heating"))):
|
||||
y = label_pair(d, y, lab, val)
|
||||
|
||||
y = section_header(d, y + 24, "Assenza")
|
||||
y = button(d, y, "Attiva assenza")
|
||||
y = button(d, y, "Disattiva assenza")
|
||||
y = section_header(d, y + 24, T("away"))
|
||||
y = button(d, y, T("turn_away_on"))
|
||||
y = button(d, y, T("turn_away_off"))
|
||||
|
||||
y = section_header(d, y + 24, "Rumore")
|
||||
y = button(d, y, "Night mode per un'ora")
|
||||
y = section_header(d, y + 24, T("noise"))
|
||||
y = button(d, y, T("night_one_hour"))
|
||||
|
||||
y = section_header(d, y + 24, "Riscaldamento")
|
||||
y = button(d, y, "Forza riscaldamento", primary=True)
|
||||
d.text(((W - d.textlength("(chiede conferma prima di partire)", font=font(22))) / 2, y - 4),
|
||||
"(chiede conferma prima di partire)", font=font(22), fill=SECONDARY)
|
||||
y = section_header(d, y + 24, T("heating"))
|
||||
y = button(d, y, T("force_heating"), primary=True)
|
||||
hint = T("confirm_hint")
|
||||
d.text(((W - d.textlength(hint, font=font(22))) / 2, y - 4), hint, font=font(22), fill=SECONDARY)
|
||||
return img
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
import sys
|
||||
|
||||
lang = "en"
|
||||
if "--lang" in sys.argv:
|
||||
lang = sys.argv[sys.argv.index("--lang") + 1]
|
||||
if lang not in TEXTS:
|
||||
raise SystemExit(f"lingua non disponibile: {lang}")
|
||||
LANG = lang
|
||||
|
||||
out = pathlib.Path("docs/anteprima") if lang == "en" else pathlib.Path("docs/anteprima") / lang
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
for name, fn in (("dashboard", main_screen), ("comandi", commands_screen),
|
||||
("impostazioni", settings_screen), ("copertina", cover_screen)):
|
||||
path = OUT / f"{name}.png"
|
||||
path = out / f"{name}.png"
|
||||
fn().save(path)
|
||||
print(f"scritto {path} ({path.stat().st_size} byte)")
|
||||
|
||||
Reference in New Issue
Block a user