fix(gui): route runtime status messages through Loc instead of hardcoded Italian

Send/Donate/Sync/Wizard view models wrote status/error strings
directly in Italian ("Importo non valido.", "Errore: …", the TLS
pin-mismatch message, …), so a user running the app in English/German/
etc. still saw them in Italian despite the 6-language Loc system
covering the rest of the UI.

Added the missing Loc keys and routed every occurrence through
Loc.Tr(...). CertificatePinMismatchException (Core) no longer bakes an
Italian message into .Message; it now exposes Host/Port so the UI can
translate it, with an English fallback for any exception type the UI
doesn't specifically handle. Also drops the now-stale USERGUIDE.md
caveat about a few messages staying in Italian regardless of language.
This commit is contained in:
2026-07-07 14:45:40 +02:00
parent 1f4421ae16
commit af2fdcc894
7 changed files with 60 additions and 25 deletions
+1 -2
View File
@@ -583,8 +583,7 @@ The Settings overlay (gear icon / menu *Settings*) contains exactly two preferen
the door to the server settings: the door to the server settings:
- **Language** — Italiano, English, Español, Français, Português, Deutsch. Applies - **Language** — Italiano, English, Español, Français, Português, Deutsch. Applies
immediately. Default: English. (A few low-level error messages are currently shown in immediately. Default: English.
Italian regardless of language.)
- **Amount unit** — how amounts are displayed *and interpreted when you type them*: - **Amount unit** — how amounts are displayed *and interpreted when you type them*:
| Unit | In satoshi | Decimals shown | | Unit | In satoshi | Decimals shown |
+18
View File
@@ -403,6 +403,24 @@ public sealed class Loc
"Certificados SSL redefinidos: tente novamente a conexão.", "Certificados SSL redefinidos: tente novamente a conexão.",
"SSL-Zertifikate zurückgesetzt: Verbindung erneut versuchen."], "SSL-Zertifikate zurückgesetzt: Verbindung erneut versuchen."],
["msg.error"] = ["Errore", "Error", "Error", "Erreur", "Erro", "Fehler"], ["msg.error"] = ["Errore", "Error", "Error", "Erreur", "Erro", "Fehler"],
["msg.broadcast.error"] = ["Errore broadcast", "Broadcast error", "Error de transmisión", "Erreur de diffusion", "Erro de transmissão", "Übertragungsfehler"],
["msg.peer.discovery.error"] = ["Errore nella scoperta peer", "Peer discovery error", "Error al descubrir peers", "Erreur de découverte des pairs", "Erro na descoberta de peers", "Fehler bei der Peer-Suche"],
["msg.peer.discovery.found"] = ["Trovati {0} nuovi server dai peer (totale {1}).", "Found {0} new servers from peers (total {1}).", "Se encontraron {0} nuevos servidores de peers (total {1}).", "{0} nouveaux serveurs trouvés via les pairs (total {1}).", "Encontrados {0} novos servidores dos peers (total {1}).", "{0} neue Server von Peers gefunden (insgesamt {1})."],
["msg.peer.discovery.none"] = ["Nessun nuovo server annunciato (totale {0}).", "No new servers announced (total {0}).", "No se anunciaron nuevos servidores (total {0}).", "Aucun nouveau serveur annoncé (total {0}).", "Nenhum novo servidor anunciado (total {0}).", "Keine neuen Server angekündigt (insgesamt {0})."],
["msg.send.sync.first"] = ["Sincronizza prima di inviare.", "Synchronize before sending.", "Sincroniza antes de enviar.", "Synchronisez avant d'envoyer.", "Sincronize antes de enviar.", "Vor dem Senden synchronisieren."],
["msg.send.connect.first"] = ["Connettiti al server e sincronizza prima di inviare.", "Connect to the server and synchronize before sending.", "Conéctate al servidor y sincroniza antes de enviar.", "Connectez-vous au serveur et synchronisez avant d'envoyer.", "Conecte-se ao servidor e sincronize antes de enviar.", "Mit dem Server verbinden und vor dem Senden synchronisieren."],
["msg.amount.invalid"] = ["Importo non valido.", "Invalid amount.", "Importe no válido.", "Montant invalide.", "Valor inválido.", "Ungültiger Betrag."],
["msg.feerate.invalid"] = ["Fee rate non valido.", "Invalid fee rate.", "Tarifa no válida.", "Taux de frais invalide.", "Taxa inválida.", "Ungültige Gebührenrate."],
["msg.broadcasted"] = ["Trasmessa", "Broadcast", "Transmitida", "Diffusée", "Transmitida", "Übertragen"],
["msg.donate.thanks"] = ["Grazie! txid", "Thank you! txid", "¡Gracias! txid", "Merci ! txid", "Obrigado! txid", "Danke! txid"],
["msg.unsigned.watchonly"] = [" · NON firmata (watch-only)", " · NOT signed (watch-only)", " · NO firmada (watch-only)", " · NON signée (watch-only)", " · NÃO assinada (watch-only)", " · NICHT signiert (watch-only)"],
["msg.cert.mismatch"] = [
"Il certificato TLS di {0} è cambiato rispetto a quello salvato. Se il server ha rinnovato il certificato, esegui il reset dei certificati SSL.",
"The TLS certificate of {0} has changed from the one saved. If the server renewed its certificate, reset the SSL certificates.",
"El certificado TLS de {0} ha cambiado respecto al guardado. Si el servidor renovó su certificado, restablece los certificados SSL.",
"Le certificat TLS de {0} a changé par rapport à celui enregistré. Si le serveur a renouvelé son certificat, réinitialisez les certificats SSL.",
"O certificado TLS de {0} mudou em relação ao salvo. Se o servidor renovou o certificado, redefina os certificados SSL.",
"Das TLS-Zertifikat von {0} hat sich gegenüber dem gespeicherten geändert. Wenn der Server das Zertifikat erneuert hat, setzen Sie die SSL-Zertifikate zurück."],
// Contacts // Contacts
["contacts.name"] = ["Nome", "Name", "Nombre", "Nom", "Nome", "Name"], ["contacts.name"] = ["Nome", "Name", "Nombre", "Nom", "Nome", "Name"],
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using NBitcoin; using NBitcoin;
using PalladiumWallet.App.Localization;
using PalladiumWallet.Core.Chain; using PalladiumWallet.Core.Chain;
using PalladiumWallet.Core.Net; using PalladiumWallet.Core.Net;
using PalladiumWallet.Core.Wallet; using PalladiumWallet.Core.Wallet;
@@ -32,7 +33,7 @@ public partial class MainWindowViewModel
if (_account is null || _doc?.Cache is null || _lastTransactions is null) if (_account is null || _doc?.Cache is null || _lastTransactions is null)
{ {
DonatePreview = "Sincronizza prima di inviare."; DonatePreview = Loc.Tr("msg.send.sync.first");
return; return;
} }
try try
@@ -40,7 +41,7 @@ public partial class MainWindowViewModel
var destination = BitcoinAddress.Create(DevAddress, PalladiumNetworks.For(Net)); var destination = BitcoinAddress.Create(DevAddress, PalladiumNetworks.For(Net));
if (!CoinAmount.TryParseIn(DonateAmount.Trim(), _config.Unit, out var amount) || amount <= 0) if (!CoinAmount.TryParseIn(DonateAmount.Trim(), _config.Unit, out var amount) || amount <= 0)
{ {
DonatePreview = "Importo non valido."; DonatePreview = Loc.Tr("msg.amount.invalid");
return; return;
} }
if (!decimal.TryParse(SendFeeRate, System.Globalization.NumberStyles.Any, if (!decimal.TryParse(SendFeeRate, System.Globalization.NumberStyles.Any,
@@ -61,7 +62,7 @@ public partial class MainWindowViewModel
{ {
_pendingDonate = null; _pendingDonate = null;
HasPendingDonate = false; HasPendingDonate = false;
DonatePreview = $"Errore: {ex.Message}"; DonatePreview = $"{Loc.Tr("msg.error")}: {ex.Message}";
} }
await Task.CompletedTask; await Task.CompletedTask;
} }
@@ -74,7 +75,7 @@ public partial class MainWindowViewModel
try try
{ {
var txid = await _client.BroadcastAsync(_pendingDonate.ToHex()); var txid = await _client.BroadcastAsync(_pendingDonate.ToHex());
DonatePreview = $"Grazie! txid: {txid}"; DonatePreview = $"{Loc.Tr("msg.donate.thanks")}: {txid}";
DonateAmount = ""; DonateAmount = "";
_pendingDonate = null; _pendingDonate = null;
HasPendingDonate = false; HasPendingDonate = false;
@@ -82,7 +83,7 @@ public partial class MainWindowViewModel
} }
catch (Exception ex) catch (Exception ex)
{ {
DonatePreview = $"Errore broadcast: {ex.Message}"; DonatePreview = $"{Loc.Tr("msg.broadcast.error")}: {ex.Message}";
} }
} }
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using NBitcoin; using NBitcoin;
using PalladiumWallet.App.Localization;
using PalladiumWallet.App.Services; using PalladiumWallet.App.Services;
using PalladiumWallet.Core.Chain; using PalladiumWallet.Core.Chain;
using PalladiumWallet.Core.Net; using PalladiumWallet.Core.Net;
@@ -47,14 +48,14 @@ public partial class MainWindowViewModel
{ {
if (_account is null || _doc?.Cache is null) if (_account is null || _doc?.Cache is null)
{ {
SendPreview = "Sincronizza prima di inviare."; SendPreview = Loc.Tr("msg.send.sync.first");
return; return;
} }
try try
{ {
if (_lastTransactions is null) if (_lastTransactions is null)
{ {
SendPreview = "Connettiti al server e sincronizza prima di inviare."; SendPreview = Loc.Tr("msg.send.connect.first");
return; return;
} }
@@ -62,12 +63,12 @@ public partial class MainWindowViewModel
long amount = 0; long amount = 0;
if (!SendAll && !CoinAmount.TryParseIn(SendAmount, _config.Unit, out amount)) if (!SendAll && !CoinAmount.TryParseIn(SendAmount, _config.Unit, out amount))
{ {
SendPreview = "Importo non valido."; SendPreview = Loc.Tr("msg.amount.invalid");
return; return;
} }
if (!decimal.TryParse(SendFeeRate, out var feeRate) || feeRate <= 0) if (!decimal.TryParse(SendFeeRate, out var feeRate) || feeRate <= 0)
{ {
SendPreview = "Fee rate non valido."; SendPreview = Loc.Tr("msg.feerate.invalid");
return; return;
} }
@@ -78,14 +79,14 @@ public partial class MainWindowViewModel
SendPreview = $"txid {_pendingSend.Txid[..16]}… · " + SendPreview = $"txid {_pendingSend.Txid[..16]}… · " +
$"fee {Fmt(_pendingSend.Fee.Satoshi)} " + $"fee {Fmt(_pendingSend.Fee.Satoshi)} " +
$"({_pendingSend.Transaction.GetVirtualSize()} vB)" + $"({_pendingSend.Transaction.GetVirtualSize()} vB)" +
(_pendingSend.Signed ? "" : " · NON firmata (watch-only)"); (_pendingSend.Signed ? "" : Loc.Tr("msg.unsigned.watchonly"));
HasPendingSend = _pendingSend.Signed; HasPendingSend = _pendingSend.Signed;
} }
catch (Exception ex) catch (Exception ex)
{ {
_pendingSend = null; _pendingSend = null;
HasPendingSend = false; HasPendingSend = false;
SendPreview = $"Errore: {ex.Message}"; SendPreview = $"{Loc.Tr("msg.error")}: {DescribeError(ex)}";
} }
await Task.CompletedTask; await Task.CompletedTask;
} }
@@ -98,7 +99,7 @@ public partial class MainWindowViewModel
try try
{ {
var txid = await _client.BroadcastAsync(_pendingSend.ToHex()); var txid = await _client.BroadcastAsync(_pendingSend.ToHex());
SendPreview = $"Trasmessa: {txid}"; SendPreview = $"{Loc.Tr("msg.broadcasted")}: {txid}";
SendTo = SendAmount = ""; SendTo = SendAmount = "";
_pendingSend = null; _pendingSend = null;
HasPendingSend = false; HasPendingSend = false;
@@ -106,7 +107,7 @@ public partial class MainWindowViewModel
} }
catch (Exception ex) catch (Exception ex)
{ {
SendPreview = $"Errore broadcast: {ex.Message}"; SendPreview = $"{Loc.Tr("msg.broadcast.error")}: {DescribeError(ex)}";
} }
} }
} }
+18 -6
View File
@@ -319,14 +319,14 @@ public partial class MainWindowViewModel
IsConnected = false; IsConnected = false;
ConnectionStatus = Loc.Tr("conn.none"); ConnectionStatus = Loc.Tr("conn.none");
ConnectionStatusShort = Loc.Tr("conn.none"); ConnectionStatusShort = Loc.Tr("conn.none");
StatusMessage = ex.Message; StatusMessage = DescribeError(ex);
} }
catch (Exception ex) catch (Exception ex)
{ {
IsConnected = _client?.IsConnected == true; IsConnected = _client?.IsConnected == true;
ConnectionStatus = IsConnected ? ConnectionStatus : Loc.Tr("conn.none"); ConnectionStatus = IsConnected ? ConnectionStatus : Loc.Tr("conn.none");
ConnectionStatusShort = IsConnected ? Loc.Tr("conn.connectedto") : Loc.Tr("conn.none"); ConnectionStatusShort = IsConnected ? Loc.Tr("conn.connectedto") : Loc.Tr("conn.none");
StatusMessage = $"Errore: {ex.Message}"; StatusMessage = $"{Loc.Tr("msg.error")}: {DescribeError(ex)}";
if (_account is not null) if (_account is not null)
{ {
_syncFailed = true; _syncFailed = true;
@@ -378,7 +378,8 @@ public partial class MainWindowViewModel
} }
if (temp is null) if (temp is null)
{ {
StatusMessage = $"Errore nella scoperta peer: {lastError?.Message ?? Loc.Tr("conn.none")}"; StatusMessage = $"{Loc.Tr("msg.peer.discovery.error")}: " +
(lastError is null ? Loc.Tr("conn.none") : DescribeError(lastError));
return; return;
} }
try try
@@ -401,15 +402,26 @@ public partial class MainWindowViewModel
SelectedKnownServer = KnownServers.FirstOrDefault(s => s.Host == selected?.Host) SelectedKnownServer = KnownServers.FirstOrDefault(s => s.Host == selected?.Host)
?? KnownServers.FirstOrDefault(); ?? KnownServers.FirstOrDefault();
StatusMessage = added > 0 StatusMessage = added > 0
? $"Trovati {added} nuovi server dai peer (totale {KnownServers.Count})." ? string.Format(Loc.Tr("msg.peer.discovery.found"), added, KnownServers.Count)
: $"Nessun nuovo server annunciato (totale {KnownServers.Count})."; : string.Format(Loc.Tr("msg.peer.discovery.none"), KnownServers.Count);
} }
catch (Exception ex) catch (Exception ex)
{ {
StatusMessage = $"Errore nella scoperta peer: {ex.Message}"; StatusMessage = $"{Loc.Tr("msg.peer.discovery.error")}: {DescribeError(ex)}";
} }
} }
/// <summary>
/// Translates known Core exception types to the current UI language; falls back to the
/// exception's own message (Core has no localization, so it is English by default).
/// </summary>
private static string DescribeError(Exception ex) => ex switch
{
CertificatePinMismatchException cert =>
string.Format(Loc.Tr("msg.cert.mismatch"), $"{cert.Host}:{cert.Port}"),
_ => ex.Message,
};
private void OnServerNotification(string method, System.Text.Json.JsonElement payload) private void OnServerNotification(string method, System.Text.Json.JsonElement payload)
{ {
if (method is "blockchain.scripthash.subscribe" or "blockchain.headers.subscribe") if (method is "blockchain.scripthash.subscribe" or "blockchain.headers.subscribe")
@@ -416,7 +416,7 @@ public partial class MainWindowViewModel
} }
catch (Exception ex) catch (Exception ex)
{ {
StatusMessage = $"Errore: {ex.Message}"; StatusMessage = $"{Loc.Tr("msg.error")}: {ex.Message}";
} }
} }
@@ -473,7 +473,7 @@ public partial class MainWindowViewModel
catch (Exception ex) catch (Exception ex)
{ {
newLock.Dispose(); newLock.Dispose();
StatusMessage = $"Errore: {ex.Message}"; StatusMessage = $"{Loc.Tr("msg.error")}: {ex.Message}";
} }
} }
+6 -2
View File
@@ -284,5 +284,9 @@ public sealed class ElectrumServerException(string error) : Exception(error);
/// It is unlocked with an explicit reset of the certificates. /// It is unlocked with an explicit reset of the certificates.
/// </summary> /// </summary>
public sealed class CertificatePinMismatchException(string host, int port) : Exception( public sealed class CertificatePinMismatchException(string host, int port) : Exception(
$"Il certificato TLS di {host}:{port} è cambiato rispetto a quello salvato. " + $"The TLS certificate of {host}:{port} has changed from the one saved. " +
"Se il server ha rinnovato il certificato, esegui il reset dei certificati SSL."); "If the server renewed its certificate, reset the SSL certificates.")
{
public string Host { get; } = host;
public int Port { get; } = port;
}