i18n phases 1+2: BAL translation layer and translatable texts

Phase 1: new bal/i18n.py, BAL's own gettext layer (domain "bal", catalogs
read with plugin.read_file()). _() asks Electrum's catalog first, then
BAL's, then returns the English source. The Qt plugin loads the catalog of
Electrum's GUI language at start-up; the CLI stays English.

Phase 2: every user-visible GUI text is now a whole, extractable sentence
(Ruff INT rules enabled). Class-level texts are marked with N_() and
translated when shown. Stored data stays language-neutral: the status
history is written in English and translated for display, the calendar
defaults follow the GUI language, and the history label and wallet labels
are never translated because BAL uses them to recognise its transactions.

No visible change apart from the double colon fixed in the will detail.
See CHANGELOG entries 58 and 59 and PLAN_I18N.md.
This commit is contained in:
2026-09-26 21:54:50 +02:00
parent e8db76338d
commit 9c654bf2bf
17 changed files with 997 additions and 228 deletions

View File

@@ -31,6 +31,7 @@ from ...core.reminders import build_ics_reminders, write_temp_ics
from .calendar import BalCalendar, BalCalendarButton
from .common import (
DECIMAL_POINT,
N_,
NLOCKTIME_BLOCKHEIGHT_MAX,
NLOCKTIME_MAX,
Any,
@@ -67,6 +68,7 @@ from .common import (
_logger,
char_width_in_lineedit,
datetime,
format_status_history,
getSaveFileName,
log_error,
os,
@@ -207,10 +209,10 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
current_index = None
default_value = None
help_text = (
help_text = N_(
"if you choose Raw, you can insert various options based on suffix:\n"
+ " - d: number of days after current day(ex: 1d means tomorrow)\n"
+ " - y: number of years after currrent day(ex: 1y means one year from today)\n"
" - d: number of days after current day(ex: 1d means tomorrow)\n"
" - y: number of years after currrent day(ex: 1y means one year from today)\n"
)
label_text = None
tooltip_text = None
@@ -276,7 +278,9 @@ class BalTimeEditWidget(QWidget, _LockTimeEditor):
]
)
#hbox.addWidget(QLabel(self.label_text))
help_button=HelpButton(self.help_text)
# help_text and tooltip_text are class attributes marked with N_():
# translate them here, when shown.
help_button=HelpButton(_(self.help_text))
help_button.setText(self.label_text)
# Show a short label (e.g. "Delivery time" / "Check Alive") when the
# user hovers the icon, so the emoji button is self-explanatory.
@@ -625,7 +629,7 @@ class LockTimeDateEdit(QDateTimeEdit, _LockTimeEditor):
class ThresholdTimeWidget(BalTimeEditWidget):
# rich_text=True is used by the HelpButton, so HTML tags (<b>, <br>) render.
help_text = (
help_text = N_(
"<b>CHECK ALIVE</b><br><br>"
"In DATA mode:<br>"
"set the date for the \u201ccheck alive\u201d parameter.<br>"
@@ -644,7 +648,7 @@ class ThresholdTimeWidget(BalTimeEditWidget):
)
label_text = "🚨"
#label_text = "Check Alive"
tooltip_text = "Check Alive"
tooltip_text = N_("Check Alive")
base_field = "threshold"
def __init__(self, bal_window, parent, init_value=None):
@@ -659,7 +663,7 @@ class ThresholdTimeWidget(BalTimeEditWidget):
class LockTimeWidget(BalTimeEditWidget):
# rich_text=True is used by the HelpButton, so HTML tags (<b>, <br>) render.
help_text = (
help_text = N_(
"<b>DELIVERY TIME</b><br><br>"
"Set Locktime for transactions.<br>"
"Any time is needed transaction will be anticipated by 1day<br><br>"
@@ -677,7 +681,7 @@ class LockTimeWidget(BalTimeEditWidget):
#label_text = "Locktime"
# Hover tooltip for the delivery-time icon; mirrors the style of the fee
# icon tooltip ("..., click for more information") so the two are consistent.
tooltip_text = "Delivery Time, click for more information"
tooltip_text = N_("Delivery Time, click for more information")
base_field = "locktime"
def __init__(self, bal_window, parent, init_value=None):
@@ -1088,9 +1092,12 @@ class WillSettingsWidget(QWidget):
basic_mode = self.bal_window.bal_plugin.is_basic_mode()
if basic_mode:
# BASIC mode: use factory defaults (the hidden settings are
# ignored) and the fixed 30/10/1 offsets.
raw_description = self.bal_window.bal_plugin.EVENT_DESCRIPTION.default
raw_summary = self.bal_window.bal_plugin.EVENT_SUMMARY.default
# ignored) and the fixed 30/10/1 offsets. The defaults are in
# the GUI language (see BalConfig).
raw_description = (
self.bal_window.bal_plugin.EVENT_DESCRIPTION.localized_default()
)
raw_summary = self.bal_window.bal_plugin.EVENT_SUMMARY.localized_default()
threshold = None
num_reminders = 3
else:
@@ -1120,6 +1127,7 @@ class WillSettingsWidget(QWidget):
version=self.bal_window.bal_plugin.version,
num_reminders=num_reminders,
threshold=threshold,
reminder_suffix=_("(reminder {idx}/{total})"),
)
except Exception as e:
_logger.error(f"failed to generate .ics: {e}")
@@ -1199,7 +1207,7 @@ class PercAmountEdit(BTCAmountEdit):
painter.drawText(
text_rect,
int(Qt.AlignmentFlag.AlignRight | Qt.AlignmentFlag.AlignVCenter),
self.base_unit() + " or perc value",
_("{} or perc value").format(self.base_unit()),
)
@@ -1317,11 +1325,12 @@ class WillWidget(QWidget):
creation = str(BalTimestamp(self.will[w].time))
def qlabel(title, value):
label = "<b>" + _(str(title)) + f":</b>\t{str(value)}"
return QLabel(label)
# ``title`` is shown as it is: callers pass a translated
# label, or data (an heir name, a will-executor URL).
return QLabel("<b>{}:</b>\t{}".format(title, value))
detaillayout.addWidget(qlabel("Locktime", locktime))
detaillayout.addWidget(qlabel("Creation Time", creation))
detaillayout.addWidget(qlabel(_("Locktime"), locktime))
detaillayout.addWidget(qlabel(_("Creation Time"), creation))
try:
total_fees = (
self.will[w].tx.input_value() - self.will[w].tx.output_value()
@@ -1331,12 +1340,17 @@ class WillWidget(QWidget):
decoded_fees = total_fees
fee_per_byte = round(total_fees / self.will[w].tx.estimated_size(), 3)
fees_str = str(decoded_fees) + " (" + str(fee_per_byte) + " sats/vbyte)"
detaillayout.addWidget(qlabel("Transaction fees:", fees_str))
detaillayout.addWidget(qlabel(_("Transaction fees"), fees_str))
# The saved status history is English: translate it for display.
detaillayout.addWidget(
qlabel("Status:", self.will[w].status + signature_suffix(self.will[w]))
qlabel(
_("Status"),
format_status_history(self.will[w].status)
+ signature_suffix(self.will[w]),
)
)
detaillayout.addWidget(QLabel(""))
detaillayout.addWidget(QLabel("<b>Heirs:</b>"))
detaillayout.addWidget(QLabel("<b>{}</b>".format(_("Heirs:"))))
for heir_name in self.will[w].heirs:
if 'w!ll3x3c"' in heir_name:
continue
@@ -1363,7 +1377,9 @@ class WillWidget(QWidget):
)
if self.will[w].we:
detaillayout.addWidget(QLabel(""))
detaillayout.addWidget(QLabel(_("<b>Willexecutor:</b:")))
detaillayout.addWidget(
QLabel("<b>{}</b>".format(_("Willexecutor:")))
)
decoded_amount = Util.decode_amount(
self.will[w].we["base_fee"], self._bal_parent.decimal_point
)