diff --git a/.gitignore b/.gitignore index 8cad61b..e69453e 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ bridge/.venv/ __pycache__/ *.pyc +# cataloghi compilati: si rigenerano in build da translations/*.ts +translations/*.qm + # temporanei *~ *.bak diff --git a/README.it.md b/README.it.md index 9aa8d55..59f061a 100644 --- a/README.it.md +++ b/README.it.md @@ -10,8 +10,37 @@ App nativa Sailfish OS per una pompa di calore **Aira Home**: mostra lo stato dell'impianto e invia i comandi dell'acqua calda sanitaria e del riscaldamento (temperatura obiettivo della casa, per zona). +L'interfaccia è in **inglese e italiano**: di serie segue la lingua del telefono +e ripiega sull'inglese; la si può scegliere anche nelle impostazioni dell'app. + ![architettura](docs/architettura.svg) +Anteprime del layout (valori veri dell'impianto, **non** screenshot — vedi +*Stato*). Queste sono in italiano; quelle in inglese stanno in +`docs/anteprima/`: + +| Home | Altri comandi | Impostazioni | Copertina | +|---|---|---|---| +| ![home](docs/anteprima/it/dashboard.png) | ![comandi](docs/anteprima/it/comandi.png) | ![impostazioni](docs/anteprima/it/impostazioni.png) | ![copertina](docs/anteprima/it/copertina.png) | + +## La lingua dell'interfaccia + +L'inglese è la **lingua sorgente** delle stringhe (sta direttamente nel codice); +l'italiano sta nel catalogo `translations/harbour-aira-it.ts`, che `lupdate` +aggiorna e `lrelease` compila in build. + +La scelta fatta nelle impostazioni è salvata in `QSettings` con il codice della +lingua; vuoto (o lingua assente) significa **lingua di sistema**, con ripiego +sull'inglese quando non esiste il catalogo corrispondente. + +Qt 5.6 non ha `QQmlEngine::retranslate()` (arriva con Qt 5.10): installare un +traduttore non basta a ritradurre quello che è già a schermo. L'app quindi, al +cambio di lingua, **ricarica la radice QML** — la scelta ha effetto subito, al +prezzo di tornare alla pagina iniziale. + +Anche i numeri seguono la lingua: le temperature passano da `QLocale`, quindi in +italiano si scrive `23,0 °C` e in inglese `23.0 °C`. + ## Perché c'è un bridge (e non una chiamata diretta al cloud) Il cloud Aira espone **gRPC su HTTP/2**. Qt 5.6 — il toolkit su cui gira ogni @@ -82,8 +111,14 @@ d'errore. **La UI Silica si verifica solo sul dispositivo.** ## Stato -- **31/31** verifiche del nucleo, compilazione senza warning +- **39/39** verifiche del nucleo, compilazione senza warning - **27/27** verifiche offline del bridge (`bridge/test_bridge_commands.py`) +- **99/99** stringhe tradotte nel catalogo italiano: il test carica il `.qm` + compilato come fa `main.cpp` e verifica che in inglese l'etichetta di stato sia + la sorgente e in italiano quella tradotta, e che togliendo il catalogo si torni + all'inglese +- numeri formattati con `QLocale`: il separatore decimale segue la lingua + (verificato per entrambe) - `qmllint` pulito su tutti i QML (valida solo la sintassi: senza il modulo Silica una proprietà inesistente passa in silenzio) - provata dal vivo in **sola lettura** su un impianto Aira Home @@ -102,7 +137,9 @@ harbour-aira.pro progetto qmake (icona, qml, docs) src/airajson.{h,cpp} logica pura, testabile senza SDK src/apiclient.{h,cpp} QNetworkAccessManager + watchdog di timeout src/settings.{h,cpp} QSettings su AppConfigLocation (sandbox SailJail) -src/main.cpp context properties: appSettings, api +src/translator.{h,cpp} lingua dell'interfaccia: locale di sistema, ripiego + sull'inglese, ricarica della UI al cambio +src/main.cpp context properties: appSettings, api, translator qml/harbour-aira.qml ApplicationWindow + copertina qml/pages/MainPage.qml dashboard e comandi acqua/riscaldamento qml/pages/CommandsPage.qml gli altri comandi, con lo stato attuale in cima @@ -118,7 +155,8 @@ bridge/harbour-aira-bridge.service unit systemd utente bridge/README.md istruzioni di installazione del bridge (inglese) docs/PIANO.md piano di progetto e scelte docs/PROTOCOL.md API del bridge -docs/anteprima/ anteprime di layout +translations/ harbour-aira-it.ts (l'inglese è la lingua sorgente) +docs/anteprima/ anteprime di layout in inglese (in italiano: it/) ``` ## Licenza diff --git a/README.md b/README.md index 05a7747..178184f 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,18 @@ status, domestic hot water, and the house target temperature per zone. - **Cover page** showing the house temperature - Address and bridge token live in the app settings (QSettings), never in the binary +- **Languages: English and Italian.** The interface follows the phone language by + default and falls back to English when there is no catalogue for it. The + language can also be picked in the settings; the app reloads to apply it, and + numbers follow the chosen language too (decimal comma in Italian, dot in + English) Layout previews (rendered with the real values the plant returned, **not** screenshots — see *Status*): -| Home | Other commands | Cover | -|---|---|---| -| ![home](docs/anteprima/dashboard.png) | ![commands](docs/anteprima/comandi.png) | ![cover](docs/anteprima/copertina.png) | +| Home | Other commands | Settings | Cover | +|---|---|---|---| +| ![home](docs/anteprima/dashboard.png) | ![commands](docs/anteprima/comandi.png) | ![settings](docs/anteprima/impostazioni.png) | ![cover](docs/anteprima/copertina.png) | ## Why there is a bridge @@ -107,10 +112,15 @@ parsing, mode labels, command payloads, error messages. Verified so far: -- **31/31** core checks (`tests/core_test.cpp`), compiled with no warnings +- **39/39** core checks (`tests/core_test.cpp`), compiled with no warnings - **27/27** bridge checks, offline (`bridge/test_bridge_commands.py`) — including that a zone command always carries every zone, and that absurd values are refused (40 °C, zone 3, text) +- **99/99** strings translated in the Italian catalogue, checked by loading the + compiled `.qm` in the test the same way `main.cpp` does: source English in, + Italian out, and back to English when the catalogue is removed +- numbers formatted through `QLocale`, so the decimal separator follows the + language (verified for both) - `qmllint` clean on every QML file. Note it validates syntax only: without the Silica module an invalid Silica property passes silently - read-only queries against one real Aira Home unit, through the bridge @@ -120,6 +130,12 @@ Not verified: **the app has not been built or run on a phone yet** — the first build is up to whoever has the Sailfish SDK. The images in `docs/anteprima/` are layout previews built from the real plant values, not screenshots. +A Qt 5.6 note on the language switch: `QQmlEngine::retranslate()` only exists +from Qt 5.10, so installing a translator is not enough to re-translate what is +already on screen. The app therefore reloads the QML root when the language +changes: the setting applies immediately, at the cost of landing back on the home +page. + Caveats: - **Unofficial client**, not in the Jolla Store: it talks to Aira's API with @@ -152,7 +168,8 @@ harbour-aira.pro qmake project (icons, qml, docs) src/airajson.{h,cpp} pure logic, testable without the SDK src/apiclient.{h,cpp} QNetworkAccessManager + timeout watchdog src/settings.{h,cpp} QSettings in AppConfigLocation (SailJail sandbox) -src/main.cpp context properties: appSettings, api +src/translator.{h,cpp} UI language: system locale, English fallback, reload +src/main.cpp context properties: appSettings, api, translator qml/harbour-aira.qml ApplicationWindow + cover qml/pages/MainPage.qml dashboard and water/heating commands qml/pages/CommandsPage.qml the other commands, current state on top @@ -161,9 +178,10 @@ qml/components/InfoRow.qml qml/components/ActionButton.qml command button, optional confirmation tests/core_test.cpp core checks (headless) bridge/ local HTTP bridge -> see bridge/README.md +translations/ harbour-aira-it.ts (English is the source language) docs/PIANO.md project plan and decisions (Italian) docs/PROTOCOL.md bridge API -docs/anteprima/ layout previews +docs/anteprima/ layout previews (English; docs/anteprima/it/ Italian) ``` ## License diff --git a/docs/PIANO.md b/docs/PIANO.md index 34ea1bf..9d283aa 100644 --- a/docs/PIANO.md +++ b/docs/PIANO.md @@ -8,7 +8,7 @@ Piano scritto il 9 ottobre 2026, prima di scrivere la UI (convenzione di lavoro) |---|---| | Accesso cloud Aira | **funziona** — `pyairahome` 2.3.0, pool Cognito `eu-north-1_cnqyjWtbz` (indice 0), token salvati in `~/.hermes/profiles/carlo/aira_tokens.json` (600) | | Lettura impianto | **funziona** — esterna, interna, ACS attuale/obiettivo, modalità, curve zone, firmware, errori | -| Bridge HTTP sul server | **scritto e provato** — `~/workspace/Carlo/Aira/aira_bridge.py`, v0.1.0 | +| Bridge HTTP sul server | **scritto e provato** — `~/workspace/Carlo/Aira/aira_bridge.py`, v**0.3.0** (servizio systemd utente attivo) | | Bluetooth sul server | **assente** — nessun adattatore HCI (esclude la via locale BLE su questa macchina) | | SDK Sailfish (`sfdk`) | **assente qui** — build e deploy li fa Carlo dal suo PC | | Jolla Phone 2026 | non raggiungibile dalla rete del server: deploy solo dal PC | @@ -119,6 +119,29 @@ Non verificabile qui (serve il suo PC + il telefono): build RPM, istallazione, U solo per LAN e VPN. *Lezione*: un test da `127.0.0.1` non attraversa quelle regole e non rivela il problema — va provato dalla LAN. +- **App 0.4.0 — localizzazione ita/eng** (richiesta dell'utente): l'interfaccia si + può mettere in italiano o inglese **dalle impostazioni**; di serie segue la lingua + del sistema, con **ripiego sull'inglese**. L'inglese è la lingua sorgente delle + stringhe (restano nel codice), l'italiano sta in `translations/harbour-aira-it.ts` + (**99 stringhe**, aggiornate con `lupdate` e compilate da `lrelease` in build). + Nuova classe `Translator` (`src/translator.{h,cpp}`): risolve la lingua **prima** + di `createView`, così le `qsTr()` del QML nascono già tradotte, e allinea + `QLocale::setDefault` alla lingua scelta — così i numeri seguono la lingua + (`23,0 °C` in italiano, `23.0 °C` in inglese): le temperature si formattano in + `ApiClient::formatValue`, non più con il `toFixed()` di JavaScript, che avrebbe + sempre messo il punto. *Vincolo di piattaforma*: **Qt 5.6 non ha + `QQmlEngine::retranslate()`** (arriva con Qt 5.10), quindi installare il + traduttore non ritraduce ciò che è già a schermo: al cambio lingua l'app + **ricarica la radice QML**. *Trappola di `lupdate`*: riconosce solo le funzioni di + traduzione note (`tr()`, `QCoreApplication::translate()`), **non una funzione + wrapper scritta da noi** — le etichette di `airajson.cpp` sono quindi chiamate + `QCoreApplication::translate("AiraJson", …)` per esteso, altrimenti sparirebbero + dal catalogo. Nucleo app: **39/39** (le attese sono cambiate in inglese e il test + carica il `.qm` vero come fa `main.cpp`, verificando entrambe le lingue, il + ripiego e i formati numerici). Anteprime rigenerate in **due lingue** + (`docs/anteprima/` inglese, `docs/anteprima/it/` italiano), impostazioni comprese + la nuova riga della lingua. + ## 6. Rischi noti - **Dipendenza dal server**: se il bridge è spento, l'app non vede nulla. Mitigazione: unità diff --git a/docs/anteprima/comandi.png b/docs/anteprima/comandi.png index 8177449..336be48 100644 Binary files a/docs/anteprima/comandi.png and b/docs/anteprima/comandi.png differ diff --git a/docs/anteprima/copertina.png b/docs/anteprima/copertina.png index 598e421..da53351 100644 Binary files a/docs/anteprima/copertina.png and b/docs/anteprima/copertina.png differ diff --git a/docs/anteprima/dashboard.png b/docs/anteprima/dashboard.png index 6164799..ec60bd3 100644 Binary files a/docs/anteprima/dashboard.png and b/docs/anteprima/dashboard.png differ diff --git a/docs/anteprima/impostazioni.png b/docs/anteprima/impostazioni.png index 9fe7f96..8c8490b 100644 Binary files a/docs/anteprima/impostazioni.png and b/docs/anteprima/impostazioni.png differ diff --git a/docs/anteprima/it/comandi.png b/docs/anteprima/it/comandi.png new file mode 100644 index 0000000..8177449 Binary files /dev/null and b/docs/anteprima/it/comandi.png differ diff --git a/docs/anteprima/it/copertina.png b/docs/anteprima/it/copertina.png new file mode 100644 index 0000000..598e421 Binary files /dev/null and b/docs/anteprima/it/copertina.png differ diff --git a/docs/anteprima/it/dashboard.png b/docs/anteprima/it/dashboard.png new file mode 100644 index 0000000..6164799 Binary files /dev/null and b/docs/anteprima/it/dashboard.png differ diff --git a/docs/anteprima/it/impostazioni.png b/docs/anteprima/it/impostazioni.png new file mode 100644 index 0000000..908049d Binary files /dev/null and b/docs/anteprima/it/impostazioni.png differ diff --git a/harbour-aira.pro b/harbour-aira.pro index fd3951a..7928a8d 100644 --- a/harbour-aira.pro +++ b/harbour-aira.pro @@ -12,11 +12,13 @@ QT += network SOURCES += \ src/main.cpp \ src/settings.cpp \ + src/translator.cpp \ src/apiclient.cpp \ src/airajson.cpp HEADERS += \ src/settings.h \ + src/translator.h \ src/apiclient.h \ src/airajson.h @@ -31,9 +33,18 @@ OTHER_FILES += \ DISTFILES += $$OTHER_FILES -# Niente catalogo di traduzione in 0.1.0: le stringhe sorgente sono in italiano -# (app personale). Se un giorno serve lo store Harbour si aggiungono i .ts e la -# compilazione con lrelease dentro lo spec, come negli altri progetti. +# Traduzioni: l'inglese e' la lingua sorgente (nessun .qm: Qt lascia le stringhe +# originali quando per il locale non c'e' catalogo = ripiego automatico), mentre +# l'italiano sta in translations/harbour-aira-it.ts. +# Con sailfishapp_i18n qmake compila i .ts in .qm e li installa in +# /usr/share//translations, che e' dove li cerca src/translator.cpp. +# NIENTE .qm committati: si rigenerano in build. +CONFIG += sailfishapp_i18n + +TRANSLATIONS += \ + translations/harbour-aira-it.ts + +OTHER_FILES += translations/*.ts # Icone: struttura standard hicolor, path assoluti (gli install manuali con # path relativi falliscono quando il tar sorgente e' incompleto). diff --git a/qml/cover/CoverPage.qml b/qml/cover/CoverPage.qml index 68e1f60..215aaed 100644 --- a/qml/cover/CoverPage.qml +++ b/qml/cover/CoverPage.qml @@ -16,7 +16,7 @@ CoverBackground { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.topMargin: Theme.paddingLarge - text: qsTr("Aira") + text: "Aira" color: Theme.primaryColor font.pixelSize: Theme.fontSizeLarge } @@ -37,7 +37,7 @@ CoverBackground { Label { width: parent.width horizontalAlignment: Text.AlignHCenter - text: qsTr("casa · obiettivo %1 °C").arg(cover.t("zone1_setpoint")) + text: qsTr("house · target %1 °C").arg(cover.t("zone1_setpoint")) color: Theme.secondaryColor font.pixelSize: Theme.fontSizeExtraSmall truncationMode: TruncationMode.Fade @@ -46,7 +46,7 @@ CoverBackground { Label { width: parent.width horizontalAlignment: Text.AlignHCenter - text: qsTr("ACS %1 °C · obiettivo %2 °C").arg(cover.t("dhw_current")).arg(cover.t("dhw_target")) + text: qsTr("DHW %1 °C · target %2 °C").arg(cover.t("dhw_current")).arg(cover.t("dhw_target")) color: Theme.secondaryColor font.pixelSize: Theme.fontSizeExtraSmall truncationMode: TruncationMode.Fade @@ -57,8 +57,8 @@ CoverBackground { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom anchors.bottomMargin: Theme.paddingMedium - text: qsTr("est. %1 °C").arg(cover.t("outdoor_temperature")) + text: qsTr("outdoor %1 °C").arg(cover.t("outdoor_temperature")) color: Theme.secondaryColor font.pixelSize: Theme.fontSizeExtraSmall } -} +} \ No newline at end of file diff --git a/qml/harbour-aira.qml b/qml/harbour-aira.qml index 7b3bf78..3dd6770 100644 --- a/qml/harbour-aira.qml +++ b/qml/harbour-aira.qml @@ -14,4 +14,4 @@ ApplicationWindow { // Nessun allowedOrientations qui: a livello di window limita TUTTE le // pagine (intersezione), anche quelle che dichiarano Orientation.All. -} +} \ No newline at end of file diff --git a/qml/pages/CommandsPage.qml b/qml/pages/CommandsPage.qml index 5440eb6..b98e420 100644 --- a/qml/pages/CommandsPage.qml +++ b/qml/pages/CommandsPage.qml @@ -17,7 +17,7 @@ Page { } function flag(key) { - return page.raw(key) ? qsTr("sì") : qsTr("no") + return page.raw(key) ? qsTr("yes") : qsTr("no") } function stato(key) { @@ -51,7 +51,7 @@ Page { width: parent.width spacing: Theme.paddingSmall - PageHeader { title: qsTr("Altri comandi") } + PageHeader { title: qsTr("Other commands") } Label { visible: page.notice.length > 0 @@ -64,80 +64,80 @@ Page { } // ---- stato attuale -------------------------------------------- // - SectionHeader { text: qsTr("Stato attuale") } + SectionHeader { text: qsTr("Current state") } - InfoRow { label: qsTr("Assenza"); value: page.flag("away_mode") } + InfoRow { label: qsTr("Away mode"); value: page.flag("away_mode") } InfoRow { label: qsTr("Night mode"); value: page.flag("night_mode") } - InfoRow { label: qsTr("Riscaldamento forzato"); value: page.flag("force_heating") } - InfoRow { label: qsTr("Acqua calda attiva"); value: page.flag("hot_water_heating") } - InfoRow { label: qsTr("Pompa"); value: page.stato("pump_active_state") } - InfoRow { label: qsTr("Modalità consentite"); value: page.stato("configured_modes") } + InfoRow { label: qsTr("Forced heating"); value: page.flag("force_heating") } + InfoRow { label: qsTr("Hot water active"); value: page.flag("hot_water_heating") } + InfoRow { label: qsTr("Pump"); value: page.stato("pump_active_state") } + InfoRow { label: qsTr("Allowed modes"); value: page.stato("configured_modes") } // ---- assenza --------------------------------------------------- // - SectionHeader { text: qsTr("Assenza") } + SectionHeader { text: qsTr("Away mode") } ActionButton { - text: qsTr("Attiva assenza") + text: qsTr("Turn away mode on") onTriggered: api.sendCommand("away_mode_on") } ActionButton { - text: qsTr("Disattiva assenza") + text: qsTr("Turn away mode off") onTriggered: api.sendCommand("away_mode_off") } // ---- rumore ---------------------------------------------------- // - SectionHeader { text: qsTr("Rumore") } + SectionHeader { text: qsTr("Noise") } ActionButton { - text: qsTr("Night mode per un'ora") + text: qsTr("Night mode for one hour") onTriggered: api.sendCommand("night_mode_1h") } // ---- riscaldamento --------------------------------------------- // - SectionHeader { text: qsTr("Riscaldamento") } + SectionHeader { text: qsTr("Heating") } ActionButton { - text: qsTr("Forza riscaldamento") - confirmText: qsTr("Avvio del riscaldamento forzato") + text: qsTr("Force heating") + confirmText: qsTr("Starting forced heating") onTriggered: api.sendCommand("force_heating_on") } ActionButton { - text: qsTr("Ferma riscaldamento forzato") + text: qsTr("Stop forced heating") onTriggered: api.sendCommand("force_heating_off") } ActionButton { - text: qsTr("Abilita funzione riscaldamento") + text: qsTr("Enable heating function") onTriggered: api.sendCommand("heating_on") } ActionButton { - text: qsTr("Disabilita funzione riscaldamento") - confirmText: qsTr("Il riscaldamento verrà disabilitato") + text: qsTr("Disable heating function") + confirmText: qsTr("Heating will be disabled") onTriggered: api.sendCommand("heating_off") } // ---- acqua calda ----------------------------------------------- // - SectionHeader { text: qsTr("Acqua calda sanitaria") } + SectionHeader { text: qsTr("Domestic hot water") } ActionButton { - text: qsTr("Ciclo antilegionella") - confirmText: qsTr("Avvio del ciclo antilegionella: richiede tempo") + text: qsTr("Legionella cycle") + confirmText: qsTr("Starting the legionella cycle: it takes a while") onTriggered: api.sendCommand("legionella_cycle") } ActionButton { - text: qsTr("Abilita riscaldamento ACS") + text: qsTr("Enable DHW heating") onTriggered: api.sendCommand("hot_water_heating_on") } ActionButton { - text: qsTr("Disabilita riscaldamento ACS") - confirmText: qsTr("L'acqua calda smetterà di essere riscaldata") + text: qsTr("Disable DHW heating") + confirmText: qsTr("Domestic hot water will stop being heated") onTriggered: api.sendCommand("hot_water_heating_off") } // ---- diagnostica ----------------------------------------------- // - SectionHeader { text: qsTr("Diagnostica") } + SectionHeader { text: qsTr("Diagnostics") } ActionButton { - text: qsTr("Verifica impianto") + text: qsTr("Plant check") onTriggered: api.sendCommand("ping") } @@ -151,7 +151,7 @@ Page { Label { x: Theme.horizontalPageMargin width: parent.width - 2 * Theme.horizontalPageMargin - text: qsTr("I comandi passano dal bridge e possono richiedere qualche secondo. Dopo l'invio lo stato si aggiorna da solo.") + text: qsTr("Commands go through the bridge and may take a few seconds. The status refreshes by itself afterwards.") color: Theme.secondaryColor font.pixelSize: Theme.fontSizeExtraSmall wrapMode: Text.Wrap @@ -163,4 +163,4 @@ Page { if (api.configured) api.refresh() } -} +} \ No newline at end of file diff --git a/qml/pages/MainPage.qml b/qml/pages/MainPage.qml index 042262d..61a3b06 100644 --- a/qml/pages/MainPage.qml +++ b/qml/pages/MainPage.qml @@ -38,12 +38,16 @@ Page { return api.summary[key] } + // I numeri si formattano nel C++ (ApiClient::formatValue): cosi' il + // separatore decimale segue la lingua dell'app, non il toFixed di JS. + function fmt(value) { return api.formatValue(value) } + function temp(key) { return api.formatValue(page.raw(key)) } function flag(key) { - return page.raw(key) ? qsTr("sì") : qsTr("no") + return page.raw(key) ? qsTr("yes") : qsTr("no") } function count(key) { @@ -98,15 +102,15 @@ Page { // fanno deref su null (TypeError + crash). PullDownMenu { MenuItem { - text: qsTr("Aggiorna") + text: qsTr("Refresh") onClicked: api.refresh() } MenuItem { - text: qsTr("Impostazioni") + text: qsTr("Settings") onClicked: page.openSettings() } MenuItem { - text: qsTr("Altri comandi") + text: qsTr("Other commands") onClicked: page.openCommands() } } @@ -116,7 +120,7 @@ Page { width: parent.width spacing: Theme.paddingSmall - PageHeader { title: qsTr("Aira") } + PageHeader { title: "Aira" } // ---- avvisi ---------------------------------------------------- // Label { @@ -134,7 +138,7 @@ Page { visible: !api.configured x: Theme.horizontalPageMargin width: parent.width - 2 * Theme.horizontalPageMargin - text: qsTr("Bridge non configurato. Apri le impostazioni e inserisci l'indirizzo del server e il token.") + text: qsTr("Bridge not configured. Open the settings and enter the server address and the token.") color: Theme.secondaryColor font.pixelSize: Theme.fontSizeSmall wrapMode: Text.Wrap @@ -143,7 +147,7 @@ Page { Button { visible: !api.configured anchors.horizontalCenter: parent.horizontalCenter - text: qsTr("Impostazioni") + text: qsTr("Settings") onClicked: page.openSettings() } @@ -151,7 +155,7 @@ Page { Label { visible: api.configured anchors.horizontalCenter: parent.horizontalCenter - text: qsTr("Temperatura in casa") + text: qsTr("House temperature") color: Theme.secondaryColor font.pixelSize: Theme.fontSizeSmall } @@ -179,7 +183,7 @@ Page { Label { visible: api.configured anchors.horizontalCenter: parent.horizontalCenter - text: qsTr("obiettivo %1 °C · %2").arg(page.temp("zone1_setpoint")).arg(api.modeLabel(page.raw("zone1_mode"))) + text: qsTr("target %1 °C · %2").arg(page.temp("zone1_setpoint")).arg(api.modeLabel(page.raw("zone1_mode"))) color: Theme.secondaryColor font.pixelSize: Theme.fontSizeExtraSmall } @@ -188,12 +192,12 @@ Page { id: casaSlider visible: api.configured width: parent.width - label: qsTr("Nuovo obiettivo casa") + label: qsTr("New house target") minimumValue: 5 maximumValue: 30 stepSize: 0.5 value: page.targetCasa - valueText: value.toFixed(1) + " °C" + valueText: page.fmt(value) + " °C" onValueChanged: { page.targetCasaTouched = true page.targetCasa = Math.round(value * 2) / 2 @@ -209,14 +213,14 @@ Page { leftMargin: Theme.horizontalPageMargin rightMargin: Theme.horizontalPageMargin } - text: qsTr("Imposta casa a %1 °C").arg(page.targetCasa.toFixed(1)) + text: qsTr("Set house to %1 °C").arg(page.fmt(page.targetCasa)) onClicked: remorse.execute(setCasaButton, - qsTr("Temperatura della casa a %1 °C").arg(page.targetCasa.toFixed(1)), + qsTr("House temperature to %1 °C").arg(page.fmt(page.targetCasa)), function() { api.sendCommand("set_heating_setpoint", page.targetCasa) }) } // ---- acqua calda sanitaria ------------------------------------- // - SectionHeader { visible: api.configured; text: qsTr("Acqua calda sanitaria") } + SectionHeader { visible: api.configured; text: qsTr("Domestic hot water") } Label { visible: api.configured @@ -240,19 +244,19 @@ Page { InfoRow { visible: api.configured - label: qsTr("Obiettivo acqua calda") + label: qsTr("DHW target") value: page.temp("dhw_target") + " °C" } Slider { visible: api.configured width: parent.width - label: qsTr("Nuovo obiettivo") + label: qsTr("New target") minimumValue: 45 maximumValue: 60 stepSize: 1 value: page.targetAcs - valueText: value + " °C" + valueText: page.fmt(value) + " °C" onValueChanged: { page.targetAcsTouched = true page.targetAcs = Math.round(value) @@ -268,9 +272,9 @@ Page { leftMargin: Theme.horizontalPageMargin rightMargin: Theme.horizontalPageMargin } - text: qsTr("Imposta obiettivo %1 °C").arg(page.targetAcs) + text: qsTr("Set target %1 °C").arg(page.targetAcs) onClicked: remorse.execute(setAcsButton, - qsTr("Obiettivo ACS a %1 °C").arg(page.targetAcs), + qsTr("DHW target to %1 °C").arg(page.targetAcs), function() { api.sendCommand("set_hot_water_target", page.targetAcs) }) } @@ -283,8 +287,8 @@ Page { leftMargin: Theme.horizontalPageMargin rightMargin: Theme.horizontalPageMargin } - text: qsTr("Avvia boost ACS") - onClicked: remorse.execute(boostOnButton, qsTr("Avvio del boost ACS"), + text: qsTr("Start DHW boost") + onClicked: remorse.execute(boostOnButton, qsTr("Starting DHW boost"), function() { api.sendCommand("hot_water_boost_on") }) } @@ -297,24 +301,24 @@ Page { leftMargin: Theme.horizontalPageMargin rightMargin: Theme.horizontalPageMargin } - text: qsTr("Ferma boost ACS") - onClicked: remorse.execute(boostOffButton, qsTr("Arresto del boost ACS"), + text: qsTr("Stop DHW boost") + onClicked: remorse.execute(boostOffButton, qsTr("Stopping DHW boost"), function() { api.sendCommand("hot_water_boost_off") }) } // ---- stato impianto ------------------------------------------- // - SectionHeader { visible: api.configured; text: qsTr("Impianto") } + SectionHeader { visible: api.configured; text: qsTr("Plant") } - InfoRow { visible: api.configured; label: qsTr("Temperatura esterna"); value: page.temp("outdoor_temperature") + " °C" } - InfoRow { visible: api.configured; label: qsTr("Temperatura interna (pompa)"); value: page.temp("indoor_temperature") + " °C" } - InfoRow { visible: api.configured; label: qsTr("Modalità"); value: api.modeLabel(page.raw("operating_status")) } - InfoRow { visible: api.configured; label: qsTr("Assenza"); value: page.flag("away_mode") } + InfoRow { visible: api.configured; label: qsTr("Outdoor temperature"); value: page.temp("outdoor_temperature") + " °C" } + InfoRow { visible: api.configured; label: qsTr("Indoor temperature (pump)"); value: page.temp("indoor_temperature") + " °C" } + InfoRow { visible: api.configured; label: qsTr("Mode"); value: api.modeLabel(page.raw("operating_status")) } + InfoRow { visible: api.configured; label: qsTr("Away mode"); value: page.flag("away_mode") } InfoRow { visible: api.configured; label: qsTr("Night mode"); value: page.flag("night_mode") } - InfoRow { visible: api.configured; label: qsTr("Riscaldamento forzato"); value: page.flag("force_heating") } - InfoRow { visible: api.configured; label: qsTr("Resistenze integrative"); value: page.flag("inline_heater") } + InfoRow { visible: api.configured; label: qsTr("Forced heating"); value: page.flag("force_heating") } + InfoRow { visible: api.configured; label: qsTr("Inline heater"); value: page.flag("inline_heater") } InfoRow { visible: api.configured; label: qsTr("Zone"); value: page.count("zones") } - InfoRow { visible: api.configured; label: qsTr("Errori"); value: page.count("errors_count") } - InfoRow { visible: api.configured; label: qsTr("Aggiornato alle"); value: page.shortTime(page.raw("pump_updated")) } + InfoRow { visible: api.configured; label: qsTr("Errors"); value: page.count("errors_count") } + InfoRow { visible: api.configured; label: qsTr("Last reading"); value: page.shortTime(page.raw("pump_updated")) } Button { visible: api.configured @@ -324,7 +328,7 @@ Page { leftMargin: Theme.horizontalPageMargin rightMargin: Theme.horizontalPageMargin } - text: qsTr("Altri comandi") + text: qsTr("Other commands") onClicked: page.openCommands() } @@ -339,7 +343,7 @@ Page { visible: api.configured x: Theme.horizontalPageMargin width: parent.width - 2 * Theme.horizontalPageMargin - text: qsTr("I comandi vengono inviati al bridge e da lì all'impianto: possono richiedere qualche secondo.") + text: qsTr("Commands are sent to the bridge and from there to the plant: they may take a few seconds.") color: Theme.secondaryColor font.pixelSize: Theme.fontSizeExtraSmall wrapMode: Text.Wrap @@ -351,4 +355,4 @@ Page { if (api.configured) api.refresh() } -} +} \ No newline at end of file diff --git a/qml/pages/SettingsPage.qml b/qml/pages/SettingsPage.qml index 3b1c599..423e321 100644 --- a/qml/pages/SettingsPage.qml +++ b/qml/pages/SettingsPage.qml @@ -21,7 +21,7 @@ Page { function testAndReport() { save() - page.testResult = qsTr("verifico…") + page.testResult = qsTr("checking…") api.testConnection() } @@ -47,13 +47,55 @@ Page { width: parent.width spacing: Theme.paddingMedium - PageHeader { title: qsTr("Impostazioni") } + PageHeader { title: qsTr("Settings") } + + // ---- lingua ---------------------------------------------------- // + SectionHeader { text: qsTr("Language") } + + // Le voci si scelgono dal menu: il valore mostrato segue la scelta + // salvata (translator.language), non l'ultimo tocco. + ComboBox { + id: langBox + x: Theme.horizontalPageMargin + width: parent.width - 2 * Theme.horizontalPageMargin + label: qsTr("Language") + value: translator.language === "it" ? "Italiano" + : translator.language === "en" ? "English" + : qsTr("System") + menu: ContextMenu { + MenuItem { + text: qsTr("System") + onClicked: translator.language = "" + } + // I nomi delle lingue restano nella lingua che nominano. + MenuItem { + text: "English" + onClicked: translator.language = "en" + } + MenuItem { + text: "Italiano" + onClicked: translator.language = "it" + } + } + } + + Label { + x: Theme.horizontalPageMargin + width: parent.width - 2 * Theme.horizontalPageMargin + text: qsTr("Changing the language reloads the app.") + color: Theme.secondaryColor + font.pixelSize: Theme.fontSizeExtraSmall + wrapMode: Text.Wrap + } + + // ---- bridge ---------------------------------------------------- // + SectionHeader { text: qsTr("Bridge") } TextField { id: urlField x: Theme.horizontalPageMargin width: parent.width - 2 * Theme.horizontalPageMargin - label: qsTr("Indirizzo del bridge") + label: qsTr("Bridge address") placeholderText: "192.168.2.50:8790" text: page.urlText onTextChanged: if (text !== page.urlText) page.urlText = text @@ -68,7 +110,7 @@ Page { id: tokenField x: Theme.horizontalPageMargin width: parent.width - 2 * Theme.horizontalPageMargin - label: qsTr("Token del bridge") + label: qsTr("Bridge token") showEchoModeToggle: true text: page.tokenText onTextChanged: if (text !== page.tokenText) page.tokenText = text @@ -80,7 +122,7 @@ Page { Label { x: Theme.horizontalPageMargin width: parent.width - 2 * Theme.horizontalPageMargin - text: qsTr("Indirizzo e token si leggono sul server: il token è in ~/.hermes/profiles/carlo/aira_bridge.json. Senza schema viene usato http://.") + text: qsTr("Address and token live on the server: the token is in aira_bridge.json next to the bridge. Without a scheme, http:// is used.") color: Theme.secondaryColor font.pixelSize: Theme.fontSizeExtraSmall wrapMode: Text.Wrap @@ -88,17 +130,17 @@ Page { Button { anchors.horizontalCenter: parent.horizontalCenter - text: qsTr("Salva") + text: qsTr("Save") onClicked: { page.save() - page.testResult = qsTr("impostazioni salvate") + page.testResult = qsTr("settings saved") page.testOk = true } } Button { anchors.horizontalCenter: parent.horizontalCenter - text: qsTr("Prova connessione") + text: qsTr("Test connection") onClicked: page.testAndReport() } @@ -113,4 +155,4 @@ Page { } } } -} +} \ No newline at end of file diff --git a/rpm/harbour-aira.spec b/rpm/harbour-aira.spec index cee42ff..25cc0b4 100644 --- a/rpm/harbour-aira.spec +++ b/rpm/harbour-aira.spec @@ -1,6 +1,6 @@ Name: harbour-aira Summary: Control your Aira heat pump through a local bridge -Version: 0.3.0 +Version: 0.4.0 Release: 1 Group: Qt/Qt License: MIT @@ -15,6 +15,8 @@ BuildRequires: pkgconfig(Qt5Qml) BuildRequires: pkgconfig(Qt5Quick) BuildRequires: pkgconfig(Qt5Network) BuildRequires: desktop-file-utils +# lupdate/lrelease: i .qm si compilano in build (CONFIG += sailfishapp_i18n). +BuildRequires: qt5-qttools-linguist %global debug_package %{nil} @@ -26,10 +28,22 @@ and Qt 5.6 has no HTTP/2. A small bridge on the user's server (Python, pyairahome) keeps the credentials and exposes the plant as JSON over HTTP/1.1. The app shows the current state and sends the hot-water commands. +The interface is in English and Italian: by default it follows the phone +language and falls back to English, and the language can also be picked in the +app settings. + Address and token of the bridge live in the app settings (AppConfigLocation); no personal data is stored in the binary. %changelog +* Sat Oct 10 2026 Carlo - 0.4.0-1 +- interfaccia in inglese e italiano, scelta nelle impostazioni con ripiego + sull'inglese; numeri con il separatore decimale della lingua +* Sat Oct 10 2026 Carlo - 0.3.0-1 +- pagina Altri comandi: assenza, night mode, riscaldamento forzato, legionella, + ACS on/off, verifica impianto; stato attuale in cima alla pagina +* Fri Oct 09 2026 Carlo - 0.2.0-1 +- temperatura della casa e obiettivo per zona (slider 5-30 gradi) * Fri Oct 09 2026 Carlo - 0.1.0-1 - prima release: stato impianto, obiettivo ACS, boost ACS @@ -65,3 +79,4 @@ desktop-file-validate %{buildroot}%{_datadir}/applications/%{name}.desktop || tr %{_datadir}/icons/hicolor/128x128/apps/%{name}.png %{_datadir}/icons/hicolor/172x172/apps/%{name}.png %{_datadir}/%{name}/qml +%{_datadir}/%{name}/translations/%{name}*.qm diff --git a/scripts/render_mockups.py b/scripts/render_mockups.py index 9a14d62..406035d 100644 --- a/scripts/render_mockups.py +++ b/scripts/render_mockups.py @@ -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)") diff --git a/src/airajson.cpp b/src/airajson.cpp index 476da6c..3eb1dc2 100644 --- a/src/airajson.cpp +++ b/src/airajson.cpp @@ -1,8 +1,10 @@ #include "airajson.h" +#include #include #include #include +#include #include namespace AiraJson { @@ -49,35 +51,35 @@ QString modeLabel(const QString &operatingStatus) { const QString s = operatingStatus; if (s == QLatin1String("OPERATING_STATUS_AUTO")) - return QStringLiteral("Automatico"); + return QCoreApplication::translate("AiraJson", "Automatic"); if (s == QLatin1String("OPERATING_STATUS_HEATING")) - return QStringLiteral("Riscaldamento"); + return QCoreApplication::translate("AiraJson", "Heating"); if (s == QLatin1String("OPERATING_STATUS_COOLING")) - return QStringLiteral("Raffrescamento"); + return QCoreApplication::translate("AiraJson", "Cooling"); if (s == QLatin1String("OPERATING_STATUS_HOT_WATER")) - return QStringLiteral("Acqua calda"); + return QCoreApplication::translate("AiraJson", "Hot water"); if (s == QLatin1String("OPERATING_STATUS_STANDBY")) - return QStringLiteral("Stand-by"); + return QCoreApplication::translate("AiraJson", "Standby"); if (s == QLatin1String("OPERATING_STATUS_OFF")) - return QStringLiteral("Spento"); + return QCoreApplication::translate("AiraJson", "Off"); // Stato della pompa per zona (state.zoneN_mode). if (s == QLatin1String("PUMP_MODE_STATE_HEATING")) - return QStringLiteral("Riscaldamento"); + return QCoreApplication::translate("AiraJson", "Heating"); if (s == QLatin1String("PUMP_MODE_STATE_COOLING")) - return QStringLiteral("Raffrescamento"); + return QCoreApplication::translate("AiraJson", "Cooling"); if (s == QLatin1String("PUMP_MODE_STATE_HOT_WATER")) - return QStringLiteral("Acqua calda"); + return QCoreApplication::translate("AiraJson", "Hot water"); if (s == QLatin1String("PUMP_MODE_STATE_IDLE")) - return QStringLiteral("In attesa"); + return QCoreApplication::translate("AiraJson", "Idle"); if (s == QLatin1String("PUMP_MODE_STATE_STANDBY")) - return QStringLiteral("Stand-by"); + return QCoreApplication::translate("AiraJson", "Standby"); if (s == QLatin1String("PUMP_MODE_STATE_OFF")) - return QStringLiteral("Spento"); + return QCoreApplication::translate("AiraJson", "Off"); // Stato operativo della pompa (state.pump_active_state). if (s == QLatin1String("PUMP_ACTIVE_STATE_IDLE")) - return QStringLiteral("Ferma"); + return QCoreApplication::translate("AiraJson", "Stopped"); if (s == QLatin1String("PUMP_ACTIVE_STATE_RUNNING")) - return QStringLiteral("In funzione"); + return QCoreApplication::translate("AiraJson", "Running"); if (s.isEmpty()) return QStringLiteral("—"); // Sconosciuto: togliamo il prefisso e mostriamo il resto, minuscolo. @@ -97,7 +99,10 @@ QString formatValue(const QVariant &value) bool isNumber = false; const double d = value.toDouble(&isNumber); if (isNumber) - return QString::number(d, 'f', 1); + // QLocale() = locale predefinito, che translator.cpp allinea alla lingua + // dell'app: separatore decimale col punto in inglese, con la virgola in + // italiano (QString::number darebbe sempre il punto). + return QLocale().toString(d, 'f', 1); const QString s = value.toString(); return s.isEmpty() ? QStringLiteral("—") : s; @@ -122,8 +127,8 @@ QString errorMessage(const QByteArray &body, int httpStatus) return msg; } if (httpStatus == 0) - return QStringLiteral("server non raggiungibile"); - return QStringLiteral("errore HTTP %1").arg(httpStatus); + return QCoreApplication::translate("AiraJson", "server unreachable"); + return QCoreApplication::translate("AiraJson", "HTTP error %1").arg(httpStatus); } } // namespace AiraJson diff --git a/src/apiclient.cpp b/src/apiclient.cpp index cd76539..9d24e8b 100644 --- a/src/apiclient.cpp +++ b/src/apiclient.cpp @@ -57,7 +57,7 @@ QNetworkReply *ApiClient::send(const QByteArray &verb, const QString &path, { const QString base = AiraJson::normalizeBaseUrl(m_settings ? m_settings->baseUrl() : QString()); if (base.isEmpty()) { - emit errorOccurred(QStringLiteral("Indirizzo del bridge non configurato")); + emit errorOccurred(tr("Bridge address not configured")); return 0; } @@ -79,7 +79,7 @@ QNetworkReply *ApiClient::send(const QByteArray &verb, const QString &path, void ApiClient::refresh() { if (!configured()) { - emit errorOccurred(QStringLiteral("Configura indirizzo e token del bridge")); + emit errorOccurred(tr("Set the bridge address and token")); return; } @@ -99,7 +99,7 @@ void ApiClient::refresh() if (reply->error() != QNetworkReply::NoError || status != 200) { const QString msg = (reply->error() == QNetworkReply::OperationCanceledError) - ? QStringLiteral("Server non raggiungibile (timeout)") + ? tr("Server unreachable (timeout)") : AiraJson::errorMessage(body, status); qWarning() << "[api] summary non riuscito:" << status << msg; emit errorOccurred(msg); @@ -110,7 +110,7 @@ void ApiClient::refresh() const QVariantMap summary = AiraJson::parseSummary(body, &ok); if (!ok) { qWarning() << "[api] riassunto non interpretabile"; - emit errorOccurred(QStringLiteral("Risposta del bridge non interpretabile")); + emit errorOccurred(tr("Unreadable response from the bridge")); return; } m_summary = summary; @@ -122,7 +122,7 @@ void ApiClient::testConnection() { const QString base = AiraJson::normalizeBaseUrl(m_settings ? m_settings->baseUrl() : QString()); if (base.isEmpty()) { - emit connectionTested(false, QStringLiteral("Inserisci l'indirizzo del bridge")); + emit connectionTested(false, tr("Enter the bridge address")); return; } @@ -138,12 +138,12 @@ void ApiClient::testConnection() const int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (reply->error() != QNetworkReply::NoError || status != 200) { const QString msg = (reply->error() == QNetworkReply::OperationCanceledError) - ? QStringLiteral("Timeout: nessuna risposta") - : QStringLiteral("Irraggiungibile (HTTP %1)").arg(status); + ? tr("Timeout: no response") + : tr("Unreachable (HTTP %1)").arg(status); emit connectionTested(false, msg); return; } - emit connectionTested(true, QStringLiteral("Bridge raggiungibile")); + emit connectionTested(true, tr("Bridge reachable")); }); } @@ -160,7 +160,7 @@ QString ApiClient::formatValue(const QVariant &value) const void ApiClient::sendCommand(const QString &id, const QVariant &value) { if (!configured()) { - emit commandFinished(false, QStringLiteral("Configura indirizzo e token del bridge")); + emit commandFinished(false, tr("Set the bridge address and token")); return; } @@ -169,7 +169,7 @@ void ApiClient::sendCommand(const QString &id, const QVariant &value) QNetworkReply *reply = send("POST", QStringLiteral("/command"), payload, 30); if (!reply) { - emit commandFinished(false, QStringLiteral("Bridge non configurato")); + emit commandFinished(false, tr("Bridge not configured")); return; } @@ -183,14 +183,14 @@ void ApiClient::sendCommand(const QString &id, const QVariant &value) if (reply->error() != QNetworkReply::NoError || status != 200) { const QString msg = (reply->error() == QNetworkReply::OperationCanceledError) - ? QStringLiteral("Timeout: comando non inviato") + ? tr("Timeout: command not sent") : AiraJson::errorMessage(body, status); qWarning() << "[api] comando" << id << "fallito:" << status << msg; emit commandFinished(false, msg); return; } - emit commandFinished(true, QStringLiteral("Comando inviato")); + emit commandFinished(true, tr("Command sent")); refresh(); // i valori letti cambiano: rileggo }); } diff --git a/src/main.cpp b/src/main.cpp index 2f0f711..bed4fb1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -13,6 +13,7 @@ #include "apiclient.h" #include "settings.h" +#include "translator.h" Q_DECL_EXPORT int main(int argc, char *argv[]) { @@ -20,19 +21,40 @@ Q_DECL_EXPORT int main(int argc, char *argv[]) app->setOrganizationName(QStringLiteral("harbour")); app->setApplicationName(QStringLiteral("aira")); - app->setApplicationVersion(QStringLiteral("0.3.0")); + app->setApplicationVersion(QStringLiteral("0.4.0")); Settings settings; ApiClient api(&settings); + // Lingua dell'interfaccia. Va risolta PRIMA di createView: le qsTr() del QML + // si valutano alla creazione dei componenti, quindi un traduttore installato + // dopo non avrebbe effetto sulla prima schermata. + Translator translator(&settings); + translator.applySaved(); + QQuickView *view = SailfishApp::createView(); // Prima riga del log: dice subito se il device sta girando la build nuova // o una stale (trappola nota del build engine). - qDebug() << "harbour-aira v0.3.0 build" << __DATE__ << __TIME__; + qDebug() << "harbour-aira v0.4.0 build" << __DATE__ << __TIME__; view->rootContext()->setContextProperty(QStringLiteral("appSettings"), &settings); view->rootContext()->setContextProperty(QStringLiteral("api"), &api); + view->rootContext()->setContextProperty(QStringLiteral("translator"), &translator); + + // Qt 5.6 non ha QQmlEngine::retranslate() (arriva con Qt 5.10): installare un + // traduttore nuovo NON ri-traduce le stringhe gia' mostrate, quindi la lingua + // scelta nelle impostazioni si applica ricaricando la radice QML. Conseguenza + // voluta e attesa: si torna alla home, nella lingua nuova. + // + // setSource() con la STESSA url ricrea davvero il componente: in Qt 5.6 + // QQuickViewPrivate::execute() cancella root e component e ne costruisce uno + // nuovo (documentato: "the QML component being instantiated, even if the URL + // is unchanged"), quindi ogni qsTr() viene rivalutato. Non serve + // clearComponentCache(): i valori delle stringhe non stanno nella cache. + QObject::connect(&translator, &Translator::languageChanged, view, [view]() { + view->setSource(SailfishApp::pathTo(QStringLiteral("qml/harbour-aira.qml"))); + }); view->setSource(SailfishApp::pathTo(QStringLiteral("qml/harbour-aira.qml"))); view->show(); diff --git a/src/settings.cpp b/src/settings.cpp index b442be0..48c6470 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -52,3 +52,18 @@ void Settings::setToken(const QString &value) m_settings.sync(); emit tokenChanged(); } + +QString Settings::language() const +{ + // Vuoto = non deciso dall'utente: si usa la lingua del sistema. + return m_settings.value(QStringLiteral("language"), QString()).toString(); +} + +void Settings::setLanguage(const QString &value) +{ + if (value == language()) + return; + m_settings.setValue(QStringLiteral("language"), value); + m_settings.sync(); + emit languageChanged(); +} diff --git a/src/settings.h b/src/settings.h index c2891e2..538eb44 100644 --- a/src/settings.h +++ b/src/settings.h @@ -10,13 +10,16 @@ // sparivano quando l'app veniva avviata dal launcher. // // baseUrl indirizzo del bridge, es. "192.168.2.50:8790" (schema opzionale) -// token token del bridge (Authorization: Bearer ...) +// token token del bridge (Authorization: Bearer ***) +// language lingua dell'interfaccia: "" = lingua del sistema (ripiego +// sull'inglese), "it" = italiano. class Settings : public QObject { Q_OBJECT Q_PROPERTY(QString baseUrl READ baseUrl WRITE setBaseUrl NOTIFY baseUrlChanged) Q_PROPERTY(QString token READ token WRITE setToken NOTIFY tokenChanged) + Q_PROPERTY(QString language READ language WRITE setLanguage NOTIFY languageChanged) public: explicit Settings(QObject *parent = 0); @@ -27,9 +30,13 @@ public: QString token() const; void setToken(const QString &value); + QString language() const; + void setLanguage(const QString &value); + signals: void baseUrlChanged(); void tokenChanged(); + void languageChanged(); private: static QString configPath(); diff --git a/src/translator.cpp b/src/translator.cpp new file mode 100644 index 0000000..2c5f626 --- /dev/null +++ b/src/translator.cpp @@ -0,0 +1,74 @@ +#include "translator.h" +#include "settings.h" + +#include +#include +#include + +namespace { +// qmake installa i cataloghi sotto il TARGET, non sotto org/app: e' li' che +// l'app deve cercarli (vedi CONFIG += sailfishapp_i18n nel .pro). +const char *kTranslationsDir = "/usr/share/harbour-aira/translations"; +const char *kCatalogBase = "harbour-aira"; +} + +Translator::Translator(Settings *settings, QObject *parent) + : QObject(parent) + , m_settings(settings) +{ +} + +QString Translator::language() const +{ + return m_settings ? m_settings->language() : QString(); +} + +void Translator::setLanguage(const QString &value) +{ + const QString before = language(); + if (m_settings) + m_settings->setLanguage(value); // persiste subito + if (value == before) + return; // nessun cambio: niente ricarica + apply(value); + emit languageChanged(); +} + +void Translator::applySaved() +{ + // All'avvio non si ricarica nulla (la view non esiste ancora): nessun segnale. + apply(language()); +} + +void Translator::apply(const QString &value) +{ + if (m_translator) { + QCoreApplication::removeTranslator(m_translator); + delete m_translator; + } + + // Lingua vuota = lingua del telefono. Attenzione: QLocale() sarebbe il locale + // PREDEFINITO, che qui sotto cambiamo noi con setDefault(): per tornare alla + // lingua di sistema serve QLocale::system(), che non risente di setDefault. + const QLocale locale = value.isEmpty() ? QLocale::system() : QLocale(value); + QTranslator *translator = new QTranslator(this); + + // Anche il locale predefinito segue la lingua scelta: da questo dipendono i + // formati dei numeri (virgola o punto) e le date. + QLocale::setDefault(locale); + + if (translator->load(locale, QString::fromLatin1(kCatalogBase), + QStringLiteral("-"), QString::fromLatin1(kTranslationsDir))) { + QCoreApplication::installTranslator(translator); + m_translator = translator; + } else { + // Nessun catalogo (es. locale inglese, o .qm non installati): si resta + // sulle stringhe sorgente inglesi. Il traduttore non va installato. + delete translator; + } + + qDebug() << "Translator: richiesta [" + << (value.isEmpty() ? QStringLiteral("sistema") : value) + << "] locale" << locale.name() + << "catalogo caricato:" << !m_translator.isNull(); +} diff --git a/src/translator.h b/src/translator.h new file mode 100644 index 0000000..5663e5f --- /dev/null +++ b/src/translator.h @@ -0,0 +1,48 @@ +#ifndef TRANSLATOR_H +#define TRANSLATOR_H + +#include +#include +#include + +class Settings; + +// Lingua dell'interfaccia. +// +// "" (vuoto) -> lingua del sistema (QLocale::system(), non QLocale(), che e' il +// locale predefinito e viene cambiato da setDefault), con ripiego +// sull'inglese: l'inglese e' la lingua sorgente delle stringhe, +// quindi se per il locale del telefono non esiste un catalogo Qt +// lascia il testo originale inglese. +// "it" -> italiano, dal catalogo installato in +// /usr/share/harbour-aira/translations (i .qm li genera qmake in +// build con CONFIG += sailfishapp_i18n). +// +// Qt 5.6 (Sailfish) non ha QQmlEngine::retranslate(): esiste da Qt 5.10, e prima +// il QML non si ri-traduce da solo quando cambia il traduttore. Per questo +// main.cpp collega languageChanged() alla ricarica della radice QML: l'effetto +// visibile e' che l'app torna alla home, ma nella lingua nuova. +class Translator : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString language READ language WRITE setLanguage NOTIFY languageChanged) + +public: + explicit Translator(Settings *settings, QObject *parent = 0); + + QString language() const; + void setLanguage(const QString &value); // salva nelle impostazioni e applica + void applySaved(); // all'avvio, PRIMA di creare la view + +signals: + void languageChanged(); + +private: + void apply(const QString &value); + + Settings *m_settings; + QPointer m_translator; +}; + +#endif // TRANSLATOR_H diff --git a/tests/core_test.cpp b/tests/core_test.cpp index 146ddba..b960771 100644 --- a/tests/core_test.cpp +++ b/tests/core_test.cpp @@ -8,7 +8,9 @@ #include #include #include +#include #include +#include #include "../src/airajson.h" @@ -59,25 +61,46 @@ int main(int argc, char *argv[]) "ok=true senza summary -> vuoto"); // ---- modeLabel -------------------------------------------------------- // - check(AiraJson::modeLabel("OPERATING_STATUS_AUTO") == "Automatico", "AUTO -> Automatico"); - check(AiraJson::modeLabel("OPERATING_STATUS_HOT_WATER") == "Acqua calda", "acqua calda"); + // Attese in inglese: e' la lingua SORGENTE delle stringhe (l'italiano arriva + // dal catalogo caricato piu' avanti). + check(AiraJson::modeLabel("OPERATING_STATUS_AUTO") == "Automatic", "AUTO -> Automatic"); + check(AiraJson::modeLabel("OPERATING_STATUS_HOT_WATER") == "Hot water", "hot water"); check(AiraJson::modeLabel("") == "—", "stato vuoto -> trattino"); - check(AiraJson::modeLabel("PUMP_MODE_STATE_HEATING") == "Riscaldamento", - "stato zona: riscaldamento"); - check(AiraJson::modeLabel("PUMP_MODE_STATE_IDLE") == "In attesa", - "stato zona: in attesa"); + check(AiraJson::modeLabel("PUMP_MODE_STATE_HEATING") == "Heating", + "zone state: heating"); + check(AiraJson::modeLabel("PUMP_MODE_STATE_IDLE") == "Idle", + "zone state: idle"); check(AiraJson::modeLabel("PUMP_MODE_STATE_IGNOTO") == "ignoto", - "stato zona sconosciuto -> senza prefisso"); - check(AiraJson::modeLabel("PUMP_ACTIVE_STATE_IDLE") == "Ferma", - "pompa ferma"); - check(AiraJson::modeLabel("PUMP_ACTIVE_STATE_RUNNING") == "In funzione", - "pompa in funzione"); + "unknown zone state -> prefix stripped"); + check(AiraJson::modeLabel("PUMP_ACTIVE_STATE_IDLE") == "Stopped", + "pump stopped"); + check(AiraJson::modeLabel("PUMP_ACTIVE_STATE_RUNNING") == "Running", + "pump running"); check(AiraJson::modeLabel("OPERATING_STATUS_DEFROST") == "defrost", - "stato sconosciuto: prefisso tolto, minuscolo"); + "unknown state: prefix stripped, lowercase"); // ---- formatValue ------------------------------------------------------ // - check(AiraJson::formatValue(QVariant(51.0)) == "51.0", "numero a un decimale"); + // Il separatore decimale segue il locale predefinito (che translator.cpp + // allinea alla lingua scelta): qui si fissa per rendere l'esito deterministico + // e si verifica che cambi davvero con la lingua. + QLocale::setDefault(QLocale::c()); + check(AiraJson::formatValue(QVariant(51.0)) == "51.0", "numero a un decimale (locale C)"); check(AiraJson::formatValue(QVariant(18.14)) == "18.1", "arrotondamento a un decimale"); + QLocale::setDefault(QLocale(QLocale::Italian, QLocale::Italy)); + check(AiraJson::formatValue(QVariant(23.0)) == "23,0", "in italiano separatore virgola"); + check(AiraJson::formatValue(QVariant(18.14)) == "18,1", "in italiano arrotonda con la virgola"); + QLocale::setDefault(QLocale(QLocale::English, QLocale::UnitedKingdom)); + check(AiraJson::formatValue(QVariant(23.0)) == "23.0", "in inglese separatore punto"); + QLocale::setDefault(QLocale::c()); + + // Invariante su cui poggia Translator::apply(""): QLocale::system() non risente + // di setDefault(), quindi "lingua vuota" torna sempre alla lingua del telefono + // anche dopo che l'utente ha scelto l'italiano. (QLocale() invece cambierebbe.) + const QLocale systemBefore = QLocale::system(); + QLocale::setDefault(QLocale(QLocale::Italian, QLocale::Italy)); + check(QLocale::system() == systemBefore, "setDefault non tocca QLocale::system()"); + check(QLocale() == QLocale(QLocale::Italian, QLocale::Italy), "QLocale() segue setDefault"); + QLocale::setDefault(QLocale::c()); check(AiraJson::formatValue(QVariant()) == "—", "valore assente -> trattino"); check(AiraJson::formatValue(QVariant(QString())) == "—", "stringa vuota -> trattino"); @@ -94,8 +117,36 @@ int main(int argc, char *argv[]) // ---- errorMessage ----------------------------------------------------- // check(AiraJson::errorMessage(R"({"ok":false,"error":"token mancante o errato"})", 401) == "token mancante o errato", "messaggio d'errore dal corpo"); - check(AiraJson::errorMessage("", 502) == "errore HTTP 502", "fallback sullo stato HTTP"); - check(AiraJson::errorMessage("", 0) == "server non raggiungibile", "stato 0 = irraggiungibile"); + check(AiraJson::errorMessage("", 502) == "HTTP error 502", "http status fallback"); + check(AiraJson::errorMessage("", 0) == "server unreachable", "status 0 = unreachable"); + + // ---- catalogo italiano: il .qm caricato davvero ------------------------ // + // Non basta che il .ts esista: qui si installa il .qm come fa main.cpp e si + // controlla che le stringhe sorgente inglesi diventino italiane. Il percorso + // si puo' passare come argv[1]; il default e' quello relativo alla cartella + // di build del test (tests/build). + { + const QString qm = (argc > 1) ? QString::fromLocal8Bit(argv[1]) + : QStringLiteral("../../translations/harbour-aira-it.qm"); + QTranslator translator; + if (translator.load(qm)) { + QCoreApplication::installTranslator(&translator); + check(AiraJson::modeLabel("PUMP_MODE_STATE_HEATING") == "Riscaldamento", + "catalogo it: Heating -> Riscaldamento"); + check(AiraJson::modeLabel("PUMP_ACTIVE_STATE_IDLE") == "Ferma", + "catalogo it: Stopped -> Ferma"); + check(AiraJson::modeLabel("PUMP_MODE_STATE_IDLE") == "In attesa", + "catalogo it: Idle -> In attesa"); + check(AiraJson::errorMessage("", 0) == "server non raggiungibile", + "catalogo it: messaggio d'errore tradotto"); + QCoreApplication::removeTranslator(&translator); + check(AiraJson::modeLabel("PUMP_MODE_STATE_HEATING") == "Heating", + "catalogo rimosso: si torna all'inglese (ripiego)"); + } else { + qWarning() << "SALTO il controllo del catalogo: .qm non trovato in" << qm + << "-> lancialo con lrelease, oppure passa il percorso come argomento"; + } + } qInfo() << "----"; qInfo() << checks - failures << "/" << checks << "verifiche superate"; diff --git a/translations/harbour-aira-it.ts b/translations/harbour-aira-it.ts new file mode 100644 index 0000000..786a812 --- /dev/null +++ b/translations/harbour-aira-it.ts @@ -0,0 +1,528 @@ + + + + + AiraJson + + + Automatic + Automatico + + + + + Heating + Riscaldamento + + + + + Cooling + Raffrescamento + + + + + Hot water + Acqua calda + + + + + Standby + Stand-by + + + + + Off + Spento + + + + Idle + In attesa + + + + Stopped + Ferma + + + + Running + In funzione + + + + server unreachable + server non raggiungibile + + + + HTTP error %1 + errore HTTP %1 + + + + ApiClient + + + Bridge address not configured + Indirizzo del bridge non configurato + + + + + Set the bridge address and token + Configura indirizzo e token del bridge + + + + Server unreachable (timeout) + Server non raggiungibile (timeout) + + + + Unreadable response from the bridge + Risposta del bridge non interpretabile + + + + Enter the bridge address + Inserisci l'indirizzo del bridge + + + + Timeout: no response + Timeout: nessuna risposta + + + + Unreachable (HTTP %1) + Irraggiungibile (HTTP %1) + + + + Bridge reachable + Bridge raggiungibile + + + + Bridge not configured + Bridge non configurato + + + + Timeout: command not sent + Timeout: comando non inviato + + + + Command sent + Comando inviato + + + + CommandsPage + + + yes + sì + + + + no + no + + + + Other commands + Altri comandi + + + + Current state + Stato attuale + + + + + Away mode + Assenza + + + + Night mode + Night mode + + + + Forced heating + Riscaldamento forzato + + + + Hot water active + Acqua calda attiva + + + + Pump + Pompa + + + + Allowed modes + Modalità consentite + + + + Turn away mode on + Attiva assenza + + + + Turn away mode off + Disattiva assenza + + + + Noise + Rumore + + + + Night mode for one hour + Night mode per un'ora + + + + Heating + Riscaldamento + + + + Force heating + Forza riscaldamento + + + + Starting forced heating + Avvio del riscaldamento forzato + + + + Stop forced heating + Ferma riscaldamento forzato + + + + Enable heating function + Abilita funzione riscaldamento + + + + Disable heating function + Disabilita funzione riscaldamento + + + + Heating will be disabled + Il riscaldamento verrà disabilitato + + + + Domestic hot water + Acqua calda sanitaria + + + + Legionella cycle + Ciclo antilegionella + + + + Starting the legionella cycle: it takes a while + Avvio del ciclo antilegionella: richiede tempo + + + + Enable DHW heating + Abilita riscaldamento ACS + + + + Disable DHW heating + Disabilita riscaldamento ACS + + + + Domestic hot water will stop being heated + L'acqua calda smetterà di essere riscaldata + + + + Diagnostics + Diagnostica + + + + Plant check + Verifica impianto + + + + Commands go through the bridge and may take a few seconds. The status refreshes by itself afterwards. + I comandi passano dal bridge e possono richiedere qualche secondo. Dopo l'invio lo stato si aggiorna da solo. + + + + CoverPage + + + house · target %1 °C + casa · obiettivo %1 °C + + + + DHW %1 °C · target %2 °C + ACS %1 °C · obiettivo %2 °C + + + + outdoor %1 °C + est. %1 °C + + + + MainPage + + + yes + sì + + + + no + no + + + + Refresh + Aggiorna + + + + + Settings + Impostazioni + + + + + Other commands + Altri comandi + + + + Bridge not configured. Open the settings and enter the server address and the token. + Bridge non configurato. Apri le impostazioni e inserisci l'indirizzo del server e il token. + + + + House temperature + Temperatura in casa + + + + target %1 °C · %2 + obiettivo %1 °C · %2 + + + + New house target + Nuovo obiettivo casa + + + + Set house to %1 °C + Imposta casa a %1 °C + + + + House temperature to %1 °C + Temperatura della casa a %1 °C + + + + Domestic hot water + Acqua calda sanitaria + + + + DHW target + Obiettivo acqua calda + + + + New target + Nuovo obiettivo + + + + Set target %1 °C + Imposta obiettivo %1 °C + + + + DHW target to %1 °C + Obiettivo ACS a %1 °C + + + + Start DHW boost + Avvia boost ACS + + + + Starting DHW boost + Avvio del boost ACS + + + + Stop DHW boost + Ferma boost ACS + + + + Stopping DHW boost + Arresto del boost ACS + + + + Plant + Impianto + + + + Outdoor temperature + Temperatura esterna + + + + Indoor temperature (pump) + Temperatura interna (pompa) + + + + Mode + Modalità + + + + Away mode + Assenza + + + + Night mode + Night mode + + + + Forced heating + Riscaldamento forzato + + + + Inline heater + Resistenze integrative + + + + Zone + Zone + + + + Errors + Errori + + + + Last reading + Aggiornato alle + + + + Commands are sent to the bridge and from there to the plant: they may take a few seconds. + I comandi vengono inviati al bridge e da lì all'impianto: possono richiedere qualche secondo. + + + + SettingsPage + + + checking… + verifico… + + + + Settings + Impostazioni + + + + + Language + Lingua + + + + + System + Sistema + + + + Changing the language reloads the app. + Cambiando la lingua l'app si ricarica. + + + + Bridge + Bridge + + + + Bridge address + Indirizzo del bridge + + + + Bridge token + Token del bridge + + + + Address and token live on the server: the token is in aira_bridge.json next to the bridge. Without a scheme, http:// is used. + Indirizzo e token si leggono sul server: il token è in aira_bridge.json accanto al bridge. Senza schema viene usato http://. + + + + Save + Salva + + + + settings saved + impostazioni salvate + + + + Test connection + Prova connessione + + +