diff --git a/.gitignore b/.gitignore index e3b7884..d43af29 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,6 @@ tmp* # Release artifacts bal_v*.zip.* tests/karen7 + +# Compiled translations: build_zip.py rebuilds them from the .po sources +*.mo diff --git a/AGENTS.md b/AGENTS.md index 7d6146f..f4b0a93 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -83,6 +83,32 @@ QT_QPA_PLATFORM=offscreen python3 -m pytest tests/test_core_*.py -q registration API differs between them (`json_db.register_dict` vs `stored_dict.register_name`). +## Translations (i18n) + +How it works: `bal/i18n.py` (Electrum's catalog first, then BAL's, then +English). Plan and decisions: `PLAN_I18N.md`. + +- Sources in git: `babel.cfg`, `bal/locale/bal.pot` (template) and + `bal/locale//LC_MESSAGES/bal.po` (one per language). The compiled + `.mo` files are NOT in git: `build_zip.py` builds them into the zip. +- Writing texts: `_("... {}").format(x)`, never f-strings or `%` inside + `_()` (Ruff INT); `N_()` for class attributes/constants, translated with + `_()` when shown. Never translate stored or compared text (wallet labels, + `HISTORY_LABEL`, the status history). +- After changing texts, refresh the catalogs (then translate the new + entries, e.g. with Poedit): + ```bash + pybabel extract -F babel.cfg --no-wrap -o bal/locale/bal.pot . + pybabel update -i bal/locale/bal.pot -d bal/locale -D bal --no-wrap + ``` +- New language: `pybabel init -i bal/locale/bal.pot -d bal/locale -D bal -l ` + (use Electrum's language code, e.g. `de_DE`). +- Dev install (symlink, not the zip): compile once to see the translations, + `pybabel compile -d bal/locale -D bal`. +- `tests/test_translations.py` checks every catalog ({} fields, `$tokens`, + no addresses/links/e-mails added by a translation) and prints a summary + when run standalone. + ## Build / release ```bash @@ -90,6 +116,8 @@ python3 build_zip.py # -> bal-electrum-plugin.zip (deterministic, prints sha25 ./make-release.sh [v0.x.y] # bump manifest version, tag, sign, push Gitea release ``` +- `build_zip.py` needs **Babel** (`pip install babel`): it compiles the + translation catalogs into the zip and stops if Babel is missing. - `make-release.sh` requires gpg and Gitea credentials (`~/.git-credentials` or `GITEA_USER`/`GITEA_TOKEN`). It bumps `bal/manifest.json` — bump the version there, never invent a new source of truth. diff --git a/CHANGELOG.md b/CHANGELOG.md index 1576e7f..68ccc01 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3327,3 +3327,124 @@ densest frames possible. the base36 count fields are 3 chars regardless of how many frames exist. **Outcome:** DONE. + +## 58. i18n phase 1: BAL translation layer (Electrum first, then BAL) + +**Date:** 2026-09-26 (branch `feature/i18n`, see `PLAN_I18N.md`) + +**Goal (owner request):** make the whole plugin translatable, starting with +Italian, so that BAL follows the language chosen in Electrum. This phase adds +the mechanism only: nothing visible changes yet. + +**What changed:** + +- New `bal/i18n.py`, BAL's own gettext layer (domain `bal`, catalogs in + `bal/locale//LC_MESSAGES/bal.mo`, read with `plugin.read_file()` so it + works from the zip and from a development checkout): + - `_()` looks a text up in Electrum's catalog first, then in BAL's, then + returns the English source (owner's decision D7: BAL reads like Electrum, + and Electrum's translations would take over if BAL ever became an internal + plugin). BAL translations whose `{}` fields differ from the source are + rejected with the same rules as Electrum's. + - `N_()` marks texts defined at import time, translated when shown. + - `init_from_config()` repeats Electrum's own language choice (Preferences > + Language, else the supported system language, else English) once, in the + Qt `Plugin.__init__`. A missing or broken catalog never stops the plugin. +- `common.py`, `theme.py`, `core/will.py`, `core/willexecutors.py` now import + `_` from `bal.i18n`. The CLI keeps Electrum's translator and stays English, + like Electrum's own CLI. + +**Verification:** new `tests/test_i18n.py`; results in entry 59. + +**Outcome:** DONE (delivered in one test ZIP together with entry 59). + +## 59. i18n phase 2: translatable texts and language-neutral stored data + +**Date:** 2026-09-26 (branch `feature/i18n`, see `PLAN_I18N.md`) + +**Goal:** make every user-visible text of the Qt GUI extractable and +translatable, without changing the English UI, and make sure no stored data +depends on the GUI language. + +**What changed:** + +- Texts built with f-strings or `.format()` inside `_()`, or glued together + from pieces, are now whole sentences with `{}` fields (Ruff INT clean). +- Class-level texts (list headers, wizard titles/messages, help texts, + download error messages, status labels) are marked with `N_()` and + translated when shown: class bodies run before the catalog is loaded. +- Helpers no longer translate their argument (`add_widget`, `get_window_title`, + `msg_set_status`, the will-detail `qlabel`): callers pass translated text. + This also stops heir names and URLs from going through the translator. +- Newly translatable: the settings help texts, the wizard download options, + the build report rows, the Ping/Select menu buttons, the QR size presets + (marked in `common.QR_PRESET_LABELS`: `core/qrtransfer.py` stays free of + Electrum imports because the Android reader ships a copy of it), and the + "(reminder i/n)" suffix of the calendar events (`build_ics_reminders` gained + `reminder_suffix`, English by default for the CLI). +- **Stored status history:** `WillItem.set_status` now always writes the + English label; the new `format_status_history()` translates it for display + (lists and will detail) and keeps unknown tokens as they are, so histories + saved by older versions (e.g. `New.Firmato.Pushed`) still display fine. +- **Calendar defaults:** `EVENT_SUMMARY` / `EVENT_DESCRIPTION` defaults follow + the GUI language (`BalConfig(..., translatable=True)`), while the English + original is what gets stored; a text written by the user is kept as it is. +- **Not translated on purpose:** `HISTORY_LABEL` (used by + `Util._label_matches_history` to recognise BAL's own local transactions), + the "BAL Invalidate/Inheritance transaction" wallet labels, + `heirs.TRANSACTION_LABEL` and the will-executor "info" fallback stored in the + config. +- Two small visible fixes: the will detail showed "Transaction fees::" and + "Status::" with a double colon. +- `pyproject.toml`: Ruff now also checks the `INT` (gettext) rules. + +**Verification (Electrum 4.8.2, Python 3.14, offscreen):** + +- `tests/test_i18n.py`: 20 passed (lookup order, rejected `{}` fields, + missing/broken catalogs, language fallback, English status storage and its + display translation, translatable config defaults, reminder suffix, QR + preset labels). +- Release-gate batch (as `make-release.sh`): 403 passed, 1 failed; the other + offline test files: 213 passed. Identical to the baseline taken before the + change; the only failures are pre-existing (`test_e5` and + `test_reproduce_none_type` need the local `tests/karen7` wallet, + `test_import_start_stop_scan_signal_wiring` fails on `main` too, + `test_gui_export_dialogs.py` hangs on `main` too). +- `smoke_test.py`, `build_zip.py` and `external_zip_test.py`: OK. +- Ruff: INT clean; no new errors (the only one left is the pre-existing + `dialogs.py` I001). +- Babel extraction (GUI + core): 424 strings (about 3,000 words), was 323. + +**Outcome:** DONE, pending the owner's test of the ZIP. + +## 60. i18n phase 3: Italian catalog + +**Date:** 2026-09-26 (branch `feature/i18n`, see `PLAN_I18N.md`) + +**Goal (owner request):** with Electrum set to Italian, BAL shows in Italian. + +**What changed:** + +- `babel.cfg` (extraction mapping, CLI and wallet_util excluded), + `bal/locale/bal.pot` (template, 424 texts) and the Italian catalog + `bal/locale/it_IT/LC_MESSAGES/bal.po`: 43 entries copied from Electrum's + own it_IT catalog (same English text), the others drafted following the + owner's glossary (D1). Draft to be reviewed by the owner (D2). +- `build_zip.py` compiles every `bal.po` into `bal.mo` inside the zip + (Babel required, the build stops without it); `.po`/`.pot` are not + shipped and `*.mo` is ignored by git (D4). +- New `tests/test_translations.py`: every catalog compiles; `{}` fields and + `$tokens` match the English text; no Bitcoin address, e-mail, URL or long + letters-and-digits word that the English text does not have (patterns from + Electrum's `electrum-locale/update.py`). +- `AGENTS.md`: how to update the catalogs, add a language, compile for the + development install. + +**Verification (Electrum 4.8.2, offscreen):** `tests/test_translations.py` +OK (it_IT: 424 translated, 0 untranslated, 0 fuzzy); all offline tests 640 +passed, only the pre-existing failures of entry 59; the built zip contains +only `bal/locale/it_IT/LC_MESSAGES/bal.mo` for the translations and +`external_zip_test.py` loads it; Ruff: no new errors. + +**Outcome:** DONE, pending the owner's review of the Italian texts and test +of the ZIP. diff --git a/PLAN_I18N.md b/PLAN_I18N.md new file mode 100644 index 0000000..4fdbc1e --- /dev/null +++ b/PLAN_I18N.md @@ -0,0 +1,453 @@ +# PLAN_I18N — Italian translation of the BAL Electrum plugin (i18n) + +> **Status:** DRAFT. Every phase still needs the owner's explicit "OK" before any code is written (HANDOFF.md, rule R4). +> **Prepared:** 2026-09-25, from a read-only analysis (no code was changed). Owner's decisions recorded on 2026-09-26 (section 6). +> **Code base analysed:** `bal-electrum-plugin` `main` @ `4a33116` (2026-09-19, manifest `0.7.0`) and Electrum `4.8.0`. +> **Line numbers** refer to that commit. After merging a newer `main`, find the code by content. + +## 0. Read this first + +1. Read `HANDOFF.md` (rules R1–R4, ZIP-FIRST, CREDIT-SAVING) and `AGENTS.md` (venvs, tests). They still apply. +2. **One exception to HANDOFF.md.** Section 5 ("Git / delivery workflow") and section 7, step 7, say: work directly on `main`, push to `origin/main`, run `make-release.sh`. **That does NOT apply to this task.** See section 3 (dedicated branch). +3. The owner's decisions (D1–D7) are recorded in section 6. For anything not covered there, ask the owner (rule R3). + +## 1. Goal and scope + +**Goal:** when Electrum is set to Italian, the whole BAL interface appears in Italian. The mechanism must allow adding more languages later just by adding one `.po` file. + +**In scope** +- The Qt GUI of the plugin (`bal/gui/qt/`) and the GUI-free strings it displays (`bal/core/`). +- One language for now: Italian (`it_IT`). + +**Out of scope** (owner's decisions, or by design) +- **BAL Easy Heirs**: excluded by the owner. +- **CLI output** (`bal/cli/`): stays English, exactly like Electrum (`run_electrum`: "the CLI is intentionally always non-localized"). +- `bal/wallet_util/` (standalone helper scripts), the Android reader (`android/`, which uses Android resources), the manual, the website and the docs. +- Other languages: later. The infrastructure will support them. + +## 2. Background (verified) + +### 2.1 How Electrum 4.8.0 does it + +- **gettext with one domain, `electrum`.** Catalogs live in `electrum/locale/locale//LC_MESSAGES/electrum.mo` (`electrum/i18n.py`, `LOCALE_DIR`). Every string goes through `electrum.i18n._()`. +- **The language is set once, at startup, for the GUI only** (`run_electrum` L433–451): + - it uses `config.LOCALIZATION_LANGUAGE` (config key `language`); + - if that is empty, it uses `electrum.gui.default_lang.get_default_language(gui_name)`, i.e. `QLocale.system().name()`, **but only if** that value is in `electrum.i18n.languages`; otherwise `en_UK`; + - `en_*` means no translation (the English source strings are shown); + - changing the language requires a restart. +- **Strings evaluated too early stay English.** Anything evaluated before `set_language()` (for example at import time) is not translated. +- **Safety check inside `_()`.** The decorator `_ensure_translation_keeps_format_string_syntax_similar` rejects a translation whose `{}` fields differ from the source. `_("")` returns `""`. There is an optional `context=` argument (pgettext). +- **Coding rules** (comments in `i18n.py`): no f-strings inside `_()`, no `%` formatting; write `_("… {}").format(x)`. There are no plural forms (`ngettext` is not used anywhere). +- **Pipeline:** + 1. `contrib/locale/push_locale.py` runs xgettext over `electrum/**/*.py`. This includes *internal* plugins. + 2. The strings go to Crowdin, where volunteers translate them. + 3. The `.po` files are stored in the `electrum-locale` repo. + 4. `contrib/locale/build_locale.sh` compiles them with msgfmt into `.mo` files and writes `stats.json`. +- **Translation security:** + - `electrum-locale/update.py` refuses any translation whose `msgstr` contains a Bitcoin address, an e-mail address, a URL, or a long word mixing letters and digits; + - `llm_proofreader/` checks every translation PR for vandalism in CI. +- **External plugins:** + - they are always ZIP files, imported with `zipimport` as `electrum_external_plugins.`; + - `BasePlugin.read_file(filename) -> bytes` reads a file from inside the ZIP, or from the internal plugin folder; + - **Electrum provides nothing for translating external plugins.** Their strings are not in `electrum.mo`. +- `i18n.py`, `gui/default_lang.py` and `read_file()` are identical in 4.7.2, 4.8.0 and 4.8.2. +- **Swiss locales** (`it_CH`, `de_CH`, `fr_CH`) are not in Electrum's list, so Electrum starts in English until the user picks a language. BAL must simply follow Electrum and not detect the language itself. Otherwise the UI would be half in one language and half in another. + +### 2.2 BAL today (`main` @ `4a33116`) + +- Source strings are in English. Most are wrapped in `_`, imported from `electrum.i18n` in these places: + - `gui/qt/common.py:55`, which re-exports it to every `gui/qt` module; + - `gui/qt/theme.py:90,108`; + - `core/will.py:32` and `core/willexecutors.py:25`; + - `cli/controller.py:29`. +- The package has 483 `_()` calls in 11 files. xgettext finds 365 distinct strings (≈ 2,230 words). +- **With Electrum set to Italian, only 39 of the 365 strings (≈ 11 %) appear in Italian.** They are the generic words that Electrum already translates (Cancel, Save, Address…). Everything specific to BAL stays in English. +- **Issues to fix before translating** (exact locations are in the Appendix): + - 14 f-strings or `.format()` calls inside `_()`: these strings can never be translated (Ruff INT001/INT002); + - 15 `_()` calls with a non-literal argument: extraction tools cannot find the text; + - 14 concatenations around `_()`: the word order breaks in other languages; + - 18 `_()` calls evaluated at import time (in class bodies): they would stay English (see 4.2); + - 2 × `_("")`: plain gettext returns the catalog header instead of an empty string; + - 12 settings help texts (`HelpButton("…")`) and the help texts of 5 `add_widget()` calls are never translated (≈ 500 words in total); + - other text shown to the user without `_()`. +- **Some stored data depends on the UI language.** + - `WillItem.set_status()` (`core/will.py:1344`) appends the *translated* status label to `WillItem.status`. + - That field is saved via `to_dict()` in the wallet DB (`wallet.db.get_dict("will")`, `gui/qt/window.py:232`), in exported files and in QR transfers. + - Real example from `tests/samanta7`: `"New.Firmato.Pushed.Checked.Replaced"`. Electrum's Italian catalog already translates *Signed*, *Error* and *Expired*, so the saved history mixes languages today. +- **Default texts in `core/plugin_base.py` exist only in English:** + - `HISTORY_LABEL` (around L246, token `{willexecutor}`); + - `EVENT_DESCRIPTION` and `EVENT_SUMMARY` (around L331–337, tokens `$wallet_name` and `$heirs_complete`). + +## 3. Branch and workflow for this task + +The owner decided, on Truman's suggestion, to do this work on a **separate branch on Gitea**, not on `main`. + +- **Branch:** `feature/i18n` (proposed name, see D6), created from `main`. +- **Never** commit or push to `main` during this task. +- **Never** run `make-release.sh` from the branch: it tags and publishes a Gitea release. Build test ZIPs with `python3 build_zip.py` only. +- **At the start of every phase:** + 1. merge `origin/main` into `feature/i18n`; + 2. resolve any conflicts; + 3. run the tests again. +- **Where conflicts are likely:** + - `gui/qt/dialogs.py`, `window.py`, `lists.py`, `plugin.py`, `widgets.py` and `core/will.py`: all were changed on `main` on 2026-09-04, 09-14 and 09-19, and commit `fb88d75` alone changed ≈ 1,800 lines of `dialogs.py`; + - `CHANGELOG.md`: both sides append at the end, so keep both entries and renumber. +- **ZIP-FIRST for every phase:** + 1. build the ZIP; + 2. the owner (and Truman) test it; + 3. only after the owner's explicit OK, commit on `feature/i18n` and push to `origin/feature/i18n` (who does this: see D3). +- Add one numbered `CHANGELOG.md` entry per phase, in English. +- Run ruff and the tests before every ZIP (HANDOFF.md). The known pre-existing test failures are listed in HANDOFF.md, section 7, step 3. +- **At the end:** open a Pull Request `feature/i18n` → `main` on Gitea (there is a precedent: `feature/bal-qr-transfer`). Release from `main` only after the merge. + +## 4. Design (decided) + +**Chosen:** a gettext catalog owned by BAL, domain `bal`, shipped inside the plugin ZIP, that follows Electrum's language and is consulted **after** Electrum's own catalog (D7). + +**Rejected:** +- keep using `electrum.i18n._`: 89 % of the text stays English; +- Python dictionaries or JSON files: a home-made format with no tooling; +- Qt Linguist (`.ts` / `.qm`): Electrum desktop does not use it, and `core/` has no Qt. + +### 4.1 New module `bal/i18n.py` + +| Function | Behaviour | +|---|---| +| `_(msg)` | 1. `""` returns `""`. 2. **Electrum first (D7):** if `electrum.i18n._(msg)` differs from `msg`, return it (Electrum has already applied its own `{}` check). 3. Otherwise, if a BAL catalog is loaded and translates `msg`, apply the `{}` safety check and return the translation. The check uses the same logic as Electrum's decorator: **copy it** with an attribution comment (MIT); do not import Electrum's private function. 4. Otherwise return `msg` (English). The CLI and English behave exactly as in Electrum, because no BAL catalog is loaded there. | +| `N_(msg)` | Returns `msg` unchanged. It marks strings defined in tables, constants and class attributes, which are translated later with `_()` when displayed. Babel extracts `N_` by default. | +| `set_language(plugin, lang)` | `None`, `""` or `en_*`: no catalog. Otherwise read `locale//LC_MESSAGES/bal.mo` with `plugin.read_file()`, falling back to `locale//…`, and build `gettext.GNUTranslations(io.BytesIO(data))`. If the catalog is missing or broken, log at info level and continue in English. Never raise. | +| `init_from_config(plugin, config)` | `lang = config.LOCALIZATION_LANGUAGE`. If it is empty, use `electrum.gui.default_lang.get_default_language(gui_name="qt")`, guarding against `ImportError` as `run_electrum` does. Then call `set_language()`. | + +- **Why Electrum first (D7):** BAL follows Electrum's wording wherever Electrum has it. If Electrum ever ships BAL as an internal plugin, Electrum's own translations (Crowdin) would be used automatically and the BAL catalog would only fill the gaps. +- **Trade-off of D7:** for a text that Electrum already translates, the BAL catalog cannot override Electrum's wording, not even with the D1 glossary. If one such case reads badly in BAL, handle that single case with the owner. +- A text that Electrum "translates" into the identical English text counts as not found and falls through to the BAL catalog. This is harmless: the BAL catalog is prefilled with Electrum's translations (3.3). +- **Do not use `gettext.translation(localedir=…)`.** When pointed inside a ZIP it silently returns `NullTranslations` (verified). +- `plugin.read_file()` is the same API BAL already uses for its icons. It works both for the ZIP install and for the dev symlink install (`AGENTS.md`). +- The state is module-level and set once. Changing the language requires restarting Electrum, as in Electrum itself. + +### 4.2 Where the language is set + +- In `bal/gui/qt/plugin.py`, `Plugin.__init__` (L67): call `init_from_config(self, config)` first. +- The CLI entry point (`bal/cli/plugin.py`) does not call it, so the CLI stays English. +- **Consequence:** `gui/qt/plugin.py` imports `dialogs`, `widgets` and `window` at module level (L49–51), and those import `lists`. So every class body is evaluated *before* `Plugin.__init__` runs, and any `_()` evaluated at import time would stay English. The cases in Appendix D must be converted in Phase 2. + +### 4.3 Files and tools + +``` +babel.cfg # extraction mapping (repo root) +bal/locale/bal.pot # template, generated +bal/locale/it_IT/LC_MESSAGES/bal.po # Italian translations (source of truth) +bal/locale/it_IT/LC_MESSAGES/bal.mo # compiled (see D4) +``` + +The tool is **Babel** (`pip install babel`). It is pure Python and works on Windows without GNU gettext. These commands were tested on this code base: + +```ini +# babel.cfg +[ignore: bal/cli/**] +[ignore: bal/wallet_util/**] +[python: bal/**.py] +``` + +```bash +mkdir -p bal/locale # extract does not create it +pybabel extract -F babel.cfg --no-wrap -o bal/locale/bal.pot . +pybabel init -i bal/locale/bal.pot -d bal/locale -D bal -l it_IT # once +pybabel update -i bal/locale/bal.pot -d bal/locale -D bal --no-wrap # after each extract +pybabel compile -d bal/locale -D bal --statistics +``` + +- Today this extracts 323 strings (≈ 1,900 words) from the GUI and core. +- After Phase 2, expect about 360 strings (≈ 2,500 words). +- Poedit (free) can be used to translate or review the `.po` file by hand. + +### 4.4 Translation safety (as in Electrum) + +- Add a check script (for example `scripts/check_translations.py`) and run it before every ZIP. It must check that: + - every `.po` file compiles; + - the `{}` fields are identical to the source (the same rule as the runtime check); + - the `$tokens` are identical (`$wallet_name`, `$heirs_complete`, …): Electrum does not check these; + - no `msgstr` contains a Bitcoin address, an e-mail address, a URL or a long letter-and-digit word (use the regexes from `electrum-locale/update.py`), unless the same text is in the `msgid`. + + It must also print the counts of translated, untranslated and fuzzy strings. +- Never put addresses or URLs inside translatable strings; pass them with `{}`. +- The owner reviews every Italian string, especially the warnings about invalidation, locktime, fees and signing. + +## 5. Phases + +Each phase follows HANDOFF.md: DISCOVER → PLAN (owner's "OK") → EXECUTE → VERIFY → ZIP → test by the owner and Truman → commit on `feature/i18n`. +Phases 1 and 2 change nothing visible, so they may be delivered as a single ZIP/test cycle (see D5). + +### Phase 1 — Infrastructure (no visible change) + +- NEW `bal/i18n.py` (section 4.1), with docstrings explaining *why* (rule R2). +- `bal/gui/qt/common.py:55`: replace `from electrum.i18n import _` with an import of `_` and `N_` from `bal.i18n`. Use a relative import, zip-safe like the existing `from ...core…` imports. +- `bal/gui/qt/theme.py:90,108`: change the local imports to `bal.i18n`. +- `bal/core/will.py:32` and `bal/core/willexecutors.py:25`: import from `bal.i18n`. +- `bal/cli/controller.py:29`: **unchanged** (the CLI stays English by design). +- `bal/gui/qt/plugin.py`, `Plugin.__init__`: call `init_from_config(self, config)`. +- NEW `tests/test_i18n.py`, covering: + - the empty string; + - Electrum's translation wins; the BAL catalog is used only when Electrum has none (D7); + - rejection of a `{}` mismatch; + - `en_UK` means no catalog; + - a missing `.mo` does not crash; + - a catalog loaded from bytes (use a small test catalog). +- **Verify:** + - the existing tests (baseline as in HANDOFF.md, section 7); + - `tests/smoke_test.py`; + - `build_zip.py` followed by `tests/external_zip_test.py`; + - manually on Windows 11 with Electrum 4.8.0, once in Italian and once in English: the UI must look exactly as before. + +### Phase 2 — String clean-up and language-neutral stored data (no visible change) + +- **2.0** Merge `origin/main` first. +- **2.1** Appendix A: turn `_(f"…")` and `_("…".format(x))` into `_("… {}").format(x)`. Include `cli/controller.py`, for lint consistency only. +- **2.2** Appendix C: turn each concatenation into one string with `{}`. + - Keep HTML outside the strings where possible, e.g. `"{}".format(_("Support:"))`. + - Fix the malformed `_("Willexecutor: str` that splits on `.`, translates the tokens that are known English labels (with an optional `NOT ` prefix), and leaves unknown or legacy tokens unchanged (`New`, `Firmato`, …); + - use it wherever the status is shown (`lists.py:589`, `widgets.py:1336`); + - this stays backward compatible: existing wallets keep displaying correctly, and older BAL versions still read plain English; + - test it with the real legacy strings in `tests/samanta7`. +- **2.9** **Default texts** (`core/plugin_base.py`: `HISTORY_LABEL`, `EVENT_DESCRIPTION`, `EVENT_SUMMARY`): + - mark the English defaults with `N_()`; + - when the stored value is missing **or equal to the English default**, use the translated default at the moment the text is used (calendar export, history label); + - the tokens must survive translation. + - **Changed during Phase 2 (safety):** `HISTORY_LABEL` stays English and is *not* translatable. `Util._label_matches_history()` uses it to recognise BAL's own local transactions (stale-history cleanup, spendable UTXOs), so a label that changed with the GUI language would no longer match the transactions saved before. Only `EVENT_SUMMARY` and `EVENT_DESCRIPTION` follow the language (`BalConfig(..., translatable=True)`). The same rule applies to every stored or compared text (wallet labels, `heirs.TRANSACTION_LABEL`, the status history). +- **2.10** `pyproject.toml`: add `"INT"` to `[tool.ruff.lint] select`. + - Ruff's hint suggests `%` formatting: **do not follow it**. Use `.format()`, which is Electrum's rule. + - `make-release.sh` treats Ruff errors as blocking, so INT must end up clean. +- **Verify:** + - Ruff INT is clean; + - the extraction count is about 360 (compare with 4.3); + - the tests pass, plus new tests for 2.8 and 2.9; + - manually in English: no visible text changed. + +### Phase 3 — Italian catalog (visible change) + +- **3.0** Merge `origin/main` first. +- **3.1** Add `babel.cfg` and the commands from 4.3, documented in `README.md` or `AGENTS.md`. +- **3.2** Generate `bal/locale/bal.pot` and create `it_IT/LC_MESSAGES/bal.po`. +- **3.3** Draft the translation using the glossary (D1); the owner reviews it (D2). Where Electrum's `it_IT` catalog already translates the exact same English text, copy Electrum's translation, so the two UIs match. +- **3.4** `build_zip.py`: + - compile `.po` into `.mo`, according to D4; + - put only the `.mo` files in the ZIP (no `.po` / `.pot`); + - run the check script (4.4) before zipping. +- **Verify:** + - Electrum in Italian shows BAL in Italian; + - Electrum in English shows BAL in English; + - missing entries fall back to English; + - `external_zip_test.py` passes; + - it works on Windows 11 with Electrum 4.8.0 (and 4.7.2); + - manual walkthrough of: wizard, settings, heirs list, will list and detail, will-executors, check / sign / broadcast, invalidate, export / import, QR transfer, calendar export. + +### Phase 4 — Final test and merge + +- The owner and Truman test the final branch ZIP. +- Merge `origin/main` into the branch one last time and test again. +- Open the Pull Request `feature/i18n` → `main` on Gitea. After the merge, follow the normal release flow from `main`. + +## 6. Decisions + +Recorded on 2026-09-26 from the owner's answers. + +- **D1 — Italian glossary.** Rule: follow Electrum's `it_IT` wording wherever Electrum already has the term or the sentence. Grammatical variants (nouns, past participles, plurals, capitals in headers and buttons) are adapted in the draft and checked by the owner during the review (D2). + + | English | Italian | + |---|---| + | will | testamento | + | heir / heirs | erede / eredi | + | will-executor | Will-Executor (kept in English) | + | check-alive | verifica se sei vivo | + | invalidate | invalida il piano | + | invalidation (noun) | invalidazione del piano | + | anticipate / postpone | anticipa / posticipa | + | locktime | locktime (kept in English; owner, 2026-09-26) | + | fee | commissione (as Electrum) | + | broadcast | trasmetti il piano | + | sign / signed | firma / firmato (as Electrum) | + | wallet | portafogli (as Electrum) | + +- **D2 — Who translates.** The developer drafts `it_IT/bal.po`; the owner reviews and corrects it (for example in Poedit). For later languages: Weblate (supports Gitea and `.po`) or Crowdin, as Electrum does. +- **D3 — Branch and commits.** The developer creates `feature/i18n`, and commits and pushes on it, each time only after the owner's explicit OK (ZIP-FIRST). +- **D4 — `.mo` files.** `build_zip.py` compiles `.po` into `.mo` at build time with Babel; `.mo` files stay out of git (add `*.mo` to `.gitignore`). If Babel is missing, the build stops with a clear error, so an English-only ZIP is never shipped by mistake. Reasons: the `.po` is the only source (no stale `.mo`), every change to a translation is readable in review, and it is how Electrum works. +- **D5 — Cycles.** Phases 1 and 2 are delivered in one ZIP/test cycle. Phase 1 is not merged into `main` early. +- **D6 — Branch name:** `feature/i18n`. +- **D7 — Lookup order.** Electrum's catalog first, then BAL's, then English (see 4.1). Proposed by the owner so that BAL and Electrum read as one, also if Electrum ever integrates BAL. + +## 7. Reminders for the developer + +- Talk to the owner in **Italian**, with simple words and no jargon (the owner is not a programmer). Code, comments, docstrings, UI source strings, docs and commits are in **English**. +- UI source strings stay in English; Italian lives only in the `.po` file. +- Never invent. When in doubt, stop and ask. +- Before writing any code, present the phase PLAN and wait for "OK". +- ZIP-FIRST; run ruff and the tests before every ZIP; one CHANGELOG entry per phase; keep reports brief (CREDIT-SAVING). +- Wallet data compatibility comes first: apart from 2.8 (status labels), do not change what is stored. + +## Appendix — Inventory at `main` @ `4a33116` + +Generated by a static scan (AST, Ruff `INT` and xgettext). The line numbers will move after merging a newer `main`: re-run the scan or search by content. These lists cover the known cases; Phase 2 still needs a full sweep. + +#### A. f-string / `.format()` inside `_()` — Ruff INT001/INT002 (14) + +| Location | Rule | +|---|---| +| `bal/cli/controller.py:92` | INT001 | +| `bal/cli/controller.py:98` | INT001 | +| `bal/cli/controller.py:104` | INT001 | +| `bal/gui/qt/common.py:225` | INT002 | +| `bal/gui/qt/common.py:240` | INT002 | +| `bal/gui/qt/dialogs.py:1226` | INT001 | +| `bal/gui/qt/dialogs.py:2491` | INT001 | +| `bal/gui/qt/window.py:831` | INT001 | +| `bal/gui/qt/window.py:836` | INT001 | +| `bal/gui/qt/window.py:1090` | INT001 | +| `bal/gui/qt/window.py:1091` | INT001 | +| `bal/gui/qt/window.py:2228` | INT001 | +| `bal/gui/qt/window.py:2236` | INT001 | +| `bal/gui/qt/window.py:2246` | INT001 | + +#### B. `_()` with a non-literal argument (not extractable) (15) + +| Location | Code | +|---|---| +| `bal/core/will.py:1344` | `_(self.STATUS[status][0])` | +| `bal/gui/qt/common.py:181` | `_(label)` | +| `bal/gui/qt/dialogs.py:327` | `_(self.message)` | +| `bal/gui/qt/dialogs.py:2288` | `_(msg)` | +| `bal/gui/qt/dialogs.py:2291` | `_(msg)` | +| `bal/gui/qt/lists.py:1240` | `_(label)` | +| `bal/gui/qt/plugin.py:1086` | `_(title)` | +| `bal/gui/qt/widgets.py:284` | `_(self.tooltip_text)` | +| `bal/gui/qt/widgets.py:1320` | `_(str(title))` | +| `bal/gui/qt/window.py:290` | `_(title)` | +| `bal/gui/qt/window.py:942` | `_(message)` | +| `bal/gui/qt/window.py:2217` | `_(self.DOWNLOAD_FAILED_TOR_MESSAGE)` | +| `bal/gui/qt/window.py:2219` | `_(self.DOWNLOAD_FAILED_MESSAGE)` | +| `bal/gui/qt/window.py:2253` | `_(self.DOWNLOAD_FAILED_TOR_MESSAGE)` | +| `bal/gui/qt/window.py:2255` | `_(self.DOWNLOAD_FAILED_MESSAGE)` | + +#### C. Concatenations around `_()` (14) + +| Location | Expression | +|---|---| +| `bal/core/will.py:1344` | `'.' + (('NOT ' if not value else '') + _(self.STATUS[status][0]))` | +| `bal/gui/qt/dialogs.py:1010` | `_('Wallet balance is too low: {} satoshi available, but the miner and…` | +| `bal/gui/qt/dialogs.py:2206` | `messages[reason] + '\n\n' + _('Skipped')` | +| `bal/gui/qt/dialogs.py:2209` | `_("Could not build the will, and the exact cause could not be determi…` | +| `bal/gui/qt/dialogs.py:2419` | `_('Expiration date: ') + str(BalTimestamp(self.threshold))` | +| `bal/gui/qt/dialogs.py:2425` | `_('Valid Txs:') + str(len(Will.only_valid_list(self.will)))` | +| `bal/gui/qt/dialogs.py:2427` | `_('Total Txs:') + str(len(self.will))` | +| `bal/gui/qt/lists.py:679` | `' ' + _('Build Your Will')` | +| `bal/gui/qt/plugin.py:301` | `'Bal ' + _('Bitcoin After Life')` | +| `bal/gui/qt/plugin.py:1034` | `'' + _('Support:') + ''` | +| `bal/gui/qt/plugin.py:1086` | `_('BAL - ') + _(title)` | +| `bal/gui/qt/widgets.py:1320` | `'' + _(str(title)) + f':\t{str(value)}'` | +| `bal/gui/qt/window.py:1022` | `_('Electrum was unable to deserialize the transaction:') + '\n' + str…` | +| `bal/gui/qt/window.py:1091` | `msg + _(f'signing: {tosign}')` | + +#### D. `_()` evaluated at import time (class body) (18) + +| Location | Scope | Call | +|---|---|---| +| `bal/gui/qt/dialogs.py:419` | class body | `_('Bitcoin After Life Will-Executors')` | +| `bal/gui/qt/dialogs.py:420` | class body | `_('Choose willexecutors download method')` | +| `bal/gui/qt/dialogs.py:528` | class body | `_('Configure and select your willexecutors')` | +| `bal/gui/qt/dialogs.py:548` | class body | `_('')` | +| `bal/gui/qt/lists.py:131` | class body | `_('Name')` | +| `bal/gui/qt/lists.py:132` | class body | `_('Address')` | +| `bal/gui/qt/lists.py:133` | class body | `_('Amount')` | +| `bal/gui/qt/lists.py:340` | class body | `_('Locktime')` | +| `bal/gui/qt/lists.py:341` | class body | `_('Txid')` | +| `bal/gui/qt/lists.py:342` | class body | `_('Will-Executor')` | +| `bal/gui/qt/lists.py:343` | class body | `_('Status')` | +| `bal/gui/qt/lists.py:344` | class body | `_('Server')` | +| `bal/gui/qt/lists.py:907` | class body | `_('')` | +| `bal/gui/qt/lists.py:908` | class body | `_('Url')` | +| `bal/gui/qt/lists.py:909` | class body | `_('S')` | +| `bal/gui/qt/lists.py:910` | class body | `_('Base fee')` | +| `bal/gui/qt/lists.py:911` | class body | `_('Info')` | +| `bal/gui/qt/lists.py:912` | class body | `_('Default Address')` | + +#### E. `_("")` (2) + +| Location | Call | +|---|---| +| `bal/gui/qt/dialogs.py:548` | `_('')` | +| `bal/gui/qt/lists.py:907` | `_('')` | + +#### F. Settings help texts never passed to `_()` (12) + +| Location | Widget | Text | +|---|---|---| +| `bal/gui/qt/plugin.py:715` | HelpButton | When checking, automatically sign and broadcast the will transactions… | +| `bal/gui/qt/plugin.py:778` | HelpButton | `How many reminder alarms the exported calendar (.ics) event contains.…` | +| `bal/gui/qt/plugin.py:789` | HelpButton | `Default message to be used in event summary\nVariables:\n $wallet_na…` | +| `bal/gui/qt/plugin.py:803` | HelpButton | `Default message to be used in event description\nVariables:\n $walle…` | +| `bal/gui/qt/plugin.py:818` | HelpButton | URL of the server that provides the will-executor list. Only availab… | +| `bal/gui/qt/plugin.py:829` | HelpButton | `Command used to open .ics calendar files.\nLeave empty to use the sys…` | +| `bal/gui/qt/plugin.py:844` | HelpButton | `After each check, save the valid will transactions into the wallet's …` | +| `bal/gui/qt/plugin.py:858` | HelpButton | `Label applied to the will transactions saved into the wallet's local …` | +| `bal/gui/qt/plugin.py:880` | HelpButton | Broadcast all transactions to willexecutors including those already p… | +| `bal/gui/qt/plugin.py:891` | HelpButton | `Run the 'Build your will' wizard every time the wallet is closed or E…` | +| `bal/gui/qt/plugin.py:909` | HelpButton | When a new transaction arrives for the wallet, automatically rebuild … | +| `bal/gui/qt/plugin.py:933` | HelpButton | `Payload size of a single QR code when exporting a will via QR.\n\nLar…` | + +#### G. `add_widget()` calls (label translated at runtime, help text never) (5) + +| Location | Label | Help text | +|---|---|---| +| `bal/gui/qt/plugin.py:697` | Hide Replaced | Hide replaced transactions from will detail and l… | +| `bal/gui/qt/plugin.py:705` | Hide Invalidated | Hide invalidated transactions from will detail an… | +| `bal/gui/qt/plugin.py:727` | Panel editable Date and Fee | When enabled, the delivery-time and check-alive d… | +| `bal/gui/qt/plugin.py:742` | `Max Will-Executor Fee (satoshi)` | `Maximum fee (in satoshi) allowed to be paid to a …` | +| `bal/gui/qt/plugin.py:757` | User Type | `Choose how much detail the plugin shows.\n\nBASIC…` | + +#### H. Other user-visible text not (correctly) translatable — known cases, not exhaustive + +| Location | Code | Note | +|---|---|---| +| `bal/gui/qt/dialogs.py:380` | `title = "Bitcoin After Life Heirs"` | Wizard title, shown untranslated at `dialogs.py:325` | +| `bal/gui/qt/dialogs.py:527` | `title = "Bitcoin After Life Will-Executors"` | Same | +| `bal/gui/qt/dialogs.py:547` | `title = "Bitcoin After Life Will Settings"` | Same | +| `bal/gui/qt/lists.py:1472` | `setText("New Will Executor")` | Not wrapped | +| `bal/gui/qt/plugin.py:673` | `QPushButton("Rebroadcast transactions")` | Not wrapped | +| `bal/gui/qt/widgets.py:1323` | `qlabel("Locktime", …)` | Translated inside `qlabel()` but not extractable | +| `bal/gui/qt/widgets.py:1324` | `qlabel("Creation Time", …)` | Same | +| `bal/gui/qt/widgets.py:1334` | `qlabel("Transaction fees:", …)` | Same; `qlabel()` adds another `:` | +| `bal/gui/qt/widgets.py:1336` | `qlabel("Status:", …)` | Same; also shows the stored status (see 2.8) | +| `bal/gui/qt/widgets.py:1339` | `QLabel("Heirs:")` | Not wrapped | +| `bal/gui/qt/widgets.py:1366` | `_("Willexecutor: Optional[str]: """Build the ``.ics`` content with one VEVENT per reminder date. @@ -169,6 +170,10 @@ def build_ics_reminders( num_reminders: requested reminder count (ADVANCED mode only). now: "today" reference; defaults to ``datetime.now()``. threshold: check-alive date (ADVANCED mode only; required there). + reminder_suffix: text appended to each event summary, with the + ``{idx}`` and ``{total}`` fields. This module stays free of + Electrum imports, so the GUI passes it already translated; the + English default keeps the CLI output unchanged. Returns: The ``.ics`` content string, or ``None`` when no reminder falls in the @@ -210,7 +215,9 @@ def build_ics_reminders( # The visible date of this event: "offset" days before the deadline. event_dt = format_time(locktime - timedelta(days=offset)) # Suffix the summary so the N events are easy to tell apart. - event_summary = ical_escape(f"{summary_base} (reminder {idx}/{total})") + event_summary = ical_escape( + "{} {}".format(summary_base, reminder_suffix.format(idx=idx, total=total)) + ) lines.extend([ "BEGIN:VEVENT", # Offset in the UID keeps each event unique (no merging). diff --git a/bal/core/will.py b/bal/core/will.py index 71d7ff1..83bad97 100644 --- a/bal/core/will.py +++ b/bal/core/will.py @@ -29,7 +29,6 @@ The status flags themselves (the source of truth) stay here; only the mapping from datetime import datetime, timezone from electrum.address_synchronizer import TX_HEIGHT_FUTURE, TX_HEIGHT_LOCAL -from electrum.i18n import _ from electrum.logging import Logger, get_logger from electrum.transaction import ( PartialTransaction, @@ -44,6 +43,7 @@ from electrum.util import ( bfh, ) +from ..i18n import N_, _ from .heirs import WillExecutorFeeTooHighException from .util import Util, copy_structure from .willexecutors import Willexecutors @@ -1273,6 +1273,34 @@ class Will: +def format_status_history(status: str) -> str: + """Return the saved status history of a will item in the GUI language. + + ``WillItem.status`` is a dot-separated history such as + ``".Signed.Pushed.NOT Valid"``. It is stored in English (see + ``WillItem.set_status``) and translated only here, for display. A token + that is not a known English label is shown unchanged: this keeps working + the histories written by older versions, which stored the label already + translated (for example ``"Firmato"``). + """ + labels = {label for label, _flag in WillItem.STATUS_DEFAULT.values()} + labels.update(WillItem.LEGACY_STATUS_LABELS) + + def translate(token): + # "ERROR!!!" is glued to the end of the history when an item has no + # id (see WillItem.__init__): keep it, translate what comes before. + suffix = "" + if token.endswith("ERROR!!!"): + token, suffix = token[: -len("ERROR!!!")], "ERROR!!!" + if token in labels: + token = _(token) + elif token.startswith("NOT ") and token[len("NOT "):] in labels: + token = _("NOT {}").format(_(token[len("NOT "):])) + return token + suffix + + return ".".join(translate(token) for token in status.split(".")) + + class WillItem(Logger): # Default status flags for an inheritance transaction. # Each entry maps an internal status key to [human-readable label, default @@ -1286,28 +1314,37 @@ class WillItem(Logger): # * "UPDATED" was added: the transaction was spendable AND valid, and a new # transaction replaces it while keeping the SAME locktime and SAME heirs. # UPDATED keeps the VALID flag (see set_status). + # + # The labels are marked with N_() but stay English here: they are written + # into the saved status history (see set_status) and translated only when + # shown, by format_status_history(). STATUS_DEFAULT = { - "ANTICIPATED": ["Anticipated", False], - "BROADCASTED": ["Broadcasted", False], - "CHECKED": ["Checked", False], - "CHECK_FAIL": ["Check Failed", False], - "COMPLETE": ["Signed", False], - "CONFIRMED": ["Confirmed", False], - "ERROR": ["Error", False], - "EXPIRED": ["Expired", False], - "EXPORTED": ["Exported", False], - "IMPORTED": ["Imported", False], - "INVALIDATED": ["Invalidated", False], - "MEMPOOL": ["Mempool", False], - "PUSH_FAIL": ["Push failed", False], - "PUSHED": ["Pushed", False], - "PARTIALLY_SIGNED": ["Partially Signed", False], - "REPLACED": ["Replaced", False], - "RESTORED": ["Restored", False], - "UPDATED": ["Updated", False], - "VALID": ["Valid", True], + "ANTICIPATED": [N_("Anticipated"), False], + "BROADCASTED": [N_("Broadcasted"), False], + "CHECKED": [N_("Checked"), False], + "CHECK_FAIL": [N_("Check Failed"), False], + "COMPLETE": [N_("Signed"), False], + "CONFIRMED": [N_("Confirmed"), False], + "ERROR": [N_("Error"), False], + "EXPIRED": [N_("Expired"), False], + "EXPORTED": [N_("Exported"), False], + "IMPORTED": [N_("Imported"), False], + "INVALIDATED": [N_("Invalidated"), False], + "MEMPOOL": [N_("Mempool"), False], + "PUSH_FAIL": [N_("Push failed"), False], + "PUSHED": [N_("Pushed"), False], + "PARTIALLY_SIGNED": [N_("Partially Signed"), False], + "REPLACED": [N_("Replaced"), False], + "RESTORED": [N_("Restored"), False], + "UPDATED": [N_("Updated"), False], + "VALID": [N_("Valid"), True], } + # Labels found in histories saved by older versions but no longer + # written: "New" opened every history (e.g. tests/samanta7). + # format_status_history() still translates them. + LEGACY_STATUS_LABELS = (N_("New"),) + def set_status(self, status, value=True): """Set a status flag and apply the related side effects. @@ -1341,7 +1378,11 @@ class WillItem(Logger): if self.STATUS[status][1] == bool(value): return None - self.status += "." + (("NOT " if not value else "") + _(self.STATUS[status][0])) + # The history is saved in the wallet, in exported files and in QR + # transfers, so it is always written in English: the saved data must + # not depend on the GUI language (older versions wrote the translated + # label). format_status_history() translates it when it is shown. + self.status += "." + (("NOT " if not value else "") + self.STATUS[status][0]) self.STATUS[status][1] = bool(value) if value: # NOTE: ANTICIPATED and UPDATED are intentionally NOT in this list, diff --git a/bal/core/willexecutors.py b/bal/core/willexecutors.py index eaea558..54bbec0 100644 --- a/bal/core/willexecutors.py +++ b/bal/core/willexecutors.py @@ -22,10 +22,10 @@ from typing import Any from aiohttp import ClientResponse from electrum import bitcoin, constants from electrum.bitcoin import COIN, TOTAL_COIN_SUPPLY_LIMIT_IN_BTC -from electrum.i18n import _ from electrum.logging import get_logger from electrum.network import Network +from ..i18n import _ from .plugin_base import BalPlugin, get_version # Per-request timeout (seconds) for interactive operations (ping / info / diff --git a/bal/gui/qt/common.py b/bal/gui/qt/common.py index b4e9896..e7d3e66 100644 --- a/bal/gui/qt/common.py +++ b/bal/gui/qt/common.py @@ -52,7 +52,6 @@ from electrum.gui.qt.util import ( read_QPixmap_from_bytes, webopen, ) -from electrum.i18n import _ from electrum.logging import get_logger from electrum.network import BestEffortRequestFailed, Network, TxBroadcastError from electrum.payment_identifier import PaymentIdentifier @@ -137,6 +136,7 @@ from ...core.will import ( WillExpiredException, WillItem, WillPostponedException, + format_status_history, ) from ...core.willexecutors import ( # noqa: F401 Willexecutors, @@ -144,6 +144,9 @@ from ...core.willexecutors import ( # noqa: F401 is_tor_active, ) +# BAL's translator: Electrum's catalog first, then BAL's (see bal/i18n.py). +from ...i18n import N_, _ + # --- Presentation helpers --- from .theme import ( server_status_text, @@ -161,6 +164,17 @@ from .window_utils import ( _logger = get_logger(__name__) +# Labels of bal.core.qrtransfer.CHUNK_PRESETS, marked for translation here +# because qrtransfer.py must stay free of Electrum imports (the Android reader +# ships a copy of it). The combos show them with _(label); +# tests/test_i18n.py checks that this list matches CHUNK_PRESETS. +QR_PRESET_LABELS = ( + N_("Small - ~150 bytes/QR (low-res cameras)"), + N_("Medium - ~400 bytes/QR"), + N_("Large - ~900 bytes/QR"), + N_("XL - ~1800 bytes/QR (high-res cameras)"), +) + class shown_cv: # noqa: N801 (intentionally lowercase: mirrors Qt signal naming) _type = bool @@ -178,13 +192,30 @@ class shown_cv: # noqa: N801 (intentionally lowercase: mirrors Qt signal namin def add_widget(grid, label, widget, row, help_): - grid.addWidget(QLabel(_(label)), row, 0) + """Add a ``label | widget | help button`` row to ``grid``. + + ``label`` and ``help_`` must already be translated by the caller: a + variable passed to _() cannot be extracted into the catalog. + """ + grid.addWidget(QLabel(label), row, 0) grid.addWidget(widget, row, 1) grid.addWidget(HelpButton(help_), row, 2) +def translated_headers(headers): + """Return a translated copy of a list's ``headers`` class attribute. + + The column headers are class attributes marked with N_(): a class body + runs at import time, before the catalog is loaded, so they are + translated here, each time the headers are (re)built. + """ + return {column: _(text) for column, text in headers.items()} + + + + def log_error(exec_info, window=None): """Log an error and optionally show it. @@ -222,7 +253,7 @@ def export_meta_gui(electrum_window, title, exporter): filter_ = "All files (*)" filename = getSaveFileName( parent=electrum_window, - title=_("Select file to save your {}".format(title)), + title=_("Select file to save your {}").format(title), filename="BALplugin_{}_{}_{}".format( BalPlugin.chainname, str(electrum_window.wallet), title ), @@ -237,7 +268,7 @@ def export_meta_gui(electrum_window, title, exporter): electrum_window.show_critical(str(e)) else: electrum_window.show_message( - _("Your {0} were exported to '{1}'".format(title, str(filename))) + _("Your {0} were exported to '{1}'").format(title, str(filename)) ) diff --git a/bal/gui/qt/dialogs.py b/bal/gui/qt/dialogs.py index 13f40cb..99d4f26 100644 --- a/bal/gui/qt/dialogs.py +++ b/bal/gui/qt/dialogs.py @@ -62,6 +62,7 @@ from .calendar import BalCalendarButton from .common import ( HEIR_DUST_AMOUNT, HEIR_REAL_AMOUNT, + N_, AmountException, Any, BalTimestamp, @@ -322,7 +323,9 @@ class BalWizardWidget(QWidget): self._bal_parent = parent self.on_next = on_next self.on_cancel = on_cancel - self.titleLabel = QLabel(self.title) + # title/message are class attributes marked with N_(): translate them + # here, when shown (a class body runs before the catalog is loaded). + self.titleLabel = QLabel(_(self.title) if self.title else "") self.vbox.addWidget(self.titleLabel) self.messageLabel = QLabel(_(self.message)) self.vbox.addWidget(self.messageLabel) @@ -377,8 +380,8 @@ class BalWizardWidget(QWidget): class BalWizardHeirsWidget(BalWizardWidget): - title = "Bitcoin After Life Heirs" - message = ( + title = N_("Bitcoin After Life Heirs") + message = N_( "Please add your heirs\n remember that 100% of wallet balance will be spent" ) @@ -416,18 +419,18 @@ class BalWizardHeirsWidget(BalWizardWidget): class BalWizardWEDownloadWidget(BalWizardWidget): - title = _("Bitcoin After Life Will-Executors") - message = _("Choose willexecutors download method") + title = N_("Bitcoin After Life Will-Executors") + message = N_("Choose willexecutors download method") def get_content(self): # question = QLabel() self.combo = QComboBox() self.combo.addItems( [ - "Automatically download and select willexecutors", - "Only download willexecutors list", - "Import willexecutor list from file", - "Manual", + _("Automatically download and select willexecutors"), + _("Only download willexecutors list"), + _("Import willexecutor list from file"), + _("Manual"), ] ) # heir_name.setFixedWidth(32 * char_width_in_lineedit()) @@ -524,8 +527,8 @@ class BalWizardWEDownloadWidget(BalWizardWidget): class BalWizardWEWidget(BalWizardWidget): - title = "Bitcoin After Life Will-Executors" - message = _("Configure and select your willexecutors") + title = N_("Bitcoin After Life Will-Executors") + message = N_("Configure and select your willexecutors") def get_content(self): # Lazy import to avoid a dialogs<->lists import cycle. @@ -544,8 +547,8 @@ class BalWizardWEWidget(BalWizardWidget): class BalWizardLocktimeAndFeeWidget(BalWizardWidget): - title = "Bitcoin After Life Will Settings" - message = _("") + title = N_("Bitcoin After Life Will Settings") + message = "" def get_content(self): widget = QWidget() @@ -748,7 +751,7 @@ class BalBuildWillDialog(BalDialog): return txs = None _logger.debug("close plugin phase 1 started") - varrow = self.msg_set_status("Checking variables") + varrow = self.msg_set_status(_("Checking variables")) try: self.bal_window.init_class_variables() except CheckAliveError as cae: @@ -766,9 +769,9 @@ class BalBuildWillDialog(BalDialog): "during phase1 CAE: {}, Continue to invalidate".format(cae) ) self.msg_set_status( - "Checking variables", varrow, - "Check Alive Threshold Passed: you have to Invalidate " - "your old Will", + _("Checking variables"), varrow, + _("Check Alive Threshold Passed: you have to Invalidate " + "your old Will"), self.COLOR_ERROR, ) else: @@ -776,7 +779,7 @@ class BalBuildWillDialog(BalDialog): return None, tx except NoHeirsException: self.msg_set_status( - "Checking variables", varrow, self.msg_alert("No Heirs") + _("Checking variables"), varrow, self.msg_alert(_("No Heirs")) ) return "no_heirs", None except Exception as e: @@ -799,18 +802,20 @@ class BalBuildWillDialog(BalDialog): max_fee=self.bal_window.bal_plugin.MAX_WILLEXECUTOR_FEE.get(), ) _logger.debug("variables ok") - self.msg_set_status("Checking variables", varrow, "Ok", self.COLOR_OK) + self.msg_set_status(_("Checking variables"), varrow, _("Ok"), self.COLOR_OK) except AmountException: self.msg_set_checking( self.msg_warning( - "In the inheritance process, " - + "the entire wallet will always be fully emptied. \n" - + "Your settings require an adjustment of the amounts" + _( + "In the inheritance process, " + "the entire wallet will always be fully emptied. \n" + "Your settings require an adjustment of the amounts" + ) ) ) except WillExecutorFeeTooHighException as e: self.msg_set_checking( - self.msg_warning(f"Will-executor fee too high: {e}") + self.msg_warning(_("Will-executor fee too high: {}").format(e)) ) self.msg_set_checking() @@ -833,7 +838,7 @@ class BalBuildWillDialog(BalDialog): # makes the CHECK button and the WIZARD behave identically and fixes # the missing-label bug (#03). _logger.debug("expired") - self.msg_set_checking("Expired") + self.msg_set_checking(_("Expired")) return "invalidate_classic", None except WillPostponedException as e: # An already signed/sent will is being postponed. Like an expired @@ -856,7 +861,7 @@ class BalBuildWillDialog(BalDialog): ) except NoHeirsException: _logger.debug("no heirs") - self.msg_set_checking("No Heirs") + self.msg_set_checking(_("No Heirs")) except NotCompleteWillException as e: _logger.debug(f"not complete {e} true") message = False @@ -1051,9 +1056,11 @@ class BalBuildWillDialog(BalDialog): dust_heirs.setdefault(hid, heir[HEIR_DUST_AMOUNT]) for hid, dust_amount in dust_heirs.items(): self.msg_set_status( - f"{_('Heir')} {hid}", + _("Heir {}").format(hid), None, - f"{dust_amount} is DUST - excluded (amount below dust limit)", + _("{} is DUST - excluded (amount below dust limit)").format( + dust_amount + ), self.COLOR_WARNING, ) @@ -1223,14 +1230,14 @@ class BalBuildWillDialog(BalDialog): for i in range(secs, 0, -1): if self._stopping: return - wait_row = self.msg_edit_row(_(f"Please wait {i}secs"), wait_row) + wait_row = self.msg_edit_row(_("Please wait {}secs").format(i), wait_row) time.sleep(1) self.msg_del_row(wait_row) def loop_broadcast_invalidating(self, tx): if self._stopping: return - self.msg_set_invalidating("Broadcasting") + self.msg_set_invalidating(_("Broadcasting")) try: tx.add_info_from_wallet(self.bal_window.wallet) self.network.run_from_another_thread(tx.add_info_from_network(self.network)) @@ -1341,7 +1348,7 @@ class BalBuildWillDialog(BalDialog): done["count"] += 1 # Show the per-server result (Ok/Ko) in bold + color so the # outcome stands out, keeping the server URL in normal weight. - result = self.msg_ok("Ok") if ok else self.msg_error("Ko") + result = self.msg_ok(_("Ok")) if ok else self.msg_error(_("Ko")) self.msg_edit_row("{} : {}".format(url, result)) self.msg_set_pushing(_status_line()) @@ -1393,8 +1400,11 @@ class BalBuildWillDialog(BalDialog): if self._stopping: return row = self.msg_edit_row( - "checking {} - {} : {}".format( - self.bal_window.willitems[wid].we["url"], wid, "Waiting" + "{} : {}".format( + _("checking {} - {}").format( + self.bal_window.willitems[wid].we["url"], wid + ), + _("Waiting"), ) ) w = self.bal_window.willitems[wid] @@ -1407,9 +1417,10 @@ class BalBuildWillDialog(BalDialog): checked = self.bal_window.willitems[wid].get_status("CHECKED") result = self.msg_ok(checked) if checked else self.msg_error(checked) row = self.msg_edit_row( - "checked {} - {} : {}".format( - self.bal_window.willitems[wid].we["url"], - wid, + "{} : {}".format( + _("checked {} - {}").format( + self.bal_window.willitems[wid].we["url"], wid + ), result, ), row, @@ -1457,7 +1468,7 @@ class BalBuildWillDialog(BalDialog): raise Exception("not tx") except Exception as e: (f"exception:{e}") - self.msg_set_invalidating(f"Error: {e}") + self.msg_set_invalidating(_("Error: {}").format(e)) raise Exception("Impossible to sign") from e def on_success_invalidate(self, success): @@ -1825,8 +1836,11 @@ class BalBuildWillDialog(BalDialog): basic_mode = self.bal_window.bal_plugin.is_basic_mode() if basic_mode: - raw_description = self.bal_window.bal_plugin.EVENT_DESCRIPTION.default - raw_summary = self.bal_window.bal_plugin.EVENT_SUMMARY.default + # Factory defaults, 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: @@ -1863,6 +1877,7 @@ class BalBuildWillDialog(BalDialog): 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}") @@ -2006,7 +2021,7 @@ class BalBuildWillDialog(BalDialog): def on_error_phase1(self, error): self.bal_window.update_all() a, b, c = error - self.msg_edit_row(self.msg_error(f"Error: {b}")) + self.msg_edit_row(self.msg_error(_("Error: {}").format(b))) import traceback _logger.error(f"error phase1: {b}\n{''.join(traceback.format_exception(a, b, c))}") button=QPushButton(_("Close")) @@ -2017,7 +2032,7 @@ class BalBuildWillDialog(BalDialog): def on_error_phase2(self, error): self.bal_window.upade_all() a, b, c = error - self.msg_edit_row(self.msg_error(f"Error: {b}")) + self.msg_edit_row(self.msg_error(_("Error: {}").format(b))) _logger.error(f"error phase2: {b}") def _executed_inheritance_status(self): @@ -2203,9 +2218,9 @@ class BalBuildWillDialog(BalDialog): } if reason in messages: - return messages[reason] + "\n\n" + _("Skipped") + return "{}\n\n{}".format(messages[reason], _("Skipped")) - return ( + return "{}\n\n{}".format( _( "Could not build the will, and the exact cause could not be " "determined. Please check that:\n" @@ -2213,12 +2228,14 @@ class BalBuildWillDialog(BalDialog): "- each heir's share is above the minimum (dust limit),\n" "- the Check Alive date is EARLIER than the delivery date,\n" "- at least one will-executor is selected and reachable." - ) - + "\n\n" - + _("Skipped") + ), + _("Skipped"), ) - def msg_set_checking(self, status="Waiting", row=None): + def msg_set_checking(self, status=None, row=None): + # The default is resolved here, not in the signature: a default + # argument is evaluated at import time, before the catalog is loaded. + status = _("Waiting") if status is None else status row = self.check_row if row is None else row self.check_row = self.msg_set_status(_("Checking your will"), row, status) @@ -2231,30 +2248,36 @@ class BalBuildWillDialog(BalDialog): def msg_set_building(self, status=None, row=None,color=None): row = self.build_row if row is None else row self.build_row = self.msg_set_status( - "Building your will", self.build_row, status, color + _("Building your will"), self.build_row, status, color ) def msg_set_signing(self, status=None, row=None): row = self.sign_row if row is None else row - self.sign_row = self.msg_set_status("Signing your will", self.sign_row, status) + self.sign_row = self.msg_set_status( + _("Signing your will"), self.sign_row, status + ) def msg_set_pushing(self, status=None, row=None): row = self.push_row if row is None else row self.push_row = self.msg_set_status( - "Broadcasting your will to executors", self.push_row, status + _("Broadcasting your will to executors"), self.push_row, status ) def msg_set_waiting(self, status=None, row=None): row = self.wait_row if row is None else row - self.wait_row = self.msg_edit_row(f"Please wait {status}secs", self.wait_row) + self.wait_row = self.msg_edit_row( + _("Please wait {}secs").format(status), self.wait_row + ) def msg_error(self, e): # Results are shown in bold so the outcome stands out from the # left-side state label (which stays in normal weight). return "{}".format(self.COLOR_ERROR, e) - def msg_ok(self, e="Ok"): - # Results are shown in bold (see msg_error). + def msg_ok(self, e=None): + # Results are shown in bold (see msg_error). The default "Ok" is + # translated here, not in the signature (evaluated at import time). + e = _("Ok") if e is None else e return "{}".format(self.COLOR_OK, e) def msg_warning(self, e): @@ -2283,12 +2306,14 @@ class BalBuildWillDialog(BalDialog): # glance. ``status`` may already contain rich-text emitted by # msg_ok/msg_error/msg_warning (which add their own ...); wrapping # it again in is harmless for those cases. - status = "Wait" if status is None else status + # ``msg`` must already be translated by the caller: a variable + # passed to _() cannot be extracted into the catalog. + status = _("Wait") if status is None else status if color is None: - line = "{}:\t{}".format(_(msg), status) + line = "{}:\t{}".format(msg, status) else: line = "{}:\t{}".format( - _(msg), color, status + msg, color, status ) return self.msg_edit_row(line, row) @@ -2416,15 +2441,15 @@ class WillDetailDialog(BalDialog): self.paint_scroll_area() self.vlayout.addWidget( - QLabel(_("Expiration date: ") + str(BalTimestamp(self.threshold))) + QLabel(_("Expiration date: {}").format(BalTimestamp(self.threshold))) ) self.vlayout.addWidget(self.scrollbox) w = QWidget() hlayout = QHBoxLayout(w) hlayout.addWidget( - QLabel(_("Valid Txs:") + str(len(Will.only_valid_list(self.will)))) + QLabel(_("Valid Txs:{}").format(len(Will.only_valid_list(self.will)))) ) - hlayout.addWidget(QLabel(_("Total Txs:") + str(len(self.will)))) + hlayout.addWidget(QLabel(_("Total Txs:{}").format(len(self.will)))) self.vlayout.addWidget(w) self.setLayout(self.vlayout) @@ -2477,18 +2502,19 @@ class WillDetailDialog(BalDialog): def toggle_replaced(self): self.bal_window.bal_plugin.hide_replaced() - toggle = _("Hide") + # Whole sentences, so a translation can change the word order. + text = _("Hide replaced") if self.bal_window.bal_plugin._hide_replaced: - toggle = _("Unhide") - self.toggle_replace_button.setText(f"{toggle} {_('replaced')}") + text = _("Unhide replaced") + self.toggle_replace_button.setText(text) self.update() def toggle_invalidated(self): self.bal_window.bal_plugin.hide_invalidated() - toggle = _("Hide") + text = _("Hide invalidated") if self.bal_window.bal_plugin._hide_invalidated: - toggle = _("Unhide") - self.toggle_invalidate_button.setText(_(f"{toggle} {_('invalidated')}")) + text = _("Unhide invalidated") + self.toggle_invalidate_button.setText(text) self.update() def update(self): @@ -2768,7 +2794,7 @@ class BalQrExportWidget(QWidget): size_row = QHBoxLayout() size_row.addWidget(QLabel(_("QR code size:"))) self.size_combo = QComboBox() - self.size_combo.addItems([label for label, _budget in CHUNK_PRESETS]) + self.size_combo.addItems([_(label) for label, _budget in CHUNK_PRESETS]) self.size_combo.setCurrentIndex( preset_index_for_chunk_size(self.chunk_size) ) @@ -2783,7 +2809,8 @@ class BalQrExportWidget(QWidget): self._format_options = ["balqr"] + list(ANIMATED_QR_FORMATS) self.format_combo.addItems( [ - format_name(fmt) + ((" (default)") if fmt == "balqr" else "") + format_name(fmt) + + (" {}".format(_("(default)")) if fmt == "balqr" else "") for fmt in self._format_options ] ) diff --git a/bal/gui/qt/lists.py b/bal/gui/qt/lists.py index bb7ece3..9f15edb 100644 --- a/bal/gui/qt/lists.py +++ b/bal/gui/qt/lists.py @@ -20,6 +20,7 @@ from PyQt6.QtWidgets import QLineEdit as _QLineEdit from PyQt6.QtWidgets import QMessageBox, QStyledItemDelegate from .common import ( + N_, OP_RETURN_PREFIX, BalTimestamp, Buttons, @@ -59,6 +60,7 @@ from .common import ( datetime, enum, export_meta_gui, + format_status_history, getOpenFileName, import_meta_gui, is_op_return_address, @@ -69,6 +71,7 @@ from .common import ( server_status_tooltip, signature_suffix, status_color, + translated_headers, tx_from_any, write_json_file, ) @@ -128,9 +131,9 @@ class HeirListWidget(MyTreeView, MessageBoxMixin): AMOUNT = enum.auto() headers = { - Columns.NAME: _("Name"), - Columns.ADDRESS: _("Address"), - Columns.AMOUNT: _("Amount"), + Columns.NAME: N_("Name"), + Columns.ADDRESS: N_("Address"), + Columns.AMOUNT: N_("Amount"), } filter_columns = [Columns.NAME, Columns.ADDRESS] @@ -245,7 +248,7 @@ class HeirListWidget(MyTreeView, MessageBoxMixin): col=self.Columns.NAME, role=self.ROLE_HEIR_KEY ) self.model().clear() - self.update_headers(self.__class__.headers) + self.update_headers(translated_headers(self.__class__.headers)) set_current = None for key in sorted(self.bal_window.heirs.keys()): heir = self.bal_window.heirs[key] @@ -337,11 +340,11 @@ class PreviewList(MyTreeView, MessageBoxMixin): SERVER = enum.auto() headers = { - Columns.LOCKTIME: _("Locktime"), - Columns.TXID: _("Txid"), - Columns.WILLEXECUTOR: _("Will-Executor"), - Columns.STATUS: _("Status"), - Columns.SERVER: _("Server"), + Columns.LOCKTIME: N_("Locktime"), + Columns.TXID: N_("Txid"), + Columns.WILLEXECUTOR: N_("Will-Executor"), + Columns.STATUS: N_("Status"), + Columns.SERVER: N_("Server"), } ROLE_HEIR_KEY = Qt.ItemDataRole.UserRole + 2000 @@ -586,7 +589,7 @@ class PreviewList(MyTreeView, MessageBoxMixin): if bal_tx.we: we = bal_tx.we["url"] labels[self.Columns.WILLEXECUTOR] = we - status = bal_tx.status + signature_suffix(bal_tx) + status = format_status_history(bal_tx.status) + signature_suffix(bal_tx) if len(status) > 53: status = "...{}".format(status[-50:]) labels[self.Columns.STATUS] = status @@ -644,7 +647,7 @@ class PreviewList(MyTreeView, MessageBoxMixin): col=self.Columns.TXID, role=self.ROLE_HEIR_KEY ) self.model().clear() - self.update_headers(self.__class__.headers) + self.update_headers(translated_headers(self.__class__.headers)) set_current = None for txid, bal_tx in self.will.items(): @@ -676,7 +679,7 @@ class PreviewList(MyTreeView, MessageBoxMixin): # The Wizard is the main entry point to create an inheritance, so make # it stand out: show a bold label next to a slightly larger icon (the # plain icon-only button was too easy to overlook). - wizard = QPushButton(" " + _("Build Your Will")) + wizard = QPushButton(" {}".format(_("Build Your Will"))) wizard.setIcon( read_QIcon_from_bytes( self.bal_window.bal_plugin.read_file("icons/wizard.png") @@ -904,12 +907,12 @@ class WillExecutorListWidget(MyTreeView): ADDRESS = enum.auto() headers = { - Columns.SELECTED: _(""), - Columns.URL: _("Url"), - Columns.STATUS: _("S"), - Columns.BASE_FEE: _("Base fee"), - Columns.INFO: _("Info"), - Columns.ADDRESS: _("Default Address"), + Columns.SELECTED: "", + Columns.URL: N_("Url"), + Columns.STATUS: N_("S"), + Columns.BASE_FEE: N_("Base fee"), + Columns.INFO: N_("Info"), + Columns.ADDRESS: N_("Default Address"), } filter_columns = [Columns.URL] @@ -1091,7 +1094,7 @@ class WillExecutorListWidget(MyTreeView): col=self.Columns.URL, role=self.ROLE_HEIR_KEY ) self.model().clear() - self.update_headers(self.__class__.headers) + self.update_headers(translated_headers(self.__class__.headers)) set_current = None @@ -1236,13 +1239,14 @@ class WillExecutorWidget(QWidget, MessageBoxMixin): buttonbox.addWidget(b) def _menu_button(label): + # ``label`` must already be translated by the caller. btn = QToolButton() - btn.setText(_(label)) + btn.setText(label) btn.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup) buttonbox.addWidget(btn) return btn - export_btn = _menu_button("Export") + export_btn = _menu_button(_("Export")) export_menu = QMenu(export_btn) export_menu.addAction(_("Export all"), lambda: self.export_file()) export_menu.addAction( @@ -1253,7 +1257,7 @@ class WillExecutorWidget(QWidget, MessageBoxMixin): ) export_btn.setMenu(export_menu) - ping_btn = _menu_button("Ping All") + ping_btn = _menu_button(_("Ping All")) ping_menu = QMenu(ping_btn) ping_menu.addAction(_("Ping all"), lambda: self.update_willexecutors()) ping_menu.addAction( @@ -1261,7 +1265,7 @@ class WillExecutorWidget(QWidget, MessageBoxMixin): ) ping_btn.setMenu(ping_menu) - select_btn = _menu_button("Select All") + select_btn = _menu_button(_("Select All")) select_menu = QMenu(select_btn) select_menu.addAction(_("Select all"), lambda: self.set_select_all(True)) select_menu.addAction( @@ -1295,7 +1299,7 @@ class WillExecutorWidget(QWidget, MessageBoxMixin): url_edit = QLineEdit() url_edit.setFixedWidth(32 * char_width_in_lineedit()) - info_edit = QLineEdit("New Will Executor") + info_edit = QLineEdit(_("New Will Executor")) info_edit.setFixedWidth(32 * char_width_in_lineedit()) base_fee_spin = QSpinBox() base_fee_spin.setRange(0, 1000000) @@ -1309,7 +1313,7 @@ class WillExecutorWidget(QWidget, MessageBoxMixin): if executor: url_edit.setText(edit_key) - info_edit.setText(str(executor.get("info", "New Will Executor"))) + info_edit.setText(str(executor.get("info", _("New Will Executor")))) base_fee_spin.setValue(int(executor.get("base_fee", 0))) address_edit.setText(str(executor.get("address", ""))) @@ -1469,7 +1473,7 @@ class WillExecutorWidget(QWidget, MessageBoxMixin): break self._add_another = False url_edit.clear() - info_edit.setText("New Will Executor") + info_edit.setText(_("New Will Executor")) base_fee_spin.setValue(0) address_edit.clear() diff --git a/bal/gui/qt/plugin.py b/bal/gui/qt/plugin.py index 6c16ada..da234f2 100644 --- a/bal/gui/qt/plugin.py +++ b/bal/gui/qt/plugin.py @@ -20,6 +20,7 @@ from electrum.util import EventListener, event_listener from PyQt6.QtWidgets import QLayout from ...core.qrtransfer import CHUNK_PRESETS, preset_index_for_chunk_size +from ...i18n import init_from_config from .common import ( BalPlugin, Buttons, @@ -67,6 +68,11 @@ class Plugin(BalPlugin, EventListener): def __init__(self, parent, config, name): _logger.info("INIT BALPLUGIN") BalPlugin.__init__(self, parent, config, name) + # Load BAL's catalog for the language Electrum's GUI is using, before + # any window or dialog builds its texts. It must come after + # BalPlugin.__init__: read_file() needs the plugin's parent and name. + # The CLI plugin does not call this, so the CLI stays in English. + init_from_config(self, config) self.bal_windows = {} # Status-bar buttons, keyed by id(sb.window()). Tracking them lets us # remove a stale button before creating a fresh one when a wallet is @@ -298,7 +304,7 @@ class Plugin(BalPlugin, EventListener): pass b = StatusBarButton( read_QIcon_from_bytes(self.read_file("icons/bal32x32.png")), - "Bal " + _("Bitcoin After Life"), + "Bal {}".format(_("Bitcoin After Life")), lambda: self.settings_dialog(sb.window()), sb.height(), ) @@ -442,7 +448,7 @@ class Plugin(BalPlugin, EventListener): def settings_dialog(self, window=None, wallet=None): - d = BalDialog(window, self, self.get_window_title("Settings")) + d = BalDialog(window, self, self.get_window_title(_("Settings"))) d.setMinimumSize(100, 200) qicon = read_QPixmap_from_bytes(self.read_file("icons/bal16x16.png")) lbl_logo = QLabel() @@ -537,7 +543,8 @@ class Plugin(BalPlugin, EventListener): # Ordered low -> high so the user picks the resolution matching their # camera. Visible to all users (BASIC and ADVANCED). qr_size_combo = QComboBox() - qr_size_combo.addItems([label for label, _budget in CHUNK_PRESETS]) + # Labels marked for translation in common.QR_PRESET_LABELS. + qr_size_combo.addItems([_(label) for label, _budget in CHUNK_PRESETS]) qr_size_combo.setCurrentIndex( preset_index_for_chunk_size(int(self.QR_CHUNK_SIZE.get())) ) @@ -656,9 +663,9 @@ class Plugin(BalPlugin, EventListener): elif kind == "spin": widget.setValue(int(cfg.default)) elif kind == "line": - widget.setText(cfg.default) + widget.setText(cfg.localized_default()) elif kind == "text": - widget.setPlainText(cfg.default) + widget.setPlainText(cfg.localized_default()) elif kind == "user_type": widget.setCurrentIndex( 1 if str(cfg.default).lower() == "advanced" else 0 @@ -670,7 +677,7 @@ class Plugin(BalPlugin, EventListener): btn.clicked.connect(reset) return btn - heir_repush = QPushButton("Rebroadcast transactions") + heir_repush = QPushButton(_("Rebroadcast transactions")) heir_repush.clicked.connect(partial(self.broadcast_transactions, True)) bal_mode = QComboBox() options = ["Easy", "Advanced", "Experimental"] @@ -696,27 +703,27 @@ class Plugin(BalPlugin, EventListener): # advanced-only settings, so the user picks basic/advanced first. add_widget( grid, - "Hide Replaced", + _("Hide Replaced"), heir_hide_replaced, 0, - "Hide replaced transactions from will detail and list", + _("Hide replaced transactions from will detail and list"), ) grid.addWidget(_make_reset_btn(self.HIDE_REPLACED, heir_hide_replaced, "check"), 0, 3) add_widget( grid, - "Hide Invalidated", + _("Hide Invalidated"), heir_hide_invalidated, 1, - "Hide invalidated transactions from will detail and list", + _("Hide invalidated transactions from will detail and list"), ) grid.addWidget(_make_reset_btn(self.HIDE_INVALIDATED, heir_hide_invalidated, "check"), 1, 3) lbl_auto_sign = QLabel(_("Auto-sign on Check")) - help_auto_sign = HelpButton( + help_auto_sign = HelpButton(_( "When checking, automatically sign and broadcast the will " "transactions to their will-executors.\n" "The wallet password is requested only if the wallet is " "encrypted." - ) + )) grid.addWidget(_hide_if_basic(lbl_auto_sign), 2, 0) grid.addWidget(_hide_if_basic(heir_auto_sign), 2, 1) grid.addWidget(_hide_if_basic(help_auto_sign), 2, 2) @@ -726,10 +733,10 @@ class Plugin(BalPlugin, EventListener): grid.addWidget(_hide_if_basic(reset_btn_auto_sign), 2, 3) add_widget( grid, - "Panel editable Date and Fee", + _("Panel editable Date and Fee"), heir_editable_dates, 3, - ( + _( "When enabled, the delivery-time and check-alive date fields " "can be edited everywhere (toolbar / Heirs tab), not only in " "the will-building wizard.\n" @@ -741,10 +748,10 @@ class Plugin(BalPlugin, EventListener): # will-executor. Visible to all users (BASIC and ADVANCED). add_widget( grid, - "Max Will-Executor Fee (satoshi)", + _("Max Will-Executor Fee (satoshi)"), heir_max_willexecutor_fee, 4, - ( + _( "Maximum fee (in satoshi) allowed to be paid to a single " "will-executor. If a will-executor charges more than this, " "the will will not be built.\n" @@ -756,10 +763,10 @@ class Plugin(BalPlugin, EventListener): # user chooses basic/advanced first, then sees the relevant options. add_widget( grid, - "User Type", + _("User Type"), user_type_combo, 5, - ( + _( "Choose how much detail the plugin shows.\n\n" "BASIC: simplified interface, safe configuration for most " "users.\n\n" @@ -774,10 +781,10 @@ class Plugin(BalPlugin, EventListener): # only in ADVANCED mode. In BASIC mode the factory defaults are always # used and these settings are hidden. lbl_num_reminders = QLabel(_("Number of reminders")) - help_num_reminders = HelpButton( + help_num_reminders = HelpButton(_( "How many reminder alarms the exported calendar (.ics) event " "contains. Range: 1 to 5 (default 3). Only used in ADVANCED mode." - ) + )) grid.addWidget(_hide_if_basic(lbl_num_reminders), 6, 0) grid.addWidget(_hide_if_basic(heir_num_reminders), 6, 1) grid.addWidget(_hide_if_basic(help_num_reminders), 6, 2) @@ -785,13 +792,13 @@ class Plugin(BalPlugin, EventListener): grid.addWidget(_hide_if_basic(reset_btn_6), 6, 3) lbl_event_summary = QLabel(_("Event summary")) - help_event_summary = HelpButton( + help_event_summary = HelpButton(_( "Default message to be used in event summary\n" "Variables:\n" " $wallet_name: name of wallet\n" " $heirs_complete: list of heirs name,address,amount\n" "Only used in ADVANCED mode." - ) + )) grid.addWidget(_hide_if_basic(lbl_event_summary), 7, 0) grid.addWidget(_hide_if_basic(edit_event_summary), 7, 1) grid.addWidget(_hide_if_basic(help_event_summary), 7, 2) @@ -799,13 +806,13 @@ class Plugin(BalPlugin, EventListener): grid.addWidget(_hide_if_basic(reset_btn_7), 7, 3) lbl_event_description = QLabel(_("Event description")) - help_event_description = HelpButton( + help_event_description = HelpButton(_( "Default message to be used in event description\n" "Variables:\n" " $wallet_name: name of wallet\n" " $heirs_complete: list of heirs name,address,amount\n" "Only used in ADVANCED mode." - ) + )) grid.addWidget(_hide_if_basic(lbl_event_description), 8, 0) grid.addWidget(_hide_if_basic(edit_event_description), 8, 1) grid.addWidget(_hide_if_basic(help_event_description), 8, 2) @@ -814,10 +821,10 @@ class Plugin(BalPlugin, EventListener): # Welist server URL: shown only in ADVANCED mode. In BASIC mode the # factory default is always used and the setting is hidden. lbl_welist_server = QLabel(_("Welist Server URL")) - help_welist_server = HelpButton( + help_welist_server = HelpButton(_( "URL of the server that provides the will-executor list. " "Only available in ADVANCED mode." - ) + )) grid.addWidget(_hide_if_basic(lbl_welist_server), 9, 0) grid.addWidget(_hide_if_basic(edit_welist_server), 9, 1) grid.addWidget(_hide_if_basic(help_welist_server), 9, 2) @@ -825,11 +832,11 @@ class Plugin(BalPlugin, EventListener): grid.addWidget(_hide_if_basic(reset_btn_9), 9, 3) lbl_calendar_app = QLabel(_("Calendar app command")) - help_calendar_app = HelpButton( + help_calendar_app = HelpButton(_( "Command used to open .ics calendar files.\n" "Leave empty to use the system default (xdg-open/open/start).\n" "Only used in ADVANCED mode." - ) + )) grid.addWidget(_hide_if_basic(lbl_calendar_app), 10, 0) grid.addWidget(_hide_if_basic(edit_calendar_app), 10, 1) grid.addWidget(_hide_if_basic(help_calendar_app), 10, 2) @@ -840,13 +847,13 @@ class Plugin(BalPlugin, EventListener): # label field is disabled while the checkbox is off (see # on_save_history_change above). lbl_save_history = QLabel(_("Save inheritance transactions in history")) - help_save_history = HelpButton( + help_save_history = HelpButton(_( "After each check, save the valid will transactions into the " "wallet's local history (the History tab), each with a label.\n" "The label may contain the variable:\n" " {willexecutor}: replaced with the will-executor URL of the item\n" "Only used in ADVANCED mode." - ) + )) grid.addWidget(_hide_if_basic(lbl_save_history), 11, 0) grid.addWidget(_hide_if_basic(heir_save_history), 11, 1) grid.addWidget(_hide_if_basic(help_save_history), 11, 2) @@ -854,13 +861,13 @@ class Plugin(BalPlugin, EventListener): grid.addWidget(_hide_if_basic(reset_btn_11), 11, 3) lbl_history_label = QLabel(_("History label")) - help_history_label = HelpButton( + help_history_label = HelpButton(_( "Label applied to the will transactions saved into the wallet's " "local history.\n" "Variables:\n" " {willexecutor}: replaced with the will-executor URL of the item\n" "Only used in ADVANCED mode." - ) + )) grid.addWidget(_hide_if_basic(lbl_history_label), 12, 0) grid.addWidget(_hide_if_basic(edit_history_label), 12, 1) grid.addWidget(_hide_if_basic(help_history_label), 12, 2) @@ -877,7 +884,7 @@ class Plugin(BalPlugin, EventListener): grid.addWidget(heir_repush, 13, 0) grid.addWidget( HelpButton( - "Broadcast all transactions to willexecutors including those already pushed" + _("Broadcast all transactions to willexecutors including those already pushed") ), 13, 2, @@ -887,12 +894,12 @@ class Plugin(BalPlugin, EventListener): # Placed below the rebroadcast button so the existing rows keep their # numbers. lbl_rebuild_on_close = QLabel(_("Rebuild will on wallet close")) - help_rebuild_on_close = HelpButton( + help_rebuild_on_close = HelpButton(_( "Run the 'Build your will' wizard every time the wallet is closed " "or Electrum is quit, so the will is rebuilt and re-validated.\n" "When disabled, the will is only rebuilt when you press Check or " "Prepare. The last built state is still saved to the wallet." - ) + )) grid.addWidget(lbl_rebuild_on_close, 14, 0) grid.addWidget(heir_rebuild_on_close, 14, 1) grid.addWidget(help_rebuild_on_close, 14, 2) @@ -905,7 +912,7 @@ class Plugin(BalPlugin, EventListener): # BASIC + ADVANCED), right below the "Rebuild will on wallet close" # row. lbl_auto_rebuild = QLabel(_("Rebuild automatically on new transactions")) - help_auto_rebuild = HelpButton( + help_auto_rebuild = HelpButton(_( "When a new transaction arrives for the wallet, automatically " "rebuild the will the same way the wizard does at wallet close: " "the delivery date is anticipated by one day so the new will " @@ -916,7 +923,7 @@ class Plugin(BalPlugin, EventListener): "threshold, or when the threshold is already in the past.\n" "When disabled (default), the will is only rebuilt on Check / " "Prepare / wallet close." - ) + )) grid.addWidget(lbl_auto_rebuild, 15, 0) grid.addWidget(heir_auto_rebuild, 15, 1) grid.addWidget(help_auto_rebuild, 15, 2) @@ -929,13 +936,13 @@ class Plugin(BalPlugin, EventListener): # size used when exporting a will via QR codes; changeable per export # inside the export dialog itself. lbl_qr_size = QLabel(_("QR Code Size")) - help_qr_size = HelpButton( + help_qr_size = HelpButton(_( "Payload size of a single QR code when exporting a will via QR.\n\n" "Larger QR codes hold more data (fewer shots) but are easier to " "scan with a high-resolution camera; smaller QR codes scan fine " "even with low-resolution cameras but require more shots.\n" "The same selector is available inside the export dialog." - ) + )) grid.addWidget(lbl_qr_size, 16, 0) grid.addWidget(qr_size_combo, 16, 1) grid.addWidget(help_qr_size, 16, 2) @@ -990,9 +997,9 @@ class Plugin(BalPlugin, EventListener): elif kind == "spin": widget.setValue(int(cfg.default)) elif kind == "line": - widget.setText(cfg.default) + widget.setText(cfg.localized_default()) elif kind == "text": - widget.setPlainText(cfg.default) + widget.setPlainText(cfg.localized_default()) elif kind == "user_type": # Default is "basic" -> combo index 0; "advanced" -> index 1. widget.setCurrentIndex( @@ -1031,7 +1038,7 @@ class Plugin(BalPlugin, EventListener): bottom_row = QHBoxLayout() bottom_row.addWidget(btn_reset) bottom_row.addStretch(1) - bottom_row.addWidget(QLabel("" + _("Support:") + "")) + bottom_row.addWidget(QLabel("{}".format(_("Support:")))) bottom_row.addWidget(lbl_support) # Outer layout: warning (top) -> settings grid -> bottom button row. @@ -1083,6 +1090,11 @@ class Plugin(BalPlugin, EventListener): ) def get_window_title(self, title): - return _("BAL - ") + _(title) + """Return the window title "BAL - ". + + ``title`` must already be translated by the caller: a variable passed + to _() cannot be extracted into the catalog. + """ + return _("BAL - {}").format(title) diff --git a/bal/gui/qt/theme.py b/bal/gui/qt/theme.py index a455811..7ece6cb 100644 --- a/bal/gui/qt/theme.py +++ b/bal/gui/qt/theme.py @@ -87,7 +87,7 @@ def server_status_text(will_item) -> str: the user always knows whether each inheritance transaction is actually stored on the will-executor servers, regardless of the row colour. """ - from electrum.i18n import _ + from ...i18n import _ if will_item.get_status("CHECK_FAIL") and not will_item.get_status("CHECKED"): return _("Not on server") @@ -105,7 +105,7 @@ def server_status_text(will_item) -> str: def server_status_tooltip(will_item) -> str: """Return a detailed tooltip for the "Server" column, including the will-executor URL (if any) and the current server state.""" - from electrum.i18n import _ + from ...i18n import _ url = None we = getattr(will_item, "we", None) diff --git a/bal/gui/qt/widgets.py b/bal/gui/qt/widgets.py index 1c1deb7..8eccef5 100644 --- a/bal/gui/qt/widgets.py +++ b/bal/gui/qt/widgets.py @@ -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 ) diff --git a/bal/gui/qt/window.py b/bal/gui/qt/window.py index 55c3784..0ab2e91 100644 --- a/bal/gui/qt/window.py +++ b/bal/gui/qt/window.py @@ -26,6 +26,7 @@ from ...core.checkalive import ( resolve_guard_threshold, ) from .common import ( + N_, OP_RETURN_PREFIX, AmountException, BalPlugin, @@ -282,12 +283,12 @@ class BalWindow: def new_heir_dialog(self, heir_key=None): heir = self.heirs.get(heir_key) - title = "New heir" + title = _("New heir") if heir: - title = f"Edit: {heir_key}" + title = _("Edit: {}").format(heir_key) d = BalDialog( - self.window, self.bal_plugin, self.bal_plugin.get_window_title(_(title)) + self.window, self.bal_plugin, self.bal_plugin.get_window_title(title) ) vbox = QVBoxLayout(d) @@ -828,12 +829,12 @@ class BalWindow: except AmountException as e: self.show_warning( _( - f"In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts.{e}" - ) + "In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts.{}" + ).format(e) ) except WillExecutorFeeTooHighException as e: self.show_error( - _(f"Will-executor fee too high: {e}") + _("Will-executor fee too high: {}").format(e) ) return except CheckAliveError: @@ -919,27 +920,27 @@ class BalWindow: _logger.info("{}:{}".format(type(e), e)) message = False if isinstance(e, HeirChangeException): - message = "Heirs changed:" + message = _("Heirs changed:") elif isinstance(e, WillExecutorNotPresent): - message = "Will-Executor not present:" + message = _("Will-Executor not present:") elif isinstance(e, WillexecutorChangeException): - message = "Will-Executor changed" + message = _("Will-Executor changed") elif isinstance(e, TxFeesChangedException): - message = "Txfees are changed" + message = _("Txfees are changed") elif isinstance(e, HeirNotFoundException): # Task #01b: replace the misleading "Heir not found" text. # This branch is most often hit because the delivery date # was anticipated, not because an heir is missing, so we use # a clear message that covers both the DATE and the HEIRS # cases (kept consistent with dialogs.py / the CHECK window). - message = ( + message = _( "Found CHANGES to the DATE or the HEIRS,\n" "a NEW WILL must be prepared." ) if message: self.show_message( - f"{_(message)}:\n {e}\n{_('will have to be built')}" + "{}:\n {}\n{}".format(message, e, _("will have to be built")) ) _logger.info("build will") @@ -959,7 +960,7 @@ class BalWindow: self.invalidate_will() except NotCompleteWillException as e: self.show_error( - "Error:{}\n {}".format( + _("Error:{}\n {}").format( str(e), _("Please, check your heirs, locktime and threshold!"), ) @@ -1019,7 +1020,9 @@ class BalWindow: except SerializationError as e: _logger.error("unable to deserialize the transaction") parent.show_critical( - _("Electrum was unable to deserialize the transaction:") + "\n" + str(e) + "{}\n{}".format( + _("Electrum was unable to deserialize the transaction:"), e + ) ) else: # Electrum's own TxDialog: keep it in front of the main window. @@ -1087,8 +1090,8 @@ class BalWindow: def get_message(): msg = "" if signed: - msg = _(f"signed: {signed}\n") - return msg + _(f"signing: {tosign}") + msg = _("signed: {}").format(signed) + "\n" + return msg + _("signing: {}").format(tosign) if txids is not None: targets = [ @@ -1610,7 +1613,7 @@ class BalWindow: willexecutors = Willexecutors.get_willexecutor_transactions(willitems, force=force) def getMsg(willexecutors): - msg = "Broadcasting Transactions to Will-Executors:\n" + msg = _("Broadcasting Transactions to Will-Executors:") + "\n" for url in willexecutors: msg += f"{url}:\t{willexecutors[url]['broadcast_status']}\n" return msg @@ -1664,8 +1667,8 @@ class BalWindow: if self.waiting_dialog._stopping: return self.waiting_dialog.update( - "checking {} - {} : {}".format( - willitems[wid].we["url"], wid, "Waiting" + _("checking {} - {} : {}").format( + willitems[wid].we["url"], wid, _("Waiting") ) ) w = willitems[wid] @@ -1673,7 +1676,7 @@ class BalWindow: Willexecutors.check_transaction(wid, w.we["url"]) ) self.waiting_dialog.update( - "checked {} - {} : {}".format( + _("checked {} - {} : {}").format( willitems[wid].we["url"], wid, willitems[wid].get_status("CHECKED"), @@ -2142,7 +2145,7 @@ class BalWindow: # Simple, user-facing message shown when the download fails for any reason # (the technical cause is in the Electrum log). - DOWNLOAD_FAILED_MESSAGE = ( + DOWNLOAD_FAILED_MESSAGE = N_( "Could not download the will-executors list.\n\n" "This is usually caused by your internet connection or a firewall, " "not by the plugin. Please check your connection (a VPN often helps) " @@ -2152,7 +2155,7 @@ class BalWindow: # Shown when the download fails while Electrum is connected through Tor: # the most common cause is a slow Tor connection, so guide the user # accordingly instead of the generic message above. - DOWNLOAD_FAILED_TOR_MESSAGE = ( + DOWNLOAD_FAILED_TOR_MESSAGE = N_( "Could not download the will-executors list over Tor.\n\n" "Electrum is connected through Tor and the connection is taking too " "long. Your Tor connection may be slow. Please try again, or use a VPN " @@ -2225,27 +2228,27 @@ class BalWindow: if err.url and err.reason == "empty response": # Server reached but returned no data for this chain. self.show_warning(_( - f"No active will-executor servers found for the " - f"{err.chain} network.\n\n" - f"The welist server at {err.url} responded but " - f"returned no will-executors for this chain." - )) + "No active will-executor servers found for the " + "{chain} network.\n\n" + "The welist server at {url} responded but " + "returned no will-executors for this chain." + ).format(chain=err.chain, url=err.url)) elif err.url: # Advanced mode with a non-empty error (network issue). self.show_warning(_( - f"Could not reach the configured welist server.\n\n" - f"Server: {err.url}\n" - f"Error: {err.reason}\n\n" - f"Please verify the welist server URL in the plugin " - f"settings." - )) + "Could not reach the configured welist server.\n\n" + "Server: {url}\n" + "Error: {reason}\n\n" + "Please verify the welist server URL in the plugin " + "settings." + ).format(url=err.url, reason=err.reason)) else: # Basic mode: the server responded but has no data for this # chain. self.show_warning(_( - f"No active will-executor found for the " - f"{err.chain} network." - )) + "No active will-executor found for the " + "{chain} network." + ).format(chain=err.chain)) else: # Tor-aware: a generic failure/timeout while on Tor is most # likely a slow Tor connection. diff --git a/bal/i18n.py b/bal/i18n.py new file mode 100644 index 0000000..5a734f7 --- /dev/null +++ b/bal/i18n.py @@ -0,0 +1,198 @@ +""" +bal.i18n +======== + +Translation layer of the BAL plugin (gettext domain ``bal``). + +Why BAL has its own catalog +--------------------------- +Electrum translates its interface with gettext (domain ``electrum``). Its +translators only see Electrum's own source code, so the texts of an external +(zip) plugin such as BAL are not in Electrum's catalog, and Electrum offers no +way to translate external plugins. BAL therefore ships its own compiled +catalogs inside the plugin: ``locale/<lang>/LC_MESSAGES/bal.mo``. + +Lookup order +------------ +:func:`_` returns the first translation found in: + +1. **Electrum's catalog.** When Electrum already translates the exact same + English text, its translation wins. BAL then reads like Electrum and + follows Electrum's own translation fixes; if Electrum ever ships BAL as an + internal plugin, Electrum's translations take over with no change here + (owner's decision D7 in ``PLAN_I18N.md``). +2. **BAL's catalog**, for the texts that only BAL has. +3. **The English source text.** + +Language +-------- +Electrum picks the GUI language once, at start-up (``run_electrum``), and a +change needs a restart. :func:`init_from_config` repeats Electrum's choice +once, when the Qt plugin is created. BAL never detects the language by +itself: otherwise part of a window could be in one language and part in +another. The CLI never calls it, so the CLI stays in English, like Electrum's. + +Rules for translatable texts +---------------------------- +* Wrap literal English text only: ``_("Sign the will")``. +* Put the variable parts in ``{}`` and fill them *after* translating: + ``_("{} heirs").format(count)``. Never build the text with an f-string or + ``%`` inside ``_()``: it would change at run time and never match a catalog + entry (Ruff rules INT001-INT003 catch this). +* Texts evaluated at import time (class attributes, tables, constants) are + marked with :func:`N_` and passed to ``_()`` when they are displayed. Every + class body runs before :func:`init_from_config`, so a ``_()`` there would + always return English. +* Never translate text that is stored or compared (wallet labels, the saved + status history, config values used to recognise BAL transactions): stored + data must not depend on the UI language. +""" + +import gettext +import io +import string +from typing import Optional + +from electrum.i18n import _ as _electrum_gettext +from electrum.logging import get_logger + +_logger = get_logger(__name__) + +#: gettext domain of the BAL catalogs. +DOMAIN = "bal" + +# BAL catalog of the current language, or None (English, CLI, or no usable +# catalog). Set by set_language(). +_catalog: Optional[gettext.GNUTranslations] = None + +# Source texts whose BAL translation was rejected: the warning is logged once, +# not at every repaint of the widget that shows the text. +_rejected: set = set() + +_formatter = string.Formatter() + + +def keeps_format_fields(msg: str, translation: str) -> bool: + """Tell whether ``translation`` keeps the ``{}`` fields of ``msg``. + + A translation that adds, drops or renames a replacement field would make + the ``.format()`` call that follows raise, or put a value in the wrong + place, so such a translation must not be used. + + The rules are copied from Electrum's + ``_ensure_translation_keeps_format_string_syntax_similar`` + (``electrum/i18n.py``, MIT licence, Copyright (C) The Electrum developers) + rather than importing that private function, which may change without + notice. Keep them identical, so a BAL translation is accepted or rejected + exactly like an Electrum one. The only addition: a malformed source text + returns False here instead of raising. + """ + try: + # Tuples of (literal_text, field_name, format_spec, conversion). + parsed1 = list(_formatter.parse(msg)) + parsed2 = list(_formatter.parse(translation)) + except ValueError: # malformed format string + return False + if len(parsed1) != len(parsed2): + return False + # The set of field names must not change (re-ordering them is allowed). + return {t[1] for t in parsed1} == {t[1] for t in parsed2} + + +def _(msg: str) -> str: + """Return ``msg`` translated into the GUI language. + + See the module docstring for the lookup order. ``_("")`` returns ``""``: + for gettext the empty text is the key of the catalog header. + """ + if msg == "": + return "" + translation = _electrum_gettext(msg) + if translation != msg or _catalog is None: + # Electrum's translation wins (Electrum has already checked its {} + # fields); without a BAL catalog there is nothing else to look up. + return translation + translation = _catalog.gettext(msg) + if translation == msg or keeps_format_fields(msg, translation): + return translation + if msg not in _rejected: + _rejected.add(msg) + _logger.warning( + f"rejected BAL translation, replacement fields differ: " + f"{msg!r} -> {translation!r}" + ) + return msg + + +def N_(msg: str) -> str: # noqa: N802 (the conventional gettext marker name) + """Mark ``msg`` as translatable and return it unchanged. + + Used where a text is defined at import time (class attributes, tables, + constants). Extraction tools (Babel, xgettext) collect ``N_("...")`` like + ``_("...")``; the code passes the value to ``_()`` when it shows it. + """ + return msg + + +def set_language(plugin, lang: Optional[str]) -> None: + """Load the BAL catalog of ``lang`` (for example ``"it_IT"``), or none. + + ``None``, ``""`` and English (``"en_*"``) load no catalog, so the English + source texts are shown, as Electrum does for ``en_*``. + + The catalog is read with ``plugin.read_file()``, the Electrum API BAL uses + for its icons: it works both from the installed zip and from a development + checkout. ``gettext.translation(localedir=...)`` is not used because, + pointed inside a zip, it silently finds nothing. + + If ``locale/<lang>/`` has no catalog, the plain language code is tried + (``locale/it/`` for ``it_IT``). A missing or unreadable catalog is logged + and BAL stays in English: a translation problem must never stop the + plugin. + """ + global _catalog + _catalog = None + _rejected.clear() + if not isinstance(lang, str) or not lang or lang.startswith("en_"): + return + codes = [lang] + if "_" in lang: + codes.append(lang.split("_", 1)[0]) + for code in codes: + path = "locale/{}/LC_MESSAGES/{}.mo".format(code, DOMAIN) + try: + data = plugin.read_file(path) + except Exception: + # Not shipped for this code (a zip raises KeyError, a development + # folder raises OSError): try the next code. + continue + try: + _catalog = gettext.GNUTranslations(io.BytesIO(data)) + except Exception as e: + _logger.info(f"unreadable BAL catalog {path}, using English: {e!r}") + return + _logger.info(f"BAL catalog loaded: {path}") + return + _logger.info(f"no BAL catalog for {lang!r}, using English") + + +def init_from_config(plugin, config) -> None: + """Load the BAL catalog of the language Electrum's Qt GUI is using. + + Repeats Electrum's own choice in ``run_electrum``: the ``language`` + setting (Preferences > Language) or, when it is empty, the system language + if Electrum supports it, else English. Called once, from the Qt plugin's + ``__init__``. Never raises. + """ + try: + lang = config.LOCALIZATION_LANGUAGE + if not lang: + from electrum.gui.default_lang import get_default_language + + lang = get_default_language(gui_name="qt") + except Exception as e: + # Electrum also falls back to English when it cannot read the + # system language. + _logger.info(f"could not read the GUI language, using English: {e!r}") + lang = None + set_language(plugin, lang) diff --git a/bal/locale/bal.pot b/bal/locale/bal.pot new file mode 100644 index 0000000..db211ba --- /dev/null +++ b/bal/locale/bal.pot @@ -0,0 +1,1918 @@ +# Translations template for BAL Electrum plugin. +# Copyright (C) 2026 Bitcoin After Life +# This file is distributed under the same license as the BAL Electrum plugin project. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2026. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: BAL Electrum plugin VERSION\n" +"Report-Msgid-Bugs-To: info@bitcoin-after.life\n" +"POT-Creation-Date: 2026-09-26 22:02+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: bal/core/plugin_base.py:361 +msgid "" +"BAL will execution of $wallet_name\r\n" +" heirs list: \r\n" +"$heirs_complete" +msgstr "" + +#: bal/core/plugin_base.py:367 +msgid "BAL -Will execution of $wallet_name" +msgstr "" + +#: bal/core/will.py:1298 +#, python-brace-format +msgid "NOT {}" +msgstr "" + +#: bal/core/will.py:1322 +msgid "Anticipated" +msgstr "" + +#: bal/core/will.py:1323 +msgid "Broadcasted" +msgstr "" + +#: bal/core/will.py:1324 +msgid "Checked" +msgstr "" + +#: bal/core/will.py:1325 +msgid "Check Failed" +msgstr "" + +#: bal/core/will.py:1326 +msgid "Signed" +msgstr "" + +#: bal/core/will.py:1327 +msgid "Confirmed" +msgstr "" + +#: bal/core/will.py:1328 bal/gui/qt/lists.py:1354 bal/gui/qt/lists.py:1368 +msgid "Error" +msgstr "" + +#: bal/core/will.py:1329 bal/gui/qt/dialogs.py:841 +msgid "Expired" +msgstr "" + +#: bal/core/will.py:1330 +msgid "Exported" +msgstr "" + +#: bal/core/will.py:1331 +msgid "Imported" +msgstr "" + +#: bal/core/will.py:1332 +msgid "Invalidated" +msgstr "" + +#: bal/core/will.py:1333 +msgid "Mempool" +msgstr "" + +#: bal/core/will.py:1334 +msgid "Push failed" +msgstr "" + +#: bal/core/will.py:1335 +msgid "Pushed" +msgstr "" + +#: bal/core/will.py:1336 +msgid "Partially Signed" +msgstr "" + +#: bal/core/will.py:1337 +msgid "Replaced" +msgstr "" + +#: bal/core/will.py:1338 +msgid "Restored" +msgstr "" + +#: bal/core/will.py:1339 +msgid "Updated" +msgstr "" + +#: bal/core/will.py:1340 bal/gui/qt/dialogs.py:2631 +msgid "Valid" +msgstr "" + +#: bal/core/will.py:1346 bal/gui/qt/window.py:551 +msgid "New" +msgstr "" + +#: bal/core/willexecutors.py:245 +msgid "Waiting..." +msgstr "" + +#: bal/core/willexecutors.py:379 bal/gui/qt/window.py:243 +#: bal/gui/qt/window.py:1643 +msgid "Success" +msgstr "" + +#: bal/core/willexecutors.py:389 bal/gui/qt/window.py:1648 +msgid "Failed" +msgstr "" + +#: bal/gui/qt/calendar.py:41 +msgid "Open" +msgstr "" + +#: bal/gui/qt/calendar.py:44 +msgid "Open with..." +msgstr "" + +#: bal/gui/qt/calendar.py:47 +msgid "Save" +msgstr "" + +#: bal/gui/qt/calendar.py:73 bal/gui/qt/widgets.py:1024 +msgid "No reminders were saved: the delivery date is too close (or already passed)" +msgstr "" + +#: bal/gui/qt/calendar.py:105 bal/gui/qt/calendar.py:126 +#, python-brace-format +msgid "Could not open the calendar file: {}" +msgstr "" + +#: bal/gui/qt/calendar.py:115 +msgid "Open calendar with..." +msgstr "" + +#: bal/gui/qt/calendar.py:116 +msgid "Enter the application command:" +msgstr "" + +#: bal/gui/qt/calendar.py:138 bal/gui/qt/widgets.py:1042 +msgid "Save calendar reminder (.ics)" +msgstr "" + +#: bal/gui/qt/calendar.py:150 bal/gui/qt/widgets.py:1062 +#, python-brace-format +msgid "" +"Calendar file saved to:\n" +"{}" +msgstr "" + +#: bal/gui/qt/calendar.py:155 bal/gui/qt/widgets.py:1058 +#, python-brace-format +msgid "Could not save the calendar file: {}" +msgstr "" + +#: bal/gui/qt/common.py:172 +msgid "Small - ~150 bytes/QR (low-res cameras)" +msgstr "" + +#: bal/gui/qt/common.py:173 +msgid "Medium - ~400 bytes/QR" +msgstr "" + +#: bal/gui/qt/common.py:174 +msgid "Large - ~900 bytes/QR" +msgstr "" + +#: bal/gui/qt/common.py:175 +msgid "XL - ~1800 bytes/QR (high-res cameras)" +msgstr "" + +#: bal/gui/qt/common.py:256 +#, python-brace-format +msgid "Select file to save your {}" +msgstr "" + +#: bal/gui/qt/common.py:271 +#, python-brace-format +msgid "Your {0} were exported to '{1}'" +msgstr "" + +#: bal/gui/qt/dialogs.py:212 +msgid "Bal Wizard Setup" +msgstr "" + +#: bal/gui/qt/dialogs.py:349 bal/gui/qt/dialogs.py:2760 +msgid "Previous" +msgstr "" + +#: bal/gui/qt/dialogs.py:353 bal/gui/qt/dialogs.py:2763 +msgid "Next" +msgstr "" + +#: bal/gui/qt/dialogs.py:357 bal/gui/qt/dialogs.py:4077 +msgid "Cancel" +msgstr "" + +#: bal/gui/qt/dialogs.py:383 +msgid "Bitcoin After Life Heirs" +msgstr "" + +#: bal/gui/qt/dialogs.py:385 +#, python-format +msgid "" +"Please add your heirs\n" +" remember that 100% of wallet balance will be spent" +msgstr "" + +#: bal/gui/qt/dialogs.py:393 bal/gui/qt/lists.py:1229 +msgid "Add" +msgstr "" + +#: bal/gui/qt/dialogs.py:395 bal/gui/qt/dialogs.py:2592 bal/gui/qt/lists.py:311 +#: bal/gui/qt/lists.py:673 bal/gui/qt/lists.py:1237 +msgid "Import" +msgstr "" + +#: bal/gui/qt/dialogs.py:397 bal/gui/qt/dialogs.py:2428 +#: bal/gui/qt/dialogs.py:2596 bal/gui/qt/lists.py:312 bal/gui/qt/lists.py:672 +#: bal/gui/qt/lists.py:1249 +msgid "Export" +msgstr "" + +#: bal/gui/qt/dialogs.py:422 bal/gui/qt/dialogs.py:530 +msgid "Bitcoin After Life Will-Executors" +msgstr "" + +#: bal/gui/qt/dialogs.py:423 +msgid "Choose willexecutors download method" +msgstr "" + +#: bal/gui/qt/dialogs.py:430 +msgid "Automatically download and select willexecutors" +msgstr "" + +#: bal/gui/qt/dialogs.py:431 +msgid "Only download willexecutors list" +msgstr "" + +#: bal/gui/qt/dialogs.py:432 +msgid "Import willexecutor list from file" +msgstr "" + +#: bal/gui/qt/dialogs.py:433 +msgid "Manual" +msgstr "" + +#: bal/gui/qt/dialogs.py:462 bal/gui/qt/lists.py:1559 +msgid "willexecutors" +msgstr "" + +#: bal/gui/qt/dialogs.py:531 +msgid "Configure and select your willexecutors" +msgstr "" + +#: bal/gui/qt/dialogs.py:550 +msgid "Bitcoin After Life Will Settings" +msgstr "" + +#: bal/gui/qt/dialogs.py:585 bal/gui/qt/dialogs.py:638 +msgid "Please wait" +msgstr "" + +#: bal/gui/qt/dialogs.py:668 +msgid "Building Will" +msgstr "" + +#: bal/gui/qt/dialogs.py:673 +msgid "Building Will:" +msgstr "" + +#: bal/gui/qt/dialogs.py:754 bal/gui/qt/dialogs.py:772 +#: bal/gui/qt/dialogs.py:782 bal/gui/qt/dialogs.py:805 +msgid "Checking variables" +msgstr "" + +#: bal/gui/qt/dialogs.py:773 +msgid "Check Alive Threshold Passed: you have to Invalidate your old Will" +msgstr "" + +#: bal/gui/qt/dialogs.py:782 bal/gui/qt/dialogs.py:864 +msgid "No Heirs" +msgstr "" + +#: bal/gui/qt/dialogs.py:805 bal/gui/qt/dialogs.py:1351 +#: bal/gui/qt/dialogs.py:2280 bal/gui/qt/window.py:2295 +msgid "Ok" +msgstr "" + +#: bal/gui/qt/dialogs.py:810 +msgid "" +"In the inheritance process, the entire wallet will always be fully emptied. \n" +"Your settings require an adjustment of the amounts" +msgstr "" + +#: bal/gui/qt/dialogs.py:818 bal/gui/qt/window.py:837 +#, python-brace-format +msgid "Will-executor fee too high: {}" +msgstr "" + +#: bal/gui/qt/dialogs.py:852 +msgid "Postponed: invalidating old will" +msgstr "" + +#: bal/gui/qt/dialogs.py:887 bal/gui/qt/dialogs.py:896 +#: bal/gui/qt/dialogs.py:2240 +msgid "Checking your will" +msgstr "" + +#: bal/gui/qt/dialogs.py:889 +msgid "An inheritance of this wallet is already executed (on blockchain)" +msgstr "" + +#: bal/gui/qt/dialogs.py:898 +msgid "Inheritance in mempool (waiting confirmation)" +msgstr "" + +#: bal/gui/qt/dialogs.py:931 +msgid "Will-Executor excluded" +msgstr "" + +#: bal/gui/qt/dialogs.py:931 bal/gui/qt/dialogs.py:1025 +#: bal/gui/qt/dialogs.py:2221 bal/gui/qt/dialogs.py:2232 +msgid "Skipped" +msgstr "" + +#: bal/gui/qt/dialogs.py:937 bal/gui/qt/dialogs.py:1709 bal/gui/qt/lists.py:345 +#: bal/gui/qt/theme.py:116 +msgid "Will-Executor" +msgstr "" + +#: bal/gui/qt/dialogs.py:938 +msgid "Not present - select one or enable backup mode" +msgstr "" + +#: bal/gui/qt/dialogs.py:1000 +msgid "All heirs' shares are below the dust limit: the inheritance cannot be created. Increase the amounts or reduce the number of heirs." +msgstr "" + +#: bal/gui/qt/dialogs.py:1016 +#, python-brace-format +msgid "Wallet balance is too low: {} satoshi available, but the miner and will-executor fees need {} satoshi (the minimum usable amount is {} satoshi). Add funds, or select fewer will-executors." +msgstr "" + +#: bal/gui/qt/dialogs.py:1059 +#, python-brace-format +msgid "Heir {}" +msgstr "" + +#: bal/gui/qt/dialogs.py:1061 +#, python-brace-format +msgid "{} is DUST - excluded (amount below dust limit)" +msgstr "" + +#: bal/gui/qt/dialogs.py:1117 +msgid "Heir" +msgstr "" + +#: bal/gui/qt/dialogs.py:1233 bal/gui/qt/dialogs.py:2269 +#, python-brace-format +msgid "Please wait {}secs" +msgstr "" + +#: bal/gui/qt/dialogs.py:1240 bal/gui/qt/dialogs.py:1292 +#: bal/gui/qt/dialogs.py:1330 +msgid "Broadcasting" +msgstr "" + +#: bal/gui/qt/dialogs.py:1351 bal/gui/qt/window.py:2297 +msgid "Ko" +msgstr "" + +#: bal/gui/qt/dialogs.py:1362 +msgid "Timeout - no answer" +msgstr "" + +#: bal/gui/qt/dialogs.py:1404 +#, python-brace-format +msgid "checking {} - {}" +msgstr "" + +#: bal/gui/qt/dialogs.py:1407 bal/gui/qt/dialogs.py:2238 +#: bal/gui/qt/window.py:1671 +msgid "Waiting" +msgstr "" + +#: bal/gui/qt/dialogs.py:1421 +#, python-brace-format +msgid "checked {} - {}" +msgstr "" + +#: bal/gui/qt/dialogs.py:1471 bal/gui/qt/dialogs.py:2024 +#: bal/gui/qt/dialogs.py:2035 +#, python-brace-format +msgid "Error: {}" +msgstr "" + +#: bal/gui/qt/dialogs.py:1533 +msgid "" +"Your will has expired and must be invalidated before it can be rebuilt.\n" +"A transaction window will now open:\n" +"please SIGN and then BROADCAST it to invalidate your old will.\n" +"After the invalidation is confirmed, press the Check button to finish the will." +msgstr "" + +#: bal/gui/qt/dialogs.py:1573 +msgid "" +"Your old will has been invalidated and the transaction was broadcast.\n" +"Electrum may need a little time to register it.\n" +"Please wait until the invalidation transaction is confirmed, then press the Check button again to finish updating your will." +msgstr "" + +#: bal/gui/qt/dialogs.py:1588 +msgid "Invalidate your old will" +msgstr "" + +#: bal/gui/qt/dialogs.py:1602 bal/gui/qt/dialogs.py:1647 +msgid "Aborted" +msgstr "" + +#: bal/gui/qt/dialogs.py:1621 +msgid "Auto-sign disabled — sign manually" +msgstr "" + +#: bal/gui/qt/dialogs.py:1635 +msgid "" +"The delivery date was automatically moved one day earlier so the updated will can correctly replace the previous one.\n" +"Please sign (and broadcast) to confirm the change." +msgstr "" + +#: bal/gui/qt/dialogs.py:1644 +msgid "Sign your will" +msgstr "" + +#: bal/gui/qt/dialogs.py:1649 bal/gui/qt/dialogs.py:1927 +msgid "Nothing to do" +msgstr "" + +#: bal/gui/qt/dialogs.py:1668 +msgid "Finished" +msgstr "" + +#: bal/gui/qt/dialogs.py:1685 +msgid "Calendar" +msgstr "" + +#: bal/gui/qt/dialogs.py:1687 bal/gui/qt/dialogs.py:1717 +#: bal/gui/qt/dialogs.py:1784 bal/gui/qt/dialogs.py:2027 +#: bal/gui/qt/dialogs.py:3026 bal/gui/qt/dialogs.py:3265 +#: bal/gui/qt/dialogs.py:4096 +msgid "Close" +msgstr "" + +#: bal/gui/qt/dialogs.py:1713 bal/gui/qt/dialogs.py:1780 +msgid "🔮 Wizard" +msgstr "" + +#: bal/gui/qt/dialogs.py:1776 bal/gui/qt/dialogs.py:2579 +msgid "Heirs" +msgstr "" + +#: bal/gui/qt/dialogs.py:1880 bal/gui/qt/widgets.py:1130 +#, python-brace-format +msgid "(reminder {idx}/{total})" +msgstr "" + +#: bal/gui/qt/dialogs.py:1944 +msgid "All done" +msgstr "" + +#: bal/gui/qt/dialogs.py:1996 +msgid "Next steps (manual): press 'Sign' to sign your will, then 'Broadcast' to send it to the will-executors." +msgstr "" + +#: bal/gui/qt/dialogs.py:2001 +msgid "Next step (manual): press 'Sign' to sign your will." +msgstr "" + +#: bal/gui/qt/dialogs.py:2005 +msgid "Next step (manual): press 'Broadcast' to send your will to the will-executors." +msgstr "" + +#: bal/gui/qt/dialogs.py:2100 +#, python-brace-format +msgid "Miner fee rate changed (the will was built with {} sat/byte, now it is {}): a new will must be prepared." +msgstr "" + +#: bal/gui/qt/dialogs.py:2105 +msgid "The miner fee rate changed: a new will must be prepared." +msgstr "" + +#: bal/gui/qt/dialogs.py:2109 +#, python-brace-format +msgid "Will-executor \"{}\" is not covered by the current will: a new will must be prepared." +msgstr "" + +#: bal/gui/qt/dialogs.py:2114 +msgid "Backup mode is enabled but the will has no backup transaction: a new will must be prepared." +msgstr "" + +#: bal/gui/qt/dialogs.py:2123 +#, python-brace-format +msgid "Heir \"{}\" is not covered by the current will (it was added or removed, or its delivery date changed): a new will must be prepared." +msgstr "" + +#: bal/gui/qt/dialogs.py:2131 +msgid "The heirs changed: a new will must be prepared." +msgstr "" + +#: bal/gui/qt/dialogs.py:2133 +msgid "A will-executor changed: a new will must be prepared." +msgstr "" + +#: bal/gui/qt/dialogs.py:2143 +msgid "The wallet contains funds that the current will does not cover yet: a new will must be prepared." +msgstr "" + +#: bal/gui/qt/dialogs.py:2147 +msgid "The will contains no valid transaction: a new will must be prepared." +msgstr "" + +#: bal/gui/qt/dialogs.py:2153 +msgid "The will is no longer coherent and must be rebuilt; the exact reason could not be determined." +msgstr "" + +#: bal/gui/qt/dialogs.py:2183 +msgid "No heirs: add at least one heir before building the will." +msgstr "" + +#: bal/gui/qt/dialogs.py:2186 +msgid "The wallet has no spendable funds, so no inheritance transaction can be created." +msgstr "" + +#: bal/gui/qt/dialogs.py:2190 +msgid "No usable will-executor: none of the servers in the list is both selected and valid. Open the will-executor settings, select at least one server and check that it is reachable." +msgstr "" + +#: bal/gui/qt/dialogs.py:2195 +msgid "No delivery date left to build: every heir's date is already covered by the existing will. Choose a later delivery date, or change an heir's date." +msgstr "" + +#: bal/gui/qt/dialogs.py:2200 +msgid "The amount to send must cover the miner fees plus this will-executor's fee, and the wallet balance is not enough: select cheaper will-executors, or add funds to the wallet." +msgstr "" + +#: bal/gui/qt/dialogs.py:2205 +msgid "A will-executor asks for more than the maximum fee you allowed: raise the maximum fee in the settings, or select a cheaper will-executor." +msgstr "" + +#: bal/gui/qt/dialogs.py:2210 +msgid "The inheritance transactions could not be assembled from the available funds (the balance may not cover the miner fees)." +msgstr "" + +#: bal/gui/qt/dialogs.py:2214 +msgid "An unexpected error stopped the transactions being prepared for a will-executor, so it was skipped. Try again, or select a different will-executor." +msgstr "" + +#: bal/gui/qt/dialogs.py:2225 +msgid "" +"Could not build the will, and the exact cause could not be determined. Please check that:\n" +"- the wallet balance covers the miner and will-executor fees,\n" +"- each heir's share is above the minimum (dust limit),\n" +"- the Check Alive date is EARLIER than the delivery date,\n" +"- at least one will-executor is selected and reachable." +msgstr "" + +#: bal/gui/qt/dialogs.py:2245 +msgid "Invalidating old will" +msgstr "" + +#: bal/gui/qt/dialogs.py:2251 +msgid "Building your will" +msgstr "" + +#: bal/gui/qt/dialogs.py:2257 +msgid "Signing your will" +msgstr "" + +#: bal/gui/qt/dialogs.py:2263 +msgid "Broadcasting your will to executors" +msgstr "" + +#: bal/gui/qt/dialogs.py:2311 +msgid "Wait" +msgstr "" + +#: bal/gui/qt/dialogs.py:2414 +msgid "Will Details" +msgstr "" + +#: bal/gui/qt/dialogs.py:2420 bal/gui/qt/dialogs.py:4071 +#: bal/gui/qt/lists.py:668 +msgid "Sign" +msgstr "" + +#: bal/gui/qt/dialogs.py:2424 bal/gui/qt/lists.py:675 +msgid "Broadcast" +msgstr "" + +#: bal/gui/qt/dialogs.py:2431 bal/gui/qt/lists.py:677 +msgid "Invalidate" +msgstr "" + +#: bal/gui/qt/dialogs.py:2436 bal/gui/qt/lists.py:674 +msgid "Merge" +msgstr "" + +#: bal/gui/qt/dialogs.py:2444 +#, python-brace-format +msgid "Expiration date: {}" +msgstr "" + +#: bal/gui/qt/dialogs.py:2450 +#, python-brace-format +msgid "Valid Txs:{}" +msgstr "" + +#: bal/gui/qt/dialogs.py:2452 +#, python-brace-format +msgid "Total Txs:{}" +msgstr "" + +#: bal/gui/qt/dialogs.py:2506 +msgid "Hide replaced" +msgstr "" + +#: bal/gui/qt/dialogs.py:2508 +msgid "Unhide replaced" +msgstr "" + +#: bal/gui/qt/dialogs.py:2514 +msgid "Hide invalidated" +msgstr "" + +#: bal/gui/qt/dialogs.py:2516 +msgid "Unhide invalidated" +msgstr "" + +#: bal/gui/qt/dialogs.py:2541 +msgid "Will-Executor Service List" +msgstr "" + +#: bal/gui/qt/dialogs.py:2588 bal/gui/qt/lists.py:314 +msgid "New Heir" +msgstr "" + +#: bal/gui/qt/dialogs.py:2630 +msgid "All" +msgstr "" + +#: bal/gui/qt/dialogs.py:2633 +msgid "Valid NC" +msgstr "" + +#: bal/gui/qt/dialogs.py:2771 bal/gui/qt/dialogs.py:2886 +msgid "Auto" +msgstr "" + +#: bal/gui/qt/dialogs.py:2773 +msgid "Automatically advance through the QR codes." +msgstr "" + +#: bal/gui/qt/dialogs.py:2777 +msgid "QR codes per second:" +msgstr "" + +#: bal/gui/qt/dialogs.py:2781 +msgid " /s" +msgstr "" + +#: bal/gui/qt/dialogs.py:2783 +msgid "Loop" +msgstr "" + +#: bal/gui/qt/dialogs.py:2786 +msgid "When the last QR code is reached, keep cycling from the first one instead of stopping." +msgstr "" + +#: bal/gui/qt/dialogs.py:2795 +msgid "QR code size:" +msgstr "" + +#: bal/gui/qt/dialogs.py:2807 +msgid "Format:" +msgstr "" + +#: bal/gui/qt/dialogs.py:2813 +msgid "(default)" +msgstr "" + +#: bal/gui/qt/dialogs.py:2880 +msgid "Stop" +msgstr "" + +#: bal/gui/qt/dialogs.py:2903 +msgid "Proprietary format; every other BAL wallet understands it." +msgstr "" + +#: bal/gui/qt/dialogs.py:2905 +msgid "Legacy BC-UR v1 (ur:bytes), compatible with older Blockchain Commons tools." +msgstr "" + +#: bal/gui/qt/dialogs.py:2907 +msgid "Standard BC-UR v2 fountain codes (ur:bytes)." +msgstr "" + +#: bal/gui/qt/dialogs.py:2908 +msgid "Coinkite BBQR (B$ frames) for BitKit & friends." +msgstr "" + +#: bal/gui/qt/dialogs.py:2914 +#, python-brace-format +msgid "" +"Scan the QR codes below, in order, with the will-opening device.\n" +"Frame 1 of {} carries the total number of codes." +msgstr "" + +#: bal/gui/qt/dialogs.py:2921 +#, python-brace-format +msgid "" +"Scan the QR codes below with the will-opening device.\n" +"{} codes carry the whole transfer (any order works, duplicates are ignored)." +msgstr "" + +#: bal/gui/qt/dialogs.py:2942 +#, python-brace-format +msgid "Frame {} of {}" +msgstr "" + +#: bal/gui/qt/dialogs.py:2965 +msgid "Export will" +msgstr "" + +#: bal/gui/qt/dialogs.py:2975 +msgid "No will transaction to export." +msgstr "" + +#: bal/gui/qt/dialogs.py:2982 +msgid "Export:" +msgstr "" + +#: bal/gui/qt/dialogs.py:2989 +msgid "Whole will" +msgstr "" + +#: bal/gui/qt/dialogs.py:2993 +msgid "Export the full will items (all details/statuses) as JSON, or only the serialized transactions." +msgstr "" + +#: bal/gui/qt/dialogs.py:3003 +msgid "Send as:" +msgstr "" + +#: bal/gui/qt/dialogs.py:3005 bal/gui/qt/dialogs.py:3242 +msgid "File" +msgstr "" + +#: bal/gui/qt/dialogs.py:3006 bal/gui/qt/dialogs.py:3243 +msgid "QR Code" +msgstr "" + +#: bal/gui/qt/dialogs.py:3007 bal/gui/qt/dialogs.py:3244 +msgid "Audio" +msgstr "" + +#: bal/gui/qt/dialogs.py:3049 +msgid "Export the will as a JSON file. Uncheck \"Whole will\" above to export only the serialized transactions (comma-separated)." +msgstr "" + +#: bal/gui/qt/dialogs.py:3057 +msgid "Export…" +msgstr "" + +#: bal/gui/qt/dialogs.py:3074 bal/gui/qt/dialogs.py:3300 +#: bal/gui/qt/window.py:1767 bal/gui/qt/window.py:1780 +msgid "Audio MODEM plugin is not available." +msgstr "" + +#: bal/gui/qt/dialogs.py:3078 bal/gui/qt/dialogs.py:3304 +msgid "Speed (KB/sec):" +msgstr "" + +#: bal/gui/qt/dialogs.py:3089 +msgid "Send" +msgstr "" + +#: bal/gui/qt/dialogs.py:3134 bal/gui/qt/dialogs.py:3168 +#: bal/gui/qt/dialogs.py:3193 +msgid "No will transaction matches the selected filter." +msgstr "" + +#: bal/gui/qt/dialogs.py:3157 +msgid "item(s)" +msgstr "" + +#: bal/gui/qt/dialogs.py:3157 +msgid "transaction(s)" +msgstr "" + +#: bal/gui/qt/dialogs.py:3159 +#, python-brace-format +msgid "{} {} will be exported." +msgstr "" + +#: bal/gui/qt/dialogs.py:3162 +#, python-brace-format +msgid "{} {} will be sent." +msgstr "" + +#: bal/gui/qt/dialogs.py:3186 bal/gui/qt/dialogs.py:3189 +#: bal/gui/qt/dialogs.py:3340 bal/gui/qt/dialogs.py:3343 +msgid "Invalid audio speed." +msgstr "" + +#: bal/gui/qt/dialogs.py:3225 +msgid "Import will" +msgstr "" + +#: bal/gui/qt/dialogs.py:3232 +msgid "" +"Choose how the will was exported: from a file, QR codes or audio.\n" +"The import never touches the live will." +msgstr "" + +#: bal/gui/qt/dialogs.py:3240 +msgid "Import from:" +msgstr "" + +#: bal/gui/qt/dialogs.py:3277 +msgid "Import the JSON will file written by the Export ▶ File option. The will opens in a read-only preview window." +msgstr "" + +#: bal/gui/qt/dialogs.py:3283 +msgid "Choose will file…" +msgstr "" + +#: bal/gui/qt/dialogs.py:3313 +msgid "Waiting for the audio transfer…" +msgstr "" + +#: bal/gui/qt/dialogs.py:3316 +msgid "Receive by audio…" +msgstr "" + +#: bal/gui/qt/dialogs.py:3357 +msgid "Receiving…" +msgstr "" + +#: bal/gui/qt/dialogs.py:3377 +msgid "No transaction data received." +msgstr "" + +#: bal/gui/qt/dialogs.py:3395 +#, python-brace-format +msgid "Waiting for audio ({:.1f} kbps)…" +msgstr "" + +#: bal/gui/qt/dialogs.py:3468 +#, python-brace-format +msgid "Could not parse a transferred will item: {}" +msgstr "" + +#: bal/gui/qt/dialogs.py:3488 +#, python-brace-format +msgid "Could not parse a transferred transaction: {}" +msgstr "" + +#: bal/gui/qt/dialogs.py:3500 +msgid "The imported will contains no valid transaction in this wallet." +msgstr "" + +#: bal/gui/qt/dialogs.py:3511 +#, python-brace-format +msgid "{} imported transaction(s) are not valid in this wallet and were skipped." +msgstr "" + +#: bal/gui/qt/dialogs.py:3611 +msgid "" +"Show the will QR codes to the camera, one after another.\n" +"After the first code, every new code is captured automatically.\n" +"The first frame sets the total number of codes; duplicates are ignored." +msgstr "" + +#: bal/gui/qt/dialogs.py:3620 bal/gui/qt/dialogs.py:3798 +msgid "Waiting for the first frame…" +msgstr "" + +#: bal/gui/qt/dialogs.py:3637 +msgid "…or paste/type the frame text here" +msgstr "" + +#: bal/gui/qt/dialogs.py:3641 +msgid "Add frame" +msgstr "" + +#: bal/gui/qt/dialogs.py:3647 bal/gui/qt/dialogs.py:3890 +msgid "Scan with camera…" +msgstr "" + +#: bal/gui/qt/dialogs.py:3650 +msgid "Start the live camera. While it runs, every new QR code shown to the camera is captured automatically." +msgstr "" + +#: bal/gui/qt/dialogs.py:3656 +msgid "Reset" +msgstr "" + +#: bal/gui/qt/dialogs.py:3663 +msgid "Review and Sign…" +msgstr "" + +#: bal/gui/qt/dialogs.py:3683 +#, python-brace-format +msgid "The scanned code belongs to a different transfer ({} frames, {}). The import was reset; scan the first code again." +msgstr "" + +#: bal/gui/qt/dialogs.py:3773 +#, python-brace-format +msgid "All {} frames stored." +msgstr "" + +#: bal/gui/qt/dialogs.py:3779 +#, python-brace-format +msgid "Stored {} of {} frames." +msgstr "" + +#: bal/gui/qt/dialogs.py:3844 +msgid "Cannot start QR scanner, no usable camera found." +msgstr "" + +#: bal/gui/qt/dialogs.py:3849 +msgid "Stop scanning" +msgstr "" + +#: bal/gui/qt/dialogs.py:3896 +#, python-brace-format +msgid "Camera error: {}" +msgstr "" + +#: bal/gui/qt/dialogs.py:3943 +#, python-brace-format +msgid "The scanned code belongs to a different transfer ({} frames, {}). The import was reset; show the first code again." +msgstr "" + +#: bal/gui/qt/dialogs.py:3986 +msgid "The transferred will contains no transactions." +msgstr "" + +#: bal/gui/qt/dialogs.py:4015 +msgid "Review and sign imported will" +msgstr "" + +#: bal/gui/qt/dialogs.py:4028 +msgid "Enter your wallet password to sign the imported transactions." +msgstr "" + +#: bal/gui/qt/dialogs.py:4074 +msgid "Skip" +msgstr "" + +#: bal/gui/qt/dialogs.py:4090 +msgid "Save signed file…" +msgstr "" + +#: bal/gui/qt/dialogs.py:4093 +msgid "Show signed QR…" +msgstr "" + +#: bal/gui/qt/dialogs.py:4117 +#, python-brace-format +msgid "Transaction {} of {}" +msgstr "" + +#: bal/gui/qt/dialogs.py:4119 +#, python-brace-format +msgid "TXID: {}" +msgstr "" + +#: bal/gui/qt/dialogs.py:4122 +msgid "unknown" +msgstr "" + +#: bal/gui/qt/dialogs.py:4128 +msgid "(no outputs)" +msgstr "" + +#: bal/gui/qt/dialogs.py:4142 +msgid "unknown (partial transaction)" +msgstr "" + +#: bal/gui/qt/dialogs.py:4144 +#, python-brace-format +msgid "" +"Total outputs: {}\n" +"Fee: {}" +msgstr "" + +#: bal/gui/qt/dialogs.py:4156 +#, python-brace-format +msgid "" +"Signed {} of {} transactions.\n" +"\n" +"Save a signed file to carry to the broadcast device, or show the signed transactions as QR codes." +msgstr "" + +#: bal/gui/qt/dialogs.py:4174 +#, python-brace-format +msgid "Could not sign the transaction: {}" +msgstr "" + +#: bal/gui/qt/dialogs.py:4193 +msgid "Signed will saved." +msgstr "" + +#: bal/gui/qt/dialogs.py:4204 +msgid "No signed transaction to show." +msgstr "" + +#: bal/gui/qt/lists.py:134 bal/gui/qt/window.py:368 +msgid "Name" +msgstr "" + +#: bal/gui/qt/lists.py:135 bal/gui/qt/lists.py:1432 bal/gui/qt/widgets.py:1395 +#: bal/gui/qt/window.py:372 +msgid "Address" +msgstr "" + +#: bal/gui/qt/lists.py:136 bal/gui/qt/window.py:307 +msgid "Amount" +msgstr "" + +#: bal/gui/qt/lists.py:232 +#, python-brace-format +msgid "Copy {}" +msgstr "" + +#: bal/gui/qt/lists.py:240 bal/gui/qt/lists.py:1007 +#, python-brace-format +msgid "Edit {}" +msgstr "" + +#: bal/gui/qt/lists.py:243 +msgid "Delete" +msgstr "" + +#: bal/gui/qt/lists.py:310 +msgid "&New Heir" +msgstr "" + +#: bal/gui/qt/lists.py:343 bal/gui/qt/widgets.py:1332 bal/gui/qt/window.py:383 +msgid "Locktime" +msgstr "" + +#: bal/gui/qt/lists.py:344 +msgid "Txid" +msgstr "" + +#: bal/gui/qt/lists.py:346 bal/gui/qt/widgets.py:1347 +msgid "Status" +msgstr "" + +#: bal/gui/qt/lists.py:347 +msgid "Server" +msgstr "" + +#: bal/gui/qt/lists.py:409 +msgid "details" +msgstr "" + +#: bal/gui/qt/lists.py:413 +msgid "sign" +msgstr "" + +#: bal/gui/qt/lists.py:419 +msgid "broadcast" +msgstr "" + +#: bal/gui/qt/lists.py:425 +msgid "check " +msgstr "" + +#: bal/gui/qt/lists.py:431 +msgid "copy id" +msgstr "" + +#: bal/gui/qt/lists.py:435 +msgid "copy" +msgstr "" + +#: bal/gui/qt/lists.py:439 +msgid "merge from txn" +msgstr "" + +#: bal/gui/qt/lists.py:445 bal/gui/qt/lists.py:1017 +msgid "delete" +msgstr "" + +#: bal/gui/qt/lists.py:504 +msgid "Transaction IDs" +msgstr "" + +#: bal/gui/qt/lists.py:510 +msgid "Transactions" +msgstr "" + +#: bal/gui/qt/lists.py:524 +msgid "Open transaction file" +msgstr "" + +#: bal/gui/qt/lists.py:536 +#, python-brace-format +msgid "Invalid transaction file: {}" +msgstr "" + +#: bal/gui/qt/lists.py:666 +msgid "Prepare" +msgstr "" + +#: bal/gui/qt/lists.py:667 +msgid "Display" +msgstr "" + +#: bal/gui/qt/lists.py:676 +msgid "Check" +msgstr "" + +#: bal/gui/qt/lists.py:682 +msgid "Build Your Will" +msgstr "" + +#: bal/gui/qt/lists.py:692 +msgid "Wizard - Build your will" +msgstr "" + +#: bal/gui/qt/lists.py:706 +msgid "Check Inheritance" +msgstr "" + +#: bal/gui/qt/lists.py:911 +msgid "Url" +msgstr "" + +#: bal/gui/qt/lists.py:912 +msgid "S" +msgstr "" + +#: bal/gui/qt/lists.py:913 +msgid "Base fee" +msgstr "" + +#: bal/gui/qt/lists.py:914 bal/gui/qt/lists.py:1414 +msgid "Info" +msgstr "" + +#: bal/gui/qt/lists.py:915 +msgid "Default Address" +msgstr "" + +#: bal/gui/qt/lists.py:990 +msgid "Copy" +msgstr "" + +#: bal/gui/qt/lists.py:995 +msgid "deselect" +msgstr "" + +#: bal/gui/qt/lists.py:1000 +msgid "select" +msgstr "" + +#: bal/gui/qt/lists.py:1012 +msgid "Ping" +msgstr "" + +#: bal/gui/qt/lists.py:1216 +msgid "Add transactions without willexecutor" +msgstr "" + +#: bal/gui/qt/lists.py:1233 +msgid "Download List" +msgstr "" + +#: bal/gui/qt/lists.py:1251 +msgid "Export all" +msgstr "" + +#: bal/gui/qt/lists.py:1253 +msgid "Export selected" +msgstr "" + +#: bal/gui/qt/lists.py:1256 +msgid "Export only valid" +msgstr "" + +#: bal/gui/qt/lists.py:1260 +msgid "Ping All" +msgstr "" + +#: bal/gui/qt/lists.py:1262 +msgid "Ping all" +msgstr "" + +#: bal/gui/qt/lists.py:1264 +msgid "Ping selected" +msgstr "" + +#: bal/gui/qt/lists.py:1268 +msgid "Select All" +msgstr "" + +#: bal/gui/qt/lists.py:1270 +msgid "Select all" +msgstr "" + +#: bal/gui/qt/lists.py:1272 +msgid "Select only valid" +msgstr "" + +#: bal/gui/qt/lists.py:1274 +msgid "Deselect all" +msgstr "" + +#: bal/gui/qt/lists.py:1276 +msgid "Deselect only invalid" +msgstr "" + +#: bal/gui/qt/lists.py:1288 bal/gui/qt/window.py:288 +#, python-brace-format +msgid "Edit: {}" +msgstr "" + +#: bal/gui/qt/lists.py:1290 bal/gui/qt/lists.py:1302 bal/gui/qt/lists.py:1316 +#: bal/gui/qt/lists.py:1476 +msgid "New Will Executor" +msgstr "" + +#: bal/gui/qt/lists.py:1323 bal/gui/qt/lists.py:1449 +msgid "URL is required" +msgstr "" + +#: bal/gui/qt/lists.py:1355 +#, python-brace-format +msgid "Could not reach server at {}" +msgstr "" + +#: bal/gui/qt/lists.py:1369 +#, python-brace-format +msgid "Error contacting server: {}" +msgstr "" + +#: bal/gui/qt/lists.py:1390 +msgid "Add another" +msgstr "" + +#: bal/gui/qt/lists.py:1403 +msgid "URL" +msgstr "" + +#: bal/gui/qt/lists.py:1408 +msgid "Will executor server URL (e.g. http://192.168.1.100:8080)" +msgstr "" + +#: bal/gui/qt/lists.py:1417 +msgid "A short description or name for this executor" +msgstr "" + +#: bal/gui/qt/lists.py:1423 +msgid "Base Fee (sats)" +msgstr "" + +#: bal/gui/qt/lists.py:1426 +msgid "Base fee in satoshis" +msgstr "" + +#: bal/gui/qt/lists.py:1435 +msgid "Bitcoin address for fee payments (optional)" +msgstr "" + +#: bal/gui/qt/lists.py:1496 +msgid "No will-executor matches the selected filter" +msgstr "" + +#: bal/gui/qt/lists.py:1552 +msgid "No will-executor is selected" +msgstr "" + +#: bal/gui/qt/plugin.py:140 +msgid "Electrum Plugins" +msgstr "" + +#: bal/gui/qt/plugin.py:307 +msgid "Bitcoin After Life" +msgstr "" + +#: bal/gui/qt/plugin.py:426 bal/gui/qt/plugin.py:451 +msgid "Settings" +msgstr "" + +#: bal/gui/qt/plugin.py:568 +msgid "BASIC" +msgstr "" + +#: bal/gui/qt/plugin.py:568 +msgid "ADVANCED" +msgstr "" + +#: bal/gui/qt/plugin.py:588 +msgid "Enable ADVANCED mode" +msgstr "" + +#: bal/gui/qt/plugin.py:589 +msgid "Type 'at My Risk' to enable ADVANCED mode" +msgstr "" + +#: bal/gui/qt/plugin.py:658 +msgid "Reset to default" +msgstr "" + +#: bal/gui/qt/plugin.py:680 +msgid "Rebroadcast transactions" +msgstr "" + +#: bal/gui/qt/plugin.py:691 +msgid "Warning: change these settings only if you know what you are doing." +msgstr "" + +#: bal/gui/qt/plugin.py:706 +msgid "Hide Replaced" +msgstr "" + +#: bal/gui/qt/plugin.py:709 +msgid "Hide replaced transactions from will detail and list" +msgstr "" + +#: bal/gui/qt/plugin.py:714 +msgid "Hide Invalidated" +msgstr "" + +#: bal/gui/qt/plugin.py:717 +msgid "Hide invalidated transactions from will detail and list" +msgstr "" + +#: bal/gui/qt/plugin.py:720 +msgid "Auto-sign on Check" +msgstr "" + +#: bal/gui/qt/plugin.py:722 +msgid "" +"When checking, automatically sign and broadcast the will transactions to their will-executors.\n" +"The wallet password is requested only if the wallet is encrypted." +msgstr "" + +#: bal/gui/qt/plugin.py:736 +msgid "Panel editable Date and Fee" +msgstr "" + +#: bal/gui/qt/plugin.py:740 +msgid "" +"When enabled, the delivery-time and check-alive date fields can be edited everywhere (toolbar / Heirs tab), not only in the will-building wizard.\n" +"When disabled, those dates are display-only outside the wizard." +msgstr "" + +#: bal/gui/qt/plugin.py:751 +msgid "Max Will-Executor Fee (satoshi)" +msgstr "" + +#: bal/gui/qt/plugin.py:755 +msgid "" +"Maximum fee (in satoshi) allowed to be paid to a single will-executor. If a will-executor charges more than this, the will will not be built.\n" +"Default: 500,000 satoshi (0.005 BTC)." +msgstr "" + +#: bal/gui/qt/plugin.py:766 +msgid "User Type" +msgstr "" + +#: bal/gui/qt/plugin.py:770 +msgid "" +"Choose how much detail the plugin shows.\n" +"\n" +"BASIC: simplified interface, safe configuration for most users.\n" +"\n" +"ADVANCED: shows every control, including the 'Check Alive' field and the Raw/Date selector. Allows configuring the welist server URL and keeps all date/fee fields always editable." +msgstr "" + +#: bal/gui/qt/plugin.py:783 +msgid "Number of reminders" +msgstr "" + +#: bal/gui/qt/plugin.py:785 +msgid "How many reminder alarms the exported calendar (.ics) event contains. Range: 1 to 5 (default 3). Only used in ADVANCED mode." +msgstr "" + +#: bal/gui/qt/plugin.py:794 +msgid "Event summary" +msgstr "" + +#: bal/gui/qt/plugin.py:796 +msgid "" +"Default message to be used in event summary\n" +"Variables:\n" +" $wallet_name: name of wallet\n" +" $heirs_complete: list of heirs name,address,amount\n" +"Only used in ADVANCED mode." +msgstr "" + +#: bal/gui/qt/plugin.py:808 +msgid "Event description" +msgstr "" + +#: bal/gui/qt/plugin.py:810 +msgid "" +"Default message to be used in event description\n" +"Variables:\n" +" $wallet_name: name of wallet\n" +" $heirs_complete: list of heirs name,address,amount\n" +"Only used in ADVANCED mode." +msgstr "" + +#: bal/gui/qt/plugin.py:823 +msgid "Welist Server URL" +msgstr "" + +#: bal/gui/qt/plugin.py:825 +msgid "URL of the server that provides the will-executor list. Only available in ADVANCED mode." +msgstr "" + +#: bal/gui/qt/plugin.py:834 +msgid "Calendar app command" +msgstr "" + +#: bal/gui/qt/plugin.py:836 +msgid "" +"Command used to open .ics calendar files.\n" +"Leave empty to use the system default (xdg-open/open/start).\n" +"Only used in ADVANCED mode." +msgstr "" + +#: bal/gui/qt/plugin.py:849 +msgid "Save inheritance transactions in history" +msgstr "" + +#: bal/gui/qt/plugin.py:851 +#, python-brace-format +msgid "" +"After each check, save the valid will transactions into the wallet's local history (the History tab), each with a label.\n" +"The label may contain the variable:\n" +" {willexecutor}: replaced with the will-executor URL of the item\n" +"Only used in ADVANCED mode." +msgstr "" + +#: bal/gui/qt/plugin.py:863 +msgid "History label" +msgstr "" + +#: bal/gui/qt/plugin.py:865 +#, python-brace-format +msgid "" +"Label applied to the will transactions saved into the wallet's local history.\n" +"Variables:\n" +" {willexecutor}: replaced with the will-executor URL of the item\n" +"Only used in ADVANCED mode." +msgstr "" + +#: bal/gui/qt/plugin.py:887 +msgid "Broadcast all transactions to willexecutors including those already pushed" +msgstr "" + +#: bal/gui/qt/plugin.py:896 +msgid "Rebuild will on wallet close" +msgstr "" + +#: bal/gui/qt/plugin.py:898 +msgid "" +"Run the 'Build your will' wizard every time the wallet is closed or Electrum is quit, so the will is rebuilt and re-validated.\n" +"When disabled, the will is only rebuilt when you press Check or Prepare. The last built state is still saved to the wallet." +msgstr "" + +#: bal/gui/qt/plugin.py:914 +msgid "Rebuild automatically on new transactions" +msgstr "" + +#: bal/gui/qt/plugin.py:916 +msgid "" +"When a new transaction arrives for the wallet, automatically rebuild the will the same way the wizard does at wallet close: the delivery date is anticipated by one day so the new will replaces the previous one, and the rebuilt transactions are signed and sent to their will-executors.\n" +"An on-chain invalidation transaction is only built when the anticipated delivery date would fall before the Check Alive threshold, or when the threshold is already in the past.\n" +"When disabled (default), the will is only rebuilt on Check / Prepare / wallet close." +msgstr "" + +#: bal/gui/qt/plugin.py:938 +msgid "QR Code Size" +msgstr "" + +#: bal/gui/qt/plugin.py:940 +msgid "" +"Payload size of a single QR code when exporting a will via QR.\n" +"\n" +"Larger QR codes hold more data (fewer shots) but are easier to scan with a high-resolution camera; smaller QR codes scan fine even with low-resolution cameras but require more shots.\n" +"The same selector is available inside the export dialog." +msgstr "" + +#: bal/gui/qt/plugin.py:1021 +msgid "Reset to Default Setting" +msgstr "" + +#: bal/gui/qt/plugin.py:1022 +msgid "Reset these settings to their default values" +msgstr "" + +#: bal/gui/qt/plugin.py:1029 +msgid "Open the Bitcoin After Life support website" +msgstr "" + +#: bal/gui/qt/plugin.py:1041 +msgid "Support:" +msgstr "" + +#: bal/gui/qt/plugin.py:1098 +#, python-brace-format +msgid "BAL - {}" +msgstr "" + +#: bal/gui/qt/theme.py:93 +msgid "Not on server" +msgstr "" + +#: bal/gui/qt/theme.py:95 +msgid "Confirmed on server" +msgstr "" + +#: bal/gui/qt/theme.py:97 +msgid "Send failed" +msgstr "" + +#: bal/gui/qt/theme.py:99 +msgid "Sent (not checked)" +msgstr "" + +#: bal/gui/qt/theme.py:101 +msgid "Signed (not sent)" +msgstr "" + +#: bal/gui/qt/theme.py:102 +msgid "Not sent" +msgstr "" + +#: bal/gui/qt/theme.py:117 +msgid "No will-executor" +msgstr "" + +#: bal/gui/qt/widgets.py:108 +msgid "Mining fee rate in sat/vByte used for the will transactions" +msgstr "" + +#: bal/gui/qt/widgets.py:123 +msgid "" +"mining fees expressed in sats/vbyte to be used in the Bitcoin transaction.\n" +"Higher value ensure your transaction will be confirmed" +msgstr "" + +#: bal/gui/qt/widgets.py:127 +msgid "Miner fee, click for more information" +msgstr "" + +#: bal/gui/qt/widgets.py:213 +msgid "" +"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" +msgstr "" + +#: bal/gui/qt/widgets.py:234 +msgid "Raw" +msgstr "" + +#: bal/gui/qt/widgets.py:234 +msgid "Date" +msgstr "" + +#: bal/gui/qt/widgets.py:633 +msgid "<b>CHECK ALIVE</b><br><br>In DATA mode:<br>set the date for the “check alive” parameter.<br>When you open the wallet, if the “check alive” date has passed, the plugin will ask if you want to postpone the inheritance, since it assumes you still have control of the wallet and that you are still alive.<br><br>In RAW mode:<br>When less than this time is missing, ask to invalidate.<br>If you fail to invalidate during this time, your transactions will be delivered to your heirs.<br><br>if you choose Raw, you can insert various options based on suffix:<br> - d: number of days after current day(ex: 1d means tomorrow)<br> - y: number of years after current day(ex: 1y means one year from today)<br><br><b>Note:</b> the date/time is expressed in your computer's <b>Local time</b> (not UTC time).<br>" +msgstr "" + +#: bal/gui/qt/widgets.py:651 +msgid "Check Alive" +msgstr "" + +#: bal/gui/qt/widgets.py:667 +msgid "<b>DELIVERY TIME</b><br><br>Set Locktime for transactions.<br>Any time is needed transaction will be anticipated by 1day<br><br>(ONLY IN ADVANCED MODE)<br>if you choose Raw, you can insert various options based on suffix:<br> - d: number of days after current day(ex: 1d means tomorrow)<br> - y: number of years after currrent day(ex: 1y means one year from today)<br><br><b>Note:</b> the date/time is expressed in your computer's <b>Local time</b> (not UTC time).<br>" +msgstr "" + +#: bal/gui/qt/widgets.py:684 +msgid "Delivery Time, click for more information" +msgstr "" + +#: bal/gui/qt/widgets.py:723 +msgid "Export reminder dates to your calendar (.ics)" +msgstr "" + +#: bal/gui/qt/widgets.py:835 +msgid "" +"Enter the date on which you want the inheritance (or backup)\n" +"of your Electrum wallet to take effect." +msgstr "" + +#: bal/gui/qt/widgets.py:866 +msgid "" +"Please note: Do not reduce the miner fees\n" +"unless you know what you’re doing" +msgstr "" + +#: bal/gui/qt/widgets.py:1210 +#, python-brace-format +msgid "{} or perc value" +msgstr "" + +#: bal/gui/qt/widgets.py:1333 +msgid "Creation Time" +msgstr "" + +#: bal/gui/qt/widgets.py:1343 +msgid "Transaction fees" +msgstr "" + +#: bal/gui/qt/widgets.py:1353 +msgid "Heirs:" +msgstr "" + +#: bal/gui/qt/widgets.py:1381 +msgid "Willexecutor:" +msgstr "" + +#: bal/gui/qt/window.py:176 +msgid "&Heirs" +msgstr "" + +#: bal/gui/qt/window.py:182 +msgid "&Will" +msgstr "" + +#: bal/gui/qt/window.py:186 +msgid "&Will-Executors" +msgstr "" + +#: bal/gui/qt/window.py:242 +msgid "Please restart Electrum to activate the BAL plugin" +msgstr "" + +#: bal/gui/qt/window.py:286 +msgid "New heir" +msgstr "" + +#: bal/gui/qt/window.py:308 +msgid "OP_RETURN Message" +msgstr "" + +#: bal/gui/qt/window.py:359 +msgid "Add another heir" +msgstr "" + +#: bal/gui/qt/window.py:370 +msgid "Unique name or description about heir" +msgstr "" + +#: bal/gui/qt/window.py:374 +msgid "Bitcoin address or OP_RETURN: prefix + hex data" +msgstr "" + +#: bal/gui/qt/window.py:378 +msgid "Fixed or Percentage amount if end with %" +msgstr "" + +#: bal/gui/qt/window.py:388 +msgid "locktime" +msgstr "" + +#: bal/gui/qt/window.py:441 +msgid "heirs" +msgstr "" + +#: bal/gui/qt/window.py:717 +msgid "Heirs are not defined" +msgstr "" + +#: bal/gui/qt/window.py:832 +#, python-brace-format +msgid "In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts.{}" +msgstr "" + +#: bal/gui/qt/window.py:843 +msgid "CheckAlive is in the past please update it to a date in the future but less than locktime" +msgstr "" + +#: bal/gui/qt/window.py:848 +msgid "locktime is lower than threshold" +msgstr "" + +#: bal/gui/qt/window.py:862 +msgid " no backup transaction or willexecutor selected" +msgstr "" + +#: bal/gui/qt/window.py:902 +msgid "" +"This inheritance was already signed/sent to will-executors and you are postponing it.\n" +"\n" +"The previously committed coins must be invalidated on-chain FIRST, otherwise a will-executor could broadcast the old (earlier) transaction and execute the inheritance too early.\n" +"\n" +"Please sign and broadcast the invalidation transaction now, then press 'Prepare' again to create the new (postponed) inheritance." +msgstr "" + +#: bal/gui/qt/window.py:923 +msgid "Heirs changed:" +msgstr "" + +#: bal/gui/qt/window.py:925 +msgid "Will-Executor not present:" +msgstr "" + +#: bal/gui/qt/window.py:927 +msgid "Will-Executor changed" +msgstr "" + +#: bal/gui/qt/window.py:929 +msgid "Txfees are changed" +msgstr "" + +#: bal/gui/qt/window.py:937 +msgid "" +"Found CHANGES to the DATE or the HEIRS,\n" +"a NEW WILL must be prepared." +msgstr "" + +#: bal/gui/qt/window.py:943 +msgid "will have to be built" +msgstr "" + +#: bal/gui/qt/window.py:963 +#, python-brace-format +msgid "" +"Error:{}\n" +" {}" +msgstr "" + +#: bal/gui/qt/window.py:965 +msgid "Please, check your heirs, locktime and threshold!" +msgstr "" + +#: bal/gui/qt/window.py:980 +msgid "" +"Your inheritance has been rebuilt and now needs to be signed again.\n" +"\n" +"Next step (manual):\n" +" 1. Press 'Sign' to sign the new transaction." +msgstr "" + +#: bal/gui/qt/window.py:987 +msgid "" +"Your inheritance has been rebuilt and now needs to be signed and re-sent to the will-executors.\n" +"\n" +"Next steps (manual):\n" +" 1. Press 'Sign' to sign the new transaction.\n" +" 2. Press 'Broadcast' to send it to the will-executors." +msgstr "" + +#: bal/gui/qt/window.py:1024 +msgid "Electrum was unable to deserialize the transaction:" +msgstr "" + +#: bal/gui/qt/window.py:1038 +msgid "no tx" +msgstr "" + +#: bal/gui/qt/window.py:1053 +msgid "Please sign and broadcast this transaction to invalidate current will" +msgstr "" + +#: bal/gui/qt/window.py:1060 +msgid "No transactions to invalidate" +msgstr "" + +#: bal/gui/qt/window.py:1077 +msgid "Calculating Transactions" +msgstr "" + +#: bal/gui/qt/window.py:1093 +#, python-brace-format +msgid "signed: {}" +msgstr "" + +#: bal/gui/qt/window.py:1094 +#, python-brace-format +msgid "signing: {}" +msgstr "" + +#: bal/gui/qt/window.py:1564 +msgid "Signing transactions..." +msgstr "" + +#: bal/gui/qt/window.py:1578 +msgid "Some transaction was not broadcasted" +msgstr "" + +#: bal/gui/qt/window.py:1582 +msgid "All transactions are broadcasted to respective Will-Executors" +msgstr "" + +#: bal/gui/qt/window.py:1601 +msgid "Selecting Will-Executors" +msgstr "" + +#: bal/gui/qt/window.py:1616 +msgid "Broadcasting Transactions to Will-Executors:" +msgstr "" + +#: bal/gui/qt/window.py:1623 bal/gui/qt/window.py:2299 +msgid "waiting..." +msgstr "" + +#: bal/gui/qt/window.py:1639 +msgid "checking..." +msgstr "" + +#: bal/gui/qt/window.py:1670 +#, python-brace-format +msgid "checking {} - {} : {}" +msgstr "" + +#: bal/gui/qt/window.py:1679 +#, python-brace-format +msgid "checked {} - {} : {}" +msgstr "" + +#: bal/gui/qt/window.py:1875 bal/gui/qt/window.py:1913 +#: bal/gui/qt/window.py:1937 +#, python-brace-format +msgid "Invalid will file: {}" +msgstr "" + +#: bal/gui/qt/window.py:1894 bal/gui/qt/window.py:1945 +msgid "will" +msgstr "" + +#: bal/gui/qt/window.py:1990 +msgid "Checking transactions" +msgstr "" + +#: bal/gui/qt/window.py:2039 +msgid "Check Transaction" +msgstr "" + +#: bal/gui/qt/window.py:2149 +msgid "" +"Could not download the will-executors list.\n" +"\n" +"This is usually caused by your internet connection or a firewall, not by the plugin. Please check your connection (a VPN often helps) and try again." +msgstr "" + +#: bal/gui/qt/window.py:2159 +msgid "" +"Could not download the will-executors list over Tor.\n" +"\n" +"Electrum is connected through Tor and the connection is taking too long. Your Tor connection may be slow. Please try again, or use a VPN (or temporarily disable Tor) for a faster connection." +msgstr "" + +#: bal/gui/qt/window.py:2169 +msgid "Downloading will-executors list..." +msgstr "" + +#: bal/gui/qt/window.py:2231 +#, python-brace-format +msgid "" +"No active will-executor servers found for the {chain} network.\n" +"\n" +"The welist server at {url} responded but returned no will-executors for this chain." +msgstr "" + +#: bal/gui/qt/window.py:2239 +#, python-brace-format +msgid "" +"Could not reach the configured welist server.\n" +"\n" +"Server: {url}\n" +"Error: {reason}\n" +"\n" +"Please verify the welist server URL in the plugin settings." +msgstr "" + +#: bal/gui/qt/window.py:2249 +#, python-brace-format +msgid "No active will-executor found for the {chain} network." +msgstr "" + +#: bal/gui/qt/window.py:2285 +msgid "Ping Will-Executors:" +msgstr "" + +#: bal/gui/qt/window.py:2345 +msgid "Ping Will-Executors" +msgstr "" + diff --git a/bal/locale/it_IT/LC_MESSAGES/bal.po b/bal/locale/it_IT/LC_MESSAGES/bal.po new file mode 100644 index 0000000..b863b1a --- /dev/null +++ b/bal/locale/it_IT/LC_MESSAGES/bal.po @@ -0,0 +1,2082 @@ +# Italian (Italy) translations for BAL Electrum plugin. +# Copyright (C) 2026 Bitcoin After Life +# This file is distributed under the same license as the BAL Electrum plugin project. +# FIRST AUTHOR <EMAIL@ADDRESS>, 2026. +# +msgid "" +msgstr "" +"Project-Id-Version: BAL Electrum plugin VERSION\n" +"Report-Msgid-Bugs-To: info@bitcoin-after.life\n" +"POT-Creation-Date: 2026-09-26 22:02+0200\n" +"PO-Revision-Date: 2026-09-26 22:02+0200\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language: it_IT\n" +"Language-Team: it_IT <LL@li.org>\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.18.0\n" + +#: bal/core/plugin_base.py:361 +msgid "" +"BAL will execution of $wallet_name\r\n" +" heirs list: \r\n" +"$heirs_complete" +msgstr "" +"BAL: esecuzione del testamento di $wallet_name\r\n" +" elenco eredi: \r\n" +"$heirs_complete" + +#: bal/core/plugin_base.py:367 +msgid "BAL -Will execution of $wallet_name" +msgstr "BAL - Esecuzione del testamento di $wallet_name" + +#: bal/core/will.py:1298 +#, python-brace-format +msgid "NOT {}" +msgstr "NON {}" + +#: bal/core/will.py:1322 +msgid "Anticipated" +msgstr "Anticipato" + +#: bal/core/will.py:1323 +msgid "Broadcasted" +msgstr "Trasmesso" + +#: bal/core/will.py:1324 +msgid "Checked" +msgstr "Verificato" + +#: bal/core/will.py:1325 +msgid "Check Failed" +msgstr "Verifica fallita" + +#. from Electrum it_IT +#: bal/core/will.py:1326 +msgid "Signed" +msgstr "Firmato" + +#: bal/core/will.py:1327 +msgid "Confirmed" +msgstr "Confermato" + +#. from Electrum it_IT +#: bal/core/will.py:1328 bal/gui/qt/lists.py:1354 bal/gui/qt/lists.py:1368 +msgid "Error" +msgstr "Errore" + +#. from Electrum it_IT +#: bal/core/will.py:1329 bal/gui/qt/dialogs.py:841 +msgid "Expired" +msgstr "Scaduto" + +#: bal/core/will.py:1330 +msgid "Exported" +msgstr "Esportato" + +#: bal/core/will.py:1331 +msgid "Imported" +msgstr "Importato" + +#: bal/core/will.py:1332 +msgid "Invalidated" +msgstr "Invalidato" + +#. from Electrum it_IT +#: bal/core/will.py:1333 +msgid "Mempool" +msgstr "Mempool" + +#: bal/core/will.py:1334 +msgid "Push failed" +msgstr "Invio fallito" + +#: bal/core/will.py:1335 +msgid "Pushed" +msgstr "Inviato" + +#: bal/core/will.py:1336 +msgid "Partially Signed" +msgstr "Parzialmente firmato" + +#: bal/core/will.py:1337 +msgid "Replaced" +msgstr "Sostituito" + +#: bal/core/will.py:1338 +msgid "Restored" +msgstr "Ripristinato" + +#: bal/core/will.py:1339 +msgid "Updated" +msgstr "Aggiornato" + +#: bal/core/will.py:1340 bal/gui/qt/dialogs.py:2631 +msgid "Valid" +msgstr "Valido" + +#. from Electrum it_IT +#: bal/core/will.py:1346 bal/gui/qt/window.py:551 +msgid "New" +msgstr "Nuovo" + +#: bal/core/willexecutors.py:245 +msgid "Waiting..." +msgstr "In attesa..." + +#. from Electrum it_IT +#: bal/core/willexecutors.py:379 bal/gui/qt/window.py:243 +#: bal/gui/qt/window.py:1643 +msgid "Success" +msgstr "Riuscito" + +#. from Electrum it_IT +#: bal/core/willexecutors.py:389 bal/gui/qt/window.py:1648 +msgid "Failed" +msgstr "Fallito" + +#: bal/gui/qt/calendar.py:41 +msgid "Open" +msgstr "Apri" + +#: bal/gui/qt/calendar.py:44 +msgid "Open with..." +msgstr "Apri con..." + +#. from Electrum it_IT +#: bal/gui/qt/calendar.py:47 +msgid "Save" +msgstr "Salva" + +#: bal/gui/qt/calendar.py:73 bal/gui/qt/widgets.py:1024 +msgid "No reminders were saved: the delivery date is too close (or already passed)" +msgstr "Nessun promemoria salvato: la data di consegna è troppo vicina (o già passata)" + +#: bal/gui/qt/calendar.py:105 bal/gui/qt/calendar.py:126 +#, python-brace-format +msgid "Could not open the calendar file: {}" +msgstr "Impossibile aprire il file del calendario: {}" + +#: bal/gui/qt/calendar.py:115 +msgid "Open calendar with..." +msgstr "Apri il calendario con..." + +#: bal/gui/qt/calendar.py:116 +msgid "Enter the application command:" +msgstr "Inserisci il comando dell'applicazione:" + +#: bal/gui/qt/calendar.py:138 bal/gui/qt/widgets.py:1042 +msgid "Save calendar reminder (.ics)" +msgstr "Salva il promemoria del calendario (.ics)" + +#: bal/gui/qt/calendar.py:150 bal/gui/qt/widgets.py:1062 +#, python-brace-format +msgid "" +"Calendar file saved to:\n" +"{}" +msgstr "" +"File del calendario salvato in:\n" +"{}" + +#: bal/gui/qt/calendar.py:155 bal/gui/qt/widgets.py:1058 +#, python-brace-format +msgid "Could not save the calendar file: {}" +msgstr "Impossibile salvare il file del calendario: {}" + +#: bal/gui/qt/common.py:172 +msgid "Small - ~150 bytes/QR (low-res cameras)" +msgstr "Piccolo - ~150 byte/QR (fotocamere a bassa risoluzione)" + +#: bal/gui/qt/common.py:173 +msgid "Medium - ~400 bytes/QR" +msgstr "Medio - ~400 byte/QR" + +#: bal/gui/qt/common.py:174 +msgid "Large - ~900 bytes/QR" +msgstr "Grande - ~900 byte/QR" + +#: bal/gui/qt/common.py:175 +msgid "XL - ~1800 bytes/QR (high-res cameras)" +msgstr "XL - ~1800 byte/QR (fotocamere ad alta risoluzione)" + +#. from Electrum it_IT +#: bal/gui/qt/common.py:256 +#, python-brace-format +msgid "Select file to save your {}" +msgstr "Seleziona file per salvare {}" + +#. from Electrum it_IT +#: bal/gui/qt/common.py:271 +#, python-brace-format +msgid "Your {0} were exported to '{1}'" +msgstr "Il tuo {0} è stato esportato a '{1}'" + +#: bal/gui/qt/dialogs.py:212 +msgid "Bal Wizard Setup" +msgstr "Bal - Configurazione guidata" + +#: bal/gui/qt/dialogs.py:349 bal/gui/qt/dialogs.py:2760 +msgid "Previous" +msgstr "Indietro" + +#. from Electrum it_IT +#: bal/gui/qt/dialogs.py:353 bal/gui/qt/dialogs.py:2763 +msgid "Next" +msgstr "Avanti" + +#. from Electrum it_IT +#: bal/gui/qt/dialogs.py:357 bal/gui/qt/dialogs.py:4077 +msgid "Cancel" +msgstr "Annulla" + +#: bal/gui/qt/dialogs.py:383 +msgid "Bitcoin After Life Heirs" +msgstr "Bitcoin After Life - Eredi" + +#: bal/gui/qt/dialogs.py:385 +#, python-format +msgid "" +"Please add your heirs\n" +" remember that 100% of wallet balance will be spent" +msgstr "" +"Aggiungi i tuoi eredi\n" +" ricorda che verrà speso il 100% del saldo del portafogli" + +#: bal/gui/qt/dialogs.py:393 bal/gui/qt/lists.py:1229 +msgid "Add" +msgstr "Aggiungi" + +#. from Electrum it_IT +#: bal/gui/qt/dialogs.py:395 bal/gui/qt/dialogs.py:2592 bal/gui/qt/lists.py:311 +#: bal/gui/qt/lists.py:673 bal/gui/qt/lists.py:1237 +msgid "Import" +msgstr "Importa" + +#. from Electrum it_IT +#: bal/gui/qt/dialogs.py:397 bal/gui/qt/dialogs.py:2428 +#: bal/gui/qt/dialogs.py:2596 bal/gui/qt/lists.py:312 bal/gui/qt/lists.py:672 +#: bal/gui/qt/lists.py:1249 +msgid "Export" +msgstr "Esporta" + +#: bal/gui/qt/dialogs.py:422 bal/gui/qt/dialogs.py:530 +msgid "Bitcoin After Life Will-Executors" +msgstr "Bitcoin After Life - Will-Executor" + +#: bal/gui/qt/dialogs.py:423 +msgid "Choose willexecutors download method" +msgstr "Scegli come scaricare i Will-Executor" + +#: bal/gui/qt/dialogs.py:430 +msgid "Automatically download and select willexecutors" +msgstr "Scarica e seleziona automaticamente i Will-Executor" + +#: bal/gui/qt/dialogs.py:431 +msgid "Only download willexecutors list" +msgstr "Scarica solo l'elenco dei Will-Executor" + +#: bal/gui/qt/dialogs.py:432 +msgid "Import willexecutor list from file" +msgstr "Importa l'elenco dei Will-Executor da file" + +#: bal/gui/qt/dialogs.py:433 +msgid "Manual" +msgstr "Manuale" + +#: bal/gui/qt/dialogs.py:462 bal/gui/qt/lists.py:1559 +msgid "willexecutors" +msgstr "Will-Executor" + +#: bal/gui/qt/dialogs.py:531 +msgid "Configure and select your willexecutors" +msgstr "Configura e seleziona i tuoi Will-Executor" + +#: bal/gui/qt/dialogs.py:550 +msgid "Bitcoin After Life Will Settings" +msgstr "Bitcoin After Life - Impostazioni del testamento" + +#. from Electrum it_IT +#: bal/gui/qt/dialogs.py:585 bal/gui/qt/dialogs.py:638 +msgid "Please wait" +msgstr "Rimani in attesa" + +#: bal/gui/qt/dialogs.py:668 +msgid "Building Will" +msgstr "Preparazione del testamento" + +#: bal/gui/qt/dialogs.py:673 +msgid "Building Will:" +msgstr "Preparazione del testamento:" + +#: bal/gui/qt/dialogs.py:754 bal/gui/qt/dialogs.py:772 +#: bal/gui/qt/dialogs.py:782 bal/gui/qt/dialogs.py:805 +msgid "Checking variables" +msgstr "Controllo dei parametri" + +#: bal/gui/qt/dialogs.py:773 +msgid "Check Alive Threshold Passed: you have to Invalidate your old Will" +msgstr "La data della verifica se sei vivo è passata: devi invalidare il piano del vecchio testamento" + +#: bal/gui/qt/dialogs.py:782 bal/gui/qt/dialogs.py:864 +msgid "No Heirs" +msgstr "Nessun erede" + +#. from Electrum it_IT +#: bal/gui/qt/dialogs.py:805 bal/gui/qt/dialogs.py:1351 +#: bal/gui/qt/dialogs.py:2280 bal/gui/qt/window.py:2295 +msgid "Ok" +msgstr "OK" + +#: bal/gui/qt/dialogs.py:810 +msgid "" +"In the inheritance process, the entire wallet will always be fully emptied. \n" +"Your settings require an adjustment of the amounts" +msgstr "" +"Nel processo di eredità il portafogli viene sempre svuotato del tutto. \n" +"Le tue impostazioni richiedono una correzione degli importi" + +#: bal/gui/qt/dialogs.py:818 bal/gui/qt/window.py:837 +#, python-brace-format +msgid "Will-executor fee too high: {}" +msgstr "Commissione del Will-Executor troppo alta: {}" + +#: bal/gui/qt/dialogs.py:852 +msgid "Postponed: invalidating old will" +msgstr "Posticipato: invalidazione del piano del vecchio testamento" + +#: bal/gui/qt/dialogs.py:887 bal/gui/qt/dialogs.py:896 +#: bal/gui/qt/dialogs.py:2240 +msgid "Checking your will" +msgstr "Controllo del testamento" + +#: bal/gui/qt/dialogs.py:889 +msgid "An inheritance of this wallet is already executed (on blockchain)" +msgstr "Un'eredità di questo portafogli è già stata eseguita (sulla blockchain)" + +#: bal/gui/qt/dialogs.py:898 +msgid "Inheritance in mempool (waiting confirmation)" +msgstr "Eredità nella mempool (in attesa di conferma)" + +#: bal/gui/qt/dialogs.py:931 +msgid "Will-Executor excluded" +msgstr "Will-Executor escluso" + +#: bal/gui/qt/dialogs.py:931 bal/gui/qt/dialogs.py:1025 +#: bal/gui/qt/dialogs.py:2221 bal/gui/qt/dialogs.py:2232 +msgid "Skipped" +msgstr "Saltato" + +#: bal/gui/qt/dialogs.py:937 bal/gui/qt/dialogs.py:1709 bal/gui/qt/lists.py:345 +#: bal/gui/qt/theme.py:116 +msgid "Will-Executor" +msgstr "Will-Executor" + +#: bal/gui/qt/dialogs.py:938 +msgid "Not present - select one or enable backup mode" +msgstr "Assente - selezionane uno o attiva la modalità 'transazione senza Will-Executor'" + +#: bal/gui/qt/dialogs.py:1000 +msgid "All heirs' shares are below the dust limit: the inheritance cannot be created. Increase the amounts or reduce the number of heirs." +msgstr "Le quote di tutti gli eredi sono sotto il limite minimo (dust): l'eredità non può essere creata. Aumenta gli importi o riduci il numero di eredi." + +#: bal/gui/qt/dialogs.py:1016 +#, python-brace-format +msgid "Wallet balance is too low: {} satoshi available, but the miner and will-executor fees need {} satoshi (the minimum usable amount is {} satoshi). Add funds, or select fewer will-executors." +msgstr "Il saldo del portafogli è troppo basso: {} satoshi disponibili, ma le commissioni dei miner e dei Will-Executor richiedono {} satoshi (l'importo minimo utilizzabile è {} satoshi). Aggiungi fondi, oppure seleziona meno Will-Executor." + +#: bal/gui/qt/dialogs.py:1059 +#, python-brace-format +msgid "Heir {}" +msgstr "Erede {}" + +#: bal/gui/qt/dialogs.py:1061 +#, python-brace-format +msgid "{} is DUST - excluded (amount below dust limit)" +msgstr "{} è DUST - escluso (importo sotto il limite minimo)" + +#: bal/gui/qt/dialogs.py:1117 +msgid "Heir" +msgstr "Erede" + +#: bal/gui/qt/dialogs.py:1233 bal/gui/qt/dialogs.py:2269 +#, python-brace-format +msgid "Please wait {}secs" +msgstr "Attendi {} secondi" + +#. from Electrum it_IT +#: bal/gui/qt/dialogs.py:1240 bal/gui/qt/dialogs.py:1292 +#: bal/gui/qt/dialogs.py:1330 +msgid "Broadcasting" +msgstr "Trasmissione" + +#: bal/gui/qt/dialogs.py:1351 bal/gui/qt/window.py:2297 +msgid "Ko" +msgstr "Ko" + +#: bal/gui/qt/dialogs.py:1362 +msgid "Timeout - no answer" +msgstr "Tempo scaduto - nessuna risposta" + +#: bal/gui/qt/dialogs.py:1404 +#, python-brace-format +msgid "checking {} - {}" +msgstr "controllo {} - {}" + +#: bal/gui/qt/dialogs.py:1407 bal/gui/qt/dialogs.py:2238 +#: bal/gui/qt/window.py:1671 +msgid "Waiting" +msgstr "In attesa" + +#: bal/gui/qt/dialogs.py:1421 +#, python-brace-format +msgid "checked {} - {}" +msgstr "controllato {} - {}" + +#: bal/gui/qt/dialogs.py:1471 bal/gui/qt/dialogs.py:2024 +#: bal/gui/qt/dialogs.py:2035 +#, python-brace-format +msgid "Error: {}" +msgstr "Errore: {}" + +#: bal/gui/qt/dialogs.py:1533 +msgid "" +"Your will has expired and must be invalidated before it can be rebuilt.\n" +"A transaction window will now open:\n" +"please SIGN and then BROADCAST it to invalidate your old will.\n" +"After the invalidation is confirmed, press the Check button to finish the will." +msgstr "" +"Il tuo testamento è scaduto e bisogna invalidare il piano prima di poterlo ricreare.\n" +"Ora si aprirà la finestra di una transazione:\n" +"FIRMALA e poi TRASMETTILA per invalidare il piano del vecchio testamento.\n" +"Quando l'invalidazione del piano è confermata, premi il pulsante Controlla per completare il testamento." + +#: bal/gui/qt/dialogs.py:1573 +msgid "" +"Your old will has been invalidated and the transaction was broadcast.\n" +"Electrum may need a little time to register it.\n" +"Please wait until the invalidation transaction is confirmed, then press the Check button again to finish updating your will." +msgstr "" +"Il piano del vecchio testamento è stato invalidato e la transazione è stata trasmessa.\n" +"Electrum potrebbe avere bisogno di un po' di tempo per registrarla.\n" +"Attendi che la transazione di invalidazione sia confermata, poi premi di nuovo il pulsante Controlla per completare l'aggiornamento del testamento." + +#: bal/gui/qt/dialogs.py:1588 +msgid "Invalidate your old will" +msgstr "Invalida il piano del vecchio testamento" + +#: bal/gui/qt/dialogs.py:1602 bal/gui/qt/dialogs.py:1647 +msgid "Aborted" +msgstr "Interrotto" + +#: bal/gui/qt/dialogs.py:1621 +msgid "Auto-sign disabled — sign manually" +msgstr "Firma automatica disattivata — firma manualmente" + +#: bal/gui/qt/dialogs.py:1635 +msgid "" +"The delivery date was automatically moved one day earlier so the updated will can correctly replace the previous one.\n" +"Please sign (and broadcast) to confirm the change." +msgstr "" +"La data di consegna è stata anticipata automaticamente di un giorno, così il testamento aggiornato può sostituire correttamente il precedente.\n" +"Firma (e trasmetti il piano) per confermare la modifica." + +#: bal/gui/qt/dialogs.py:1644 +msgid "Sign your will" +msgstr "Firma il testamento" + +#: bal/gui/qt/dialogs.py:1649 bal/gui/qt/dialogs.py:1927 +msgid "Nothing to do" +msgstr "Niente da fare" + +#: bal/gui/qt/dialogs.py:1668 +msgid "Finished" +msgstr "Finito" + +#: bal/gui/qt/dialogs.py:1685 +msgid "Calendar" +msgstr "Calendario" + +#. from Electrum it_IT +#: bal/gui/qt/dialogs.py:1687 bal/gui/qt/dialogs.py:1717 +#: bal/gui/qt/dialogs.py:1784 bal/gui/qt/dialogs.py:2027 +#: bal/gui/qt/dialogs.py:3026 bal/gui/qt/dialogs.py:3265 +#: bal/gui/qt/dialogs.py:4096 +msgid "Close" +msgstr "Chiudi" + +#: bal/gui/qt/dialogs.py:1713 bal/gui/qt/dialogs.py:1780 +msgid "🔮 Wizard" +msgstr "🔮 Procedura guidata" + +#: bal/gui/qt/dialogs.py:1776 bal/gui/qt/dialogs.py:2579 +msgid "Heirs" +msgstr "Eredi" + +#: bal/gui/qt/dialogs.py:1880 bal/gui/qt/widgets.py:1130 +#, python-brace-format +msgid "(reminder {idx}/{total})" +msgstr "(promemoria {idx}/{total})" + +#: bal/gui/qt/dialogs.py:1944 +msgid "All done" +msgstr "Tutto fatto" + +#: bal/gui/qt/dialogs.py:1996 +msgid "Next steps (manual): press 'Sign' to sign your will, then 'Broadcast' to send it to the will-executors." +msgstr "Prossimi passi (manuali): premi 'Firma' per firmare il testamento, poi 'Trasmetti il piano' per inviarlo ai Will-Executor." + +#: bal/gui/qt/dialogs.py:2001 +msgid "Next step (manual): press 'Sign' to sign your will." +msgstr "Prossimo passo (manuale): premi 'Firma' per firmare il testamento." + +#: bal/gui/qt/dialogs.py:2005 +msgid "Next step (manual): press 'Broadcast' to send your will to the will-executors." +msgstr "Prossimo passo (manuale): premi 'Trasmetti il piano' per inviare il testamento ai Will-Executor." + +#: bal/gui/qt/dialogs.py:2100 +#, python-brace-format +msgid "Miner fee rate changed (the will was built with {} sat/byte, now it is {}): a new will must be prepared." +msgstr "La commissione dei miner è cambiata (il testamento è stato creato con {} sat/byte, ora è {}): bisogna preparare un nuovo testamento." + +#: bal/gui/qt/dialogs.py:2105 +msgid "The miner fee rate changed: a new will must be prepared." +msgstr "La commissione dei miner è cambiata: bisogna preparare un nuovo testamento." + +#: bal/gui/qt/dialogs.py:2109 +#, python-brace-format +msgid "Will-executor \"{}\" is not covered by the current will: a new will must be prepared." +msgstr "Il Will-Executor \"{}\" non è compreso nel testamento attuale: bisogna preparare un nuovo testamento." + +#: bal/gui/qt/dialogs.py:2114 +msgid "Backup mode is enabled but the will has no backup transaction: a new will must be prepared." +msgstr "La transazione senza Will-Executor (tx di backup) è attiva ma il testamento non ha la transazione scritta: bisogna preparare un nuovo testamento." + +#: bal/gui/qt/dialogs.py:2123 +#, python-brace-format +msgid "Heir \"{}\" is not covered by the current will (it was added or removed, or its delivery date changed): a new will must be prepared." +msgstr "L'erede \"{}\" non è compreso nel testamento attuale (è stato aggiunto o rimosso, o la sua data di consegna è cambiata): bisogna preparare un nuovo testamento." + +#: bal/gui/qt/dialogs.py:2131 +msgid "The heirs changed: a new will must be prepared." +msgstr "Gli eredi sono cambiati: bisogna preparare un nuovo testamento." + +#: bal/gui/qt/dialogs.py:2133 +msgid "A will-executor changed: a new will must be prepared." +msgstr "Un Will-Executor è cambiato: bisogna preparare un nuovo testamento." + +#: bal/gui/qt/dialogs.py:2143 +msgid "The wallet contains funds that the current will does not cover yet: a new will must be prepared." +msgstr "Il portafogli contiene fondi che il testamento attuale non comprende ancora: bisogna preparare un nuovo testamento." + +#: bal/gui/qt/dialogs.py:2147 +msgid "The will contains no valid transaction: a new will must be prepared." +msgstr "Il testamento non contiene transazioni valide: bisogna preparare un nuovo testamento." + +#: bal/gui/qt/dialogs.py:2153 +msgid "The will is no longer coherent and must be rebuilt; the exact reason could not be determined." +msgstr "Il testamento non è più coerente e va ricreato; non è stato possibile determinare il motivo esatto." + +#: bal/gui/qt/dialogs.py:2183 +msgid "No heirs: add at least one heir before building the will." +msgstr "Nessun erede: aggiungi almeno un erede prima di preparare il testamento." + +#: bal/gui/qt/dialogs.py:2186 +msgid "The wallet has no spendable funds, so no inheritance transaction can be created." +msgstr "Il portafogli non ha fondi spendibili, quindi non si può creare nessuna transazione di eredità." + +#: bal/gui/qt/dialogs.py:2190 +msgid "No usable will-executor: none of the servers in the list is both selected and valid. Open the will-executor settings, select at least one server and check that it is reachable." +msgstr "Nessun Will-Executor utilizzabile: nessun server dell'elenco è sia selezionato sia valido. Apri le impostazioni dei Will-Executor, seleziona almeno un server e controlla che sia raggiungibile." + +#: bal/gui/qt/dialogs.py:2195 +msgid "No delivery date left to build: every heir's date is already covered by the existing will. Choose a later delivery date, or change an heir's date." +msgstr "Nessuna data di consegna da preparare: la data di ogni erede è già compresa nel testamento esistente. Scegli una data di consegna successiva, oppure cambia la data di un erede." + +#: bal/gui/qt/dialogs.py:2200 +msgid "The amount to send must cover the miner fees plus this will-executor's fee, and the wallet balance is not enough: select cheaper will-executors, or add funds to the wallet." +msgstr "L'importo da inviare deve coprire le commissioni dei miner più la commissione di questo Will-Executor, e il saldo del portafogli non basta: seleziona Will-Executor più economici, oppure aggiungi fondi al portafogli." + +#: bal/gui/qt/dialogs.py:2205 +msgid "A will-executor asks for more than the maximum fee you allowed: raise the maximum fee in the settings, or select a cheaper will-executor." +msgstr "Un Will-Executor chiede più della commissione massima che hai permesso: alza la commissione massima nelle impostazioni, oppure seleziona un Will-Executor più economico." + +#: bal/gui/qt/dialogs.py:2210 +msgid "The inheritance transactions could not be assembled from the available funds (the balance may not cover the miner fees)." +msgstr "Non è stato possibile comporre le transazioni di eredità con i fondi disponibili (il saldo potrebbe non coprire le commissioni dei miner)." + +#: bal/gui/qt/dialogs.py:2214 +msgid "An unexpected error stopped the transactions being prepared for a will-executor, so it was skipped. Try again, or select a different will-executor." +msgstr "Un errore imprevisto ha interrotto la preparazione delle transazioni per un Will-Executor, che è stato saltato. Riprova, oppure seleziona un altro Will-Executor." + +#: bal/gui/qt/dialogs.py:2225 +msgid "" +"Could not build the will, and the exact cause could not be determined. Please check that:\n" +"- the wallet balance covers the miner and will-executor fees,\n" +"- each heir's share is above the minimum (dust limit),\n" +"- the Check Alive date is EARLIER than the delivery date,\n" +"- at least one will-executor is selected and reachable." +msgstr "" +"Non è stato possibile preparare il testamento, e non è stato possibile determinare la causa esatta. Controlla che:\n" +"- il saldo del portafogli copra le commissioni dei miner e dei Will-Executor,\n" +"- la quota di ogni erede sia sopra il minimo (limite dust),\n" +"- la data della verifica se sei vivo sia PRECEDENTE alla data di consegna,\n" +"- almeno un Will-Executor sia selezionato e raggiungibile." + +#: bal/gui/qt/dialogs.py:2245 +msgid "Invalidating old will" +msgstr "Invalidazione del piano del vecchio testamento" + +#: bal/gui/qt/dialogs.py:2251 +msgid "Building your will" +msgstr "Preparazione del testamento" + +#: bal/gui/qt/dialogs.py:2257 +msgid "Signing your will" +msgstr "Firma del testamento" + +#: bal/gui/qt/dialogs.py:2263 +msgid "Broadcasting your will to executors" +msgstr "Trasmissione del piano ai Will-Executor" + +#: bal/gui/qt/dialogs.py:2311 +msgid "Wait" +msgstr "Attendi" + +#: bal/gui/qt/dialogs.py:2414 +msgid "Will Details" +msgstr "Dettagli del testamento" + +#. from Electrum it_IT +#: bal/gui/qt/dialogs.py:2420 bal/gui/qt/dialogs.py:4071 +#: bal/gui/qt/lists.py:668 +msgid "Sign" +msgstr "Firma" + +#. from Electrum it_IT +#: bal/gui/qt/dialogs.py:2424 bal/gui/qt/lists.py:675 +msgid "Broadcast" +msgstr "Trasmissione" + +#: bal/gui/qt/dialogs.py:2431 bal/gui/qt/lists.py:677 +msgid "Invalidate" +msgstr "Invalida il piano" + +#: bal/gui/qt/dialogs.py:2436 bal/gui/qt/lists.py:674 +msgid "Merge" +msgstr "Unisci" + +#: bal/gui/qt/dialogs.py:2444 +#, python-brace-format +msgid "Expiration date: {}" +msgstr "Data di scadenza: {}" + +#: bal/gui/qt/dialogs.py:2450 +#, python-brace-format +msgid "Valid Txs:{}" +msgstr "Tx valide: {}" + +#: bal/gui/qt/dialogs.py:2452 +#, python-brace-format +msgid "Total Txs:{}" +msgstr "Tx totali: {}" + +#: bal/gui/qt/dialogs.py:2506 +msgid "Hide replaced" +msgstr "Nascondi sostituite" + +#: bal/gui/qt/dialogs.py:2508 +msgid "Unhide replaced" +msgstr "Mostra sostituite" + +#: bal/gui/qt/dialogs.py:2514 +msgid "Hide invalidated" +msgstr "Nascondi invalidate" + +#: bal/gui/qt/dialogs.py:2516 +msgid "Unhide invalidated" +msgstr "Mostra invalidate" + +#: bal/gui/qt/dialogs.py:2541 +msgid "Will-Executor Service List" +msgstr "Elenco dei servizi Will-Executor" + +#: bal/gui/qt/dialogs.py:2588 bal/gui/qt/lists.py:314 +msgid "New Heir" +msgstr "Nuovo erede" + +#. from Electrum it_IT +#: bal/gui/qt/dialogs.py:2630 +msgid "All" +msgstr "Tutti" + +#: bal/gui/qt/dialogs.py:2633 +msgid "Valid NC" +msgstr "Valido NC" + +#: bal/gui/qt/dialogs.py:2771 bal/gui/qt/dialogs.py:2886 +msgid "Auto" +msgstr "Auto" + +#: bal/gui/qt/dialogs.py:2773 +msgid "Automatically advance through the QR codes." +msgstr "Scorri automaticamente i codici QR." + +#: bal/gui/qt/dialogs.py:2777 +msgid "QR codes per second:" +msgstr "Codici QR al secondo:" + +#: bal/gui/qt/dialogs.py:2781 +msgid " /s" +msgstr " /s" + +#: bal/gui/qt/dialogs.py:2783 +msgid "Loop" +msgstr "Ripeti" + +#: bal/gui/qt/dialogs.py:2786 +msgid "When the last QR code is reached, keep cycling from the first one instead of stopping." +msgstr "Arrivato all'ultimo codice QR, riparti dal primo invece di fermarti." + +#: bal/gui/qt/dialogs.py:2795 +msgid "QR code size:" +msgstr "Dimensione del codice QR:" + +#: bal/gui/qt/dialogs.py:2807 +msgid "Format:" +msgstr "Formato:" + +#: bal/gui/qt/dialogs.py:2813 +msgid "(default)" +msgstr "(predefinito)" + +#: bal/gui/qt/dialogs.py:2880 +msgid "Stop" +msgstr "Ferma" + +#: bal/gui/qt/dialogs.py:2903 +msgid "Proprietary format; every other BAL wallet understands it." +msgstr "Formato proprietario; ogni altro portafogli BAL lo capisce." + +#: bal/gui/qt/dialogs.py:2905 +msgid "Legacy BC-UR v1 (ur:bytes), compatible with older Blockchain Commons tools." +msgstr "BC-UR v1 legacy (ur:bytes), compatibile con i vecchi strumenti Blockchain Commons." + +#: bal/gui/qt/dialogs.py:2907 +msgid "Standard BC-UR v2 fountain codes (ur:bytes)." +msgstr "Codici fountain standard BC-UR v2 (ur:bytes)." + +#: bal/gui/qt/dialogs.py:2908 +msgid "Coinkite BBQR (B$ frames) for BitKit & friends." +msgstr "Coinkite BBQR (frame B$) per BitKit e simili." + +#: bal/gui/qt/dialogs.py:2914 +#, python-brace-format +msgid "" +"Scan the QR codes below, in order, with the will-opening device.\n" +"Frame 1 of {} carries the total number of codes." +msgstr "" +"Scansiona i codici QR qui sotto, in ordine, con il dispositivo che apre il testamento.\n" +"Il frame 1 di {} contiene il numero totale di codici." + +#: bal/gui/qt/dialogs.py:2921 +#, python-brace-format +msgid "" +"Scan the QR codes below with the will-opening device.\n" +"{} codes carry the whole transfer (any order works, duplicates are ignored)." +msgstr "" +"Scansiona i codici QR qui sotto con il dispositivo che apre il testamento.\n" +"{} codici contengono tutto il trasferimento (l'ordine non conta, i doppioni vengono ignorati)." + +#: bal/gui/qt/dialogs.py:2942 +#, python-brace-format +msgid "Frame {} of {}" +msgstr "Frame {} di {}" + +#: bal/gui/qt/dialogs.py:2965 +msgid "Export will" +msgstr "Esporta il testamento" + +#: bal/gui/qt/dialogs.py:2975 +msgid "No will transaction to export." +msgstr "Nessuna transazione del testamento da esportare." + +#: bal/gui/qt/dialogs.py:2982 +msgid "Export:" +msgstr "Esporta:" + +#: bal/gui/qt/dialogs.py:2989 +msgid "Whole will" +msgstr "Testamento completo" + +#: bal/gui/qt/dialogs.py:2993 +msgid "Export the full will items (all details/statuses) as JSON, or only the serialized transactions." +msgstr "Esporta gli elementi completi del testamento (tutti i dettagli e gli stati) in JSON, oppure solo le transazioni serializzate." + +#: bal/gui/qt/dialogs.py:3003 +msgid "Send as:" +msgstr "Invia come:" + +#. from Electrum it_IT +#: bal/gui/qt/dialogs.py:3005 bal/gui/qt/dialogs.py:3242 +msgid "File" +msgstr "File" + +#: bal/gui/qt/dialogs.py:3006 bal/gui/qt/dialogs.py:3243 +msgid "QR Code" +msgstr "Codice QR" + +#: bal/gui/qt/dialogs.py:3007 bal/gui/qt/dialogs.py:3244 +msgid "Audio" +msgstr "Audio" + +#: bal/gui/qt/dialogs.py:3049 +msgid "Export the will as a JSON file. Uncheck \"Whole will\" above to export only the serialized transactions (comma-separated)." +msgstr "Esporta il testamento come file JSON. Togli la spunta a \"Testamento completo\" qui sopra per esportare solo le transazioni serializzate (separate da virgola)." + +#: bal/gui/qt/dialogs.py:3057 +msgid "Export…" +msgstr "Esporta…" + +#: bal/gui/qt/dialogs.py:3074 bal/gui/qt/dialogs.py:3300 +#: bal/gui/qt/window.py:1767 bal/gui/qt/window.py:1780 +msgid "Audio MODEM plugin is not available." +msgstr "Il plugin Audio MODEM non è disponibile." + +#: bal/gui/qt/dialogs.py:3078 bal/gui/qt/dialogs.py:3304 +msgid "Speed (KB/sec):" +msgstr "Velocità (KB/sec):" + +#. from Electrum it_IT +#: bal/gui/qt/dialogs.py:3089 +msgid "Send" +msgstr "Invia" + +#: bal/gui/qt/dialogs.py:3134 bal/gui/qt/dialogs.py:3168 +#: bal/gui/qt/dialogs.py:3193 +msgid "No will transaction matches the selected filter." +msgstr "Nessuna transazione del testamento corrisponde al filtro selezionato." + +#: bal/gui/qt/dialogs.py:3157 +msgid "item(s)" +msgstr "elemento/i" + +#: bal/gui/qt/dialogs.py:3157 +msgid "transaction(s)" +msgstr "transazione/i" + +#: bal/gui/qt/dialogs.py:3159 +#, python-brace-format +msgid "{} {} will be exported." +msgstr "Verranno esportati: {} {}." + +#: bal/gui/qt/dialogs.py:3162 +#, python-brace-format +msgid "{} {} will be sent." +msgstr "Verranno inviati: {} {}." + +#: bal/gui/qt/dialogs.py:3186 bal/gui/qt/dialogs.py:3189 +#: bal/gui/qt/dialogs.py:3340 bal/gui/qt/dialogs.py:3343 +msgid "Invalid audio speed." +msgstr "Velocità audio non valida." + +#: bal/gui/qt/dialogs.py:3225 +msgid "Import will" +msgstr "Importa il testamento" + +#: bal/gui/qt/dialogs.py:3232 +msgid "" +"Choose how the will was exported: from a file, QR codes or audio.\n" +"The import never touches the live will." +msgstr "" +"Scegli come è stato esportato il testamento: da file, codici QR o audio.\n" +"L'importazione non modifica mai il testamento in uso." + +#: bal/gui/qt/dialogs.py:3240 +msgid "Import from:" +msgstr "Importa da:" + +#: bal/gui/qt/dialogs.py:3277 +msgid "Import the JSON will file written by the Export ▶ File option. The will opens in a read-only preview window." +msgstr "Importa il file JSON del testamento creato con l'opzione Esporta ▶ File. Il testamento si apre in una finestra di anteprima in sola lettura." + +#: bal/gui/qt/dialogs.py:3283 +msgid "Choose will file…" +msgstr "Scegli il file del testamento…" + +#: bal/gui/qt/dialogs.py:3313 +msgid "Waiting for the audio transfer…" +msgstr "In attesa del trasferimento audio…" + +#: bal/gui/qt/dialogs.py:3316 +msgid "Receive by audio…" +msgstr "Ricevi via audio…" + +#: bal/gui/qt/dialogs.py:3357 +msgid "Receiving…" +msgstr "Ricezione in corso…" + +#: bal/gui/qt/dialogs.py:3377 +msgid "No transaction data received." +msgstr "Nessun dato di transazione ricevuto." + +#: bal/gui/qt/dialogs.py:3395 +#, python-brace-format +msgid "Waiting for audio ({:.1f} kbps)…" +msgstr "In attesa dell'audio ({:.1f} kbps)…" + +#: bal/gui/qt/dialogs.py:3468 +#, python-brace-format +msgid "Could not parse a transferred will item: {}" +msgstr "Impossibile leggere un elemento del testamento trasferito: {}" + +#: bal/gui/qt/dialogs.py:3488 +#, python-brace-format +msgid "Could not parse a transferred transaction: {}" +msgstr "Impossibile leggere una transazione trasferita: {}" + +#: bal/gui/qt/dialogs.py:3500 +msgid "The imported will contains no valid transaction in this wallet." +msgstr "Il testamento importato non contiene transazioni valide in questo portafogli." + +#: bal/gui/qt/dialogs.py:3511 +#, python-brace-format +msgid "{} imported transaction(s) are not valid in this wallet and were skipped." +msgstr "{} transazione/i importata/e non valide in questo portafogli, quindi saltate." + +#: bal/gui/qt/dialogs.py:3611 +msgid "" +"Show the will QR codes to the camera, one after another.\n" +"After the first code, every new code is captured automatically.\n" +"The first frame sets the total number of codes; duplicates are ignored." +msgstr "" +"Mostra alla fotocamera i codici QR del testamento, uno dopo l'altro.\n" +"Dopo il primo codice, ogni nuovo codice viene acquisito automaticamente.\n" +"Il primo frame indica il numero totale di codici; i doppioni vengono ignorati." + +#: bal/gui/qt/dialogs.py:3620 bal/gui/qt/dialogs.py:3798 +msgid "Waiting for the first frame…" +msgstr "In attesa del primo frame…" + +#: bal/gui/qt/dialogs.py:3637 +msgid "…or paste/type the frame text here" +msgstr "…oppure incolla o scrivi qui il testo del frame" + +#: bal/gui/qt/dialogs.py:3641 +msgid "Add frame" +msgstr "Aggiungi frame" + +#: bal/gui/qt/dialogs.py:3647 bal/gui/qt/dialogs.py:3890 +msgid "Scan with camera…" +msgstr "Scansiona con la fotocamera…" + +#: bal/gui/qt/dialogs.py:3650 +msgid "Start the live camera. While it runs, every new QR code shown to the camera is captured automatically." +msgstr "Avvia la fotocamera. Mentre è attiva, ogni nuovo codice QR mostrato viene acquisito automaticamente." + +#. from Electrum it_IT +#: bal/gui/qt/dialogs.py:3656 +msgid "Reset" +msgstr "Reimposta" + +#: bal/gui/qt/dialogs.py:3663 +msgid "Review and Sign…" +msgstr "Controlla e firma…" + +#: bal/gui/qt/dialogs.py:3683 +#, python-brace-format +msgid "The scanned code belongs to a different transfer ({} frames, {}). The import was reset; scan the first code again." +msgstr "Il codice scansionato appartiene a un altro trasferimento ({} frame, {}). L'importazione è stata azzerata; scansiona di nuovo il primo codice." + +#: bal/gui/qt/dialogs.py:3773 +#, python-brace-format +msgid "All {} frames stored." +msgstr "Tutti i {} frame salvati." + +#: bal/gui/qt/dialogs.py:3779 +#, python-brace-format +msgid "Stored {} of {} frames." +msgstr "Salvati {} frame su {}." + +#. from Electrum it_IT +#: bal/gui/qt/dialogs.py:3844 +msgid "Cannot start QR scanner, no usable camera found." +msgstr "Impossibile avviare lo scanner QR, nessuna fotocamera utilizzabile trovata." + +#: bal/gui/qt/dialogs.py:3849 +msgid "Stop scanning" +msgstr "Ferma la scansione" + +#: bal/gui/qt/dialogs.py:3896 +#, python-brace-format +msgid "Camera error: {}" +msgstr "Errore della fotocamera: {}" + +#: bal/gui/qt/dialogs.py:3943 +#, python-brace-format +msgid "The scanned code belongs to a different transfer ({} frames, {}). The import was reset; show the first code again." +msgstr "Il codice scansionato appartiene a un altro trasferimento ({} frame, {}). L'importazione è stata azzerata; mostra di nuovo il primo codice." + +#: bal/gui/qt/dialogs.py:3986 +msgid "The transferred will contains no transactions." +msgstr "Il testamento trasferito non contiene transazioni." + +#: bal/gui/qt/dialogs.py:4015 +msgid "Review and sign imported will" +msgstr "Controlla e firma il testamento importato" + +#: bal/gui/qt/dialogs.py:4028 +msgid "Enter your wallet password to sign the imported transactions." +msgstr "Inserisci la password del portafogli per firmare le transazioni importate." + +#: bal/gui/qt/dialogs.py:4074 +msgid "Skip" +msgstr "Salta" + +#: bal/gui/qt/dialogs.py:4090 +msgid "Save signed file…" +msgstr "Salva il file firmato…" + +#: bal/gui/qt/dialogs.py:4093 +msgid "Show signed QR…" +msgstr "Mostra i QR firmati…" + +#: bal/gui/qt/dialogs.py:4117 +#, python-brace-format +msgid "Transaction {} of {}" +msgstr "Transazione {} di {}" + +#: bal/gui/qt/dialogs.py:4119 +#, python-brace-format +msgid "TXID: {}" +msgstr "TXID: {}" + +#. from Electrum it_IT +#: bal/gui/qt/dialogs.py:4122 +msgid "unknown" +msgstr "sconosciuto" + +#: bal/gui/qt/dialogs.py:4128 +msgid "(no outputs)" +msgstr "(nessun output)" + +#: bal/gui/qt/dialogs.py:4142 +msgid "unknown (partial transaction)" +msgstr "sconosciuta (transazione parziale)" + +#: bal/gui/qt/dialogs.py:4144 +#, python-brace-format +msgid "" +"Total outputs: {}\n" +"Fee: {}" +msgstr "" +"Totale output: {}\n" +"Commissione: {}" + +#: bal/gui/qt/dialogs.py:4156 +#, python-brace-format +msgid "" +"Signed {} of {} transactions.\n" +"\n" +"Save a signed file to carry to the broadcast device, or show the signed transactions as QR codes." +msgstr "" +"Firmate {} transazioni su {}.\n" +"\n" +"Salva un file firmato da portare sul dispositivo che trasmette il piano, oppure mostra le transazioni firmate come codici QR." + +#: bal/gui/qt/dialogs.py:4174 +#, python-brace-format +msgid "Could not sign the transaction: {}" +msgstr "Impossibile firmare la transazione: {}" + +#: bal/gui/qt/dialogs.py:4193 +msgid "Signed will saved." +msgstr "Testamento firmato salvato." + +#: bal/gui/qt/dialogs.py:4204 +msgid "No signed transaction to show." +msgstr "Nessuna transazione firmata da mostrare." + +#. from Electrum it_IT +#: bal/gui/qt/lists.py:134 bal/gui/qt/window.py:368 +msgid "Name" +msgstr "Nome" + +#. from Electrum it_IT +#: bal/gui/qt/lists.py:135 bal/gui/qt/lists.py:1432 bal/gui/qt/widgets.py:1395 +#: bal/gui/qt/window.py:372 +msgid "Address" +msgstr "Indirizzo" + +#. from Electrum it_IT +#: bal/gui/qt/lists.py:136 bal/gui/qt/window.py:307 +msgid "Amount" +msgstr "Importo" + +#. from Electrum it_IT +#: bal/gui/qt/lists.py:232 +#, python-brace-format +msgid "Copy {}" +msgstr "Copia {}" + +#. from Electrum it_IT +#: bal/gui/qt/lists.py:240 bal/gui/qt/lists.py:1007 +#, python-brace-format +msgid "Edit {}" +msgstr "Modifica {}" + +#. from Electrum it_IT +#: bal/gui/qt/lists.py:243 +msgid "Delete" +msgstr "Elimina" + +#: bal/gui/qt/lists.py:310 +msgid "&New Heir" +msgstr "&Nuovo erede" + +#: bal/gui/qt/lists.py:343 bal/gui/qt/widgets.py:1332 bal/gui/qt/window.py:383 +msgid "Locktime" +msgstr "Locktime" + +#: bal/gui/qt/lists.py:344 +msgid "Txid" +msgstr "Txid" + +#. from Electrum it_IT +#: bal/gui/qt/lists.py:346 bal/gui/qt/widgets.py:1347 +msgid "Status" +msgstr "Stato" + +#. from Electrum it_IT +#: bal/gui/qt/lists.py:347 +msgid "Server" +msgstr "Server" + +#: bal/gui/qt/lists.py:409 +msgid "details" +msgstr "dettagli" + +#: bal/gui/qt/lists.py:413 +msgid "sign" +msgstr "firma" + +#: bal/gui/qt/lists.py:419 +msgid "broadcast" +msgstr "trasmetti il piano" + +#: bal/gui/qt/lists.py:425 +msgid "check " +msgstr "controlla " + +#: bal/gui/qt/lists.py:431 +msgid "copy id" +msgstr "copia id" + +#: bal/gui/qt/lists.py:435 +msgid "copy" +msgstr "copia" + +#: bal/gui/qt/lists.py:439 +msgid "merge from txn" +msgstr "unisci da transazione" + +#: bal/gui/qt/lists.py:445 bal/gui/qt/lists.py:1017 +msgid "delete" +msgstr "elimina" + +#: bal/gui/qt/lists.py:504 +msgid "Transaction IDs" +msgstr "ID delle transazioni" + +#. from Electrum it_IT +#: bal/gui/qt/lists.py:510 +msgid "Transactions" +msgstr "Transazioni" + +#: bal/gui/qt/lists.py:524 +msgid "Open transaction file" +msgstr "Apri il file della transazione" + +#: bal/gui/qt/lists.py:536 +#, python-brace-format +msgid "Invalid transaction file: {}" +msgstr "File della transazione non valido: {}" + +#: bal/gui/qt/lists.py:666 +msgid "Prepare" +msgstr "Prepara" + +#: bal/gui/qt/lists.py:667 +msgid "Display" +msgstr "Mostra" + +#: bal/gui/qt/lists.py:676 +msgid "Check" +msgstr "Controlla" + +#: bal/gui/qt/lists.py:682 +msgid "Build Your Will" +msgstr "Crea il tuo testamento" + +#: bal/gui/qt/lists.py:692 +msgid "Wizard - Build your will" +msgstr "Procedura guidata - Crea il tuo testamento" + +#: bal/gui/qt/lists.py:706 +msgid "Check Inheritance" +msgstr "Controlla l'eredità" + +#: bal/gui/qt/lists.py:911 +msgid "Url" +msgstr "URL" + +#: bal/gui/qt/lists.py:912 +msgid "S" +msgstr "S" + +#: bal/gui/qt/lists.py:913 +msgid "Base fee" +msgstr "Commissione base" + +#. from Electrum it_IT +#: bal/gui/qt/lists.py:914 bal/gui/qt/lists.py:1414 +msgid "Info" +msgstr "Info" + +#: bal/gui/qt/lists.py:915 +msgid "Default Address" +msgstr "Indirizzo predefinito" + +#. from Electrum it_IT +#: bal/gui/qt/lists.py:990 +msgid "Copy" +msgstr "Copia" + +#: bal/gui/qt/lists.py:995 +msgid "deselect" +msgstr "deseleziona" + +#: bal/gui/qt/lists.py:1000 +msgid "select" +msgstr "seleziona" + +#: bal/gui/qt/lists.py:1012 +msgid "Ping" +msgstr "Ping" + +#: bal/gui/qt/lists.py:1216 +msgid "Add transactions without willexecutor" +msgstr "Aggiungi transazioni senza Will-Executor" + +#: bal/gui/qt/lists.py:1233 +msgid "Download List" +msgstr "Scarica l'elenco" + +#: bal/gui/qt/lists.py:1251 +msgid "Export all" +msgstr "Esporta tutto" + +#: bal/gui/qt/lists.py:1253 +msgid "Export selected" +msgstr "Esporta i selezionati" + +#: bal/gui/qt/lists.py:1256 +msgid "Export only valid" +msgstr "Esporta solo i validi" + +#: bal/gui/qt/lists.py:1260 +msgid "Ping All" +msgstr "Ping a tutti" + +#: bal/gui/qt/lists.py:1262 +msgid "Ping all" +msgstr "Ping a tutti" + +#: bal/gui/qt/lists.py:1264 +msgid "Ping selected" +msgstr "Ping ai selezionati" + +#. from Electrum it_IT +#: bal/gui/qt/lists.py:1268 +msgid "Select All" +msgstr "Seleziona Tutto" + +#: bal/gui/qt/lists.py:1270 +msgid "Select all" +msgstr "Seleziona tutti" + +#: bal/gui/qt/lists.py:1272 +msgid "Select only valid" +msgstr "Seleziona solo i validi" + +#: bal/gui/qt/lists.py:1274 +msgid "Deselect all" +msgstr "Deseleziona tutti" + +#: bal/gui/qt/lists.py:1276 +msgid "Deselect only invalid" +msgstr "Deseleziona solo i non validi" + +#: bal/gui/qt/lists.py:1288 bal/gui/qt/window.py:288 +#, python-brace-format +msgid "Edit: {}" +msgstr "Modifica: {}" + +#: bal/gui/qt/lists.py:1290 bal/gui/qt/lists.py:1302 bal/gui/qt/lists.py:1316 +#: bal/gui/qt/lists.py:1476 +msgid "New Will Executor" +msgstr "Nuovo Will-Executor" + +#: bal/gui/qt/lists.py:1323 bal/gui/qt/lists.py:1449 +msgid "URL is required" +msgstr "L'URL è obbligatorio" + +#: bal/gui/qt/lists.py:1355 +#, python-brace-format +msgid "Could not reach server at {}" +msgstr "Impossibile raggiungere il server {}" + +#: bal/gui/qt/lists.py:1369 +#, python-brace-format +msgid "Error contacting server: {}" +msgstr "Errore nel contattare il server: {}" + +#: bal/gui/qt/lists.py:1390 +msgid "Add another" +msgstr "Aggiungi un altro" + +#: bal/gui/qt/lists.py:1403 +msgid "URL" +msgstr "URL" + +#: bal/gui/qt/lists.py:1408 +msgid "Will executor server URL (e.g. http://192.168.1.100:8080)" +msgstr "URL del server del Will-Executor (es. http://192.168.1.100:8080)" + +#: bal/gui/qt/lists.py:1417 +msgid "A short description or name for this executor" +msgstr "Una breve descrizione o un nome per questo Will-Executor" + +#: bal/gui/qt/lists.py:1423 +msgid "Base Fee (sats)" +msgstr "Commissione base (sat)" + +#: bal/gui/qt/lists.py:1426 +msgid "Base fee in satoshis" +msgstr "Commissione base in satoshi" + +#: bal/gui/qt/lists.py:1435 +msgid "Bitcoin address for fee payments (optional)" +msgstr "Indirizzo Bitcoin per il pagamento delle commissioni (facoltativo)" + +#: bal/gui/qt/lists.py:1496 +msgid "No will-executor matches the selected filter" +msgstr "Nessun Will-Executor corrisponde al filtro selezionato" + +#: bal/gui/qt/lists.py:1552 +msgid "No will-executor is selected" +msgstr "Nessun Will-Executor selezionato" + +#. from Electrum it_IT +#: bal/gui/qt/plugin.py:140 +msgid "Electrum Plugins" +msgstr "Plugin Electrum" + +#: bal/gui/qt/plugin.py:307 +msgid "Bitcoin After Life" +msgstr "Bitcoin After Life" + +#. from Electrum it_IT +#: bal/gui/qt/plugin.py:426 bal/gui/qt/plugin.py:451 +msgid "Settings" +msgstr "Impostazioni" + +#: bal/gui/qt/plugin.py:568 +msgid "BASIC" +msgstr "BASE" + +#: bal/gui/qt/plugin.py:568 +msgid "ADVANCED" +msgstr "AVANZATO" + +#: bal/gui/qt/plugin.py:588 +msgid "Enable ADVANCED mode" +msgstr "Attiva la modalità AVANZATA" + +#: bal/gui/qt/plugin.py:589 +msgid "Type 'at My Risk' to enable ADVANCED mode" +msgstr "Scrivi 'at My Risk' per attivare la modalità AVANZATA" + +#: bal/gui/qt/plugin.py:658 +msgid "Reset to default" +msgstr "Ripristina il valore predefinito" + +#: bal/gui/qt/plugin.py:680 +msgid "Rebroadcast transactions" +msgstr "Trasmetti di nuovo il piano" + +#: bal/gui/qt/plugin.py:691 +msgid "Warning: change these settings only if you know what you are doing." +msgstr "Attenzione: cambia queste impostazioni solo se sai cosa stai facendo." + +#: bal/gui/qt/plugin.py:706 +msgid "Hide Replaced" +msgstr "Nascondi sostituite" + +#: bal/gui/qt/plugin.py:709 +msgid "Hide replaced transactions from will detail and list" +msgstr "Nascondi le transazioni sostituite dal dettaglio e dall'elenco del testamento" + +#: bal/gui/qt/plugin.py:714 +msgid "Hide Invalidated" +msgstr "Nascondi invalidate" + +#: bal/gui/qt/plugin.py:717 +msgid "Hide invalidated transactions from will detail and list" +msgstr "Nascondi le transazioni invalidate dal dettaglio e dall'elenco del testamento" + +#: bal/gui/qt/plugin.py:720 +msgid "Auto-sign on Check" +msgstr "Firma automatica al Controllo" + +#: bal/gui/qt/plugin.py:722 +msgid "" +"When checking, automatically sign and broadcast the will transactions to their will-executors.\n" +"The wallet password is requested only if the wallet is encrypted." +msgstr "" +"Durante il controllo, firma automaticamente le transazioni del testamento e trasmetti il piano ai rispettivi Will-Executor.\n" +"La password del portafogli viene chiesta solo se il portafogli è cifrato." + +#: bal/gui/qt/plugin.py:736 +msgid "Panel editable Date and Fee" +msgstr "Data e commissione modificabili nel pannello" + +#: bal/gui/qt/plugin.py:740 +msgid "" +"When enabled, the delivery-time and check-alive date fields can be edited everywhere (toolbar / Heirs tab), not only in the will-building wizard.\n" +"When disabled, those dates are display-only outside the wizard." +msgstr "" +"Se attivo, i campi della data di consegna e della data della verifica se sei vivo si possono modificare ovunque (barra degli strumenti / scheda Eredi), non solo nella procedura guidata del testamento.\n" +"Se disattivo, fuori dalla procedura guidata queste date si possono solo vedere." + +#: bal/gui/qt/plugin.py:751 +msgid "Max Will-Executor Fee (satoshi)" +msgstr "Commissione massima del Will-Executor (satoshi)" + +#: bal/gui/qt/plugin.py:755 +msgid "" +"Maximum fee (in satoshi) allowed to be paid to a single will-executor. If a will-executor charges more than this, the will will not be built.\n" +"Default: 500,000 satoshi (0.005 BTC)." +msgstr "" +"Commissione massima (in satoshi) che si può pagare a un singolo Will-Executor. Se un Will-Executor chiede di più, il testamento non viene preparato.\n" +"Predefinito: 500.000 satoshi (0,005 BTC)." + +#: bal/gui/qt/plugin.py:766 +msgid "User Type" +msgstr "Tipo di utente" + +#: bal/gui/qt/plugin.py:770 +msgid "" +"Choose how much detail the plugin shows.\n" +"\n" +"BASIC: simplified interface, safe configuration for most users.\n" +"\n" +"ADVANCED: shows every control, including the 'Check Alive' field and the Raw/Date selector. Allows configuring the welist server URL and keeps all date/fee fields always editable." +msgstr "" +"Scegli quanti dettagli mostra il plugin.\n" +"\n" +"BASE: interfaccia semplificata, configurazione sicura per la maggior parte degli utenti.\n" +"\n" +"AVANZATO: mostra tutti i controlli, compresi il campo 'Verifica se sei vivo' e il selettore Raw/Data. Permette di configurare l'URL del server welist e lascia sempre modificabili tutti i campi di data e commissione." + +#: bal/gui/qt/plugin.py:783 +msgid "Number of reminders" +msgstr "Numero di promemoria" + +#: bal/gui/qt/plugin.py:785 +msgid "How many reminder alarms the exported calendar (.ics) event contains. Range: 1 to 5 (default 3). Only used in ADVANCED mode." +msgstr "Quanti promemoria contiene l'evento del calendario esportato (.ics). Da 1 a 5 (predefinito 3). Usato solo in modalità AVANZATA." + +#: bal/gui/qt/plugin.py:794 +msgid "Event summary" +msgstr "Titolo dell'evento" + +#: bal/gui/qt/plugin.py:796 +msgid "" +"Default message to be used in event summary\n" +"Variables:\n" +" $wallet_name: name of wallet\n" +" $heirs_complete: list of heirs name,address,amount\n" +"Only used in ADVANCED mode." +msgstr "" +"Messaggio predefinito per il titolo dell'evento\n" +"Variabili:\n" +" $wallet_name: nome del portafogli\n" +" $heirs_complete: elenco degli eredi con nome, indirizzo, importo\n" +"Usato solo in modalità AVANZATA." + +#: bal/gui/qt/plugin.py:808 +msgid "Event description" +msgstr "Descrizione dell'evento" + +#: bal/gui/qt/plugin.py:810 +msgid "" +"Default message to be used in event description\n" +"Variables:\n" +" $wallet_name: name of wallet\n" +" $heirs_complete: list of heirs name,address,amount\n" +"Only used in ADVANCED mode." +msgstr "" +"Messaggio predefinito per la descrizione dell'evento\n" +"Variabili:\n" +" $wallet_name: nome del portafogli\n" +" $heirs_complete: elenco degli eredi con nome, indirizzo, importo\n" +"Usato solo in modalità AVANZATA." + +#: bal/gui/qt/plugin.py:823 +msgid "Welist Server URL" +msgstr "URL del server welist" + +#: bal/gui/qt/plugin.py:825 +msgid "URL of the server that provides the will-executor list. Only available in ADVANCED mode." +msgstr "URL del server che fornisce l'elenco dei Will-Executor. Disponibile solo in modalità AVANZATA." + +#: bal/gui/qt/plugin.py:834 +msgid "Calendar app command" +msgstr "Comando dell'app calendario" + +#: bal/gui/qt/plugin.py:836 +msgid "" +"Command used to open .ics calendar files.\n" +"Leave empty to use the system default (xdg-open/open/start).\n" +"Only used in ADVANCED mode." +msgstr "" +"Comando usato per aprire i file di calendario .ics.\n" +"Lascia vuoto per usare quello predefinito del sistema (xdg-open/open/start).\n" +"Usato solo in modalità AVANZATA." + +#: bal/gui/qt/plugin.py:849 +msgid "Save inheritance transactions in history" +msgstr "Salva le transazioni di eredità nella cronologia" + +#: bal/gui/qt/plugin.py:851 +#, python-brace-format +msgid "" +"After each check, save the valid will transactions into the wallet's local history (the History tab), each with a label.\n" +"The label may contain the variable:\n" +" {willexecutor}: replaced with the will-executor URL of the item\n" +"Only used in ADVANCED mode." +msgstr "" +"Dopo ogni controllo, salva le transazioni valide del testamento nella cronologia locale del portafogli (la scheda Cronologia), ognuna con un'etichetta.\n" +"L'etichetta può contenere la variabile:\n" +" {willexecutor}: sostituita con l'URL del Will-Executor dell'elemento\n" +"Usato solo in modalità AVANZATA." + +#: bal/gui/qt/plugin.py:863 +msgid "History label" +msgstr "Etichetta nella cronologia" + +#: bal/gui/qt/plugin.py:865 +#, python-brace-format +msgid "" +"Label applied to the will transactions saved into the wallet's local history.\n" +"Variables:\n" +" {willexecutor}: replaced with the will-executor URL of the item\n" +"Only used in ADVANCED mode." +msgstr "" +"Etichetta applicata alle transazioni del testamento salvate nella cronologia locale del portafogli.\n" +"Variabili:\n" +" {willexecutor}: sostituita con l'URL del Will-Executor dell'elemento\n" +"Usato solo in modalità AVANZATA." + +#: bal/gui/qt/plugin.py:887 +msgid "Broadcast all transactions to willexecutors including those already pushed" +msgstr "Trasmetti il piano ai Will-Executor con tutte le transazioni, comprese quelle già inviate" + +#: bal/gui/qt/plugin.py:896 +msgid "Rebuild will on wallet close" +msgstr "Ricrea il testamento alla chiusura del portafogli" + +#: bal/gui/qt/plugin.py:898 +msgid "" +"Run the 'Build your will' wizard every time the wallet is closed or Electrum is quit, so the will is rebuilt and re-validated.\n" +"When disabled, the will is only rebuilt when you press Check or Prepare. The last built state is still saved to the wallet." +msgstr "" +"Avvia la procedura 'Crea il tuo testamento' ogni volta che il portafogli viene chiuso o si esce da Electrum, così il testamento viene ricreato e riconvalidato.\n" +"Se disattivo, il testamento viene ricreato solo quando premi Controlla o Prepara. L'ultimo stato preparato viene comunque salvato nel portafogli." + +#: bal/gui/qt/plugin.py:914 +msgid "Rebuild automatically on new transactions" +msgstr "Ricrea automaticamente con nuove transazioni" + +#: bal/gui/qt/plugin.py:916 +msgid "" +"When a new transaction arrives for the wallet, automatically rebuild the will the same way the wizard does at wallet close: the delivery date is anticipated by one day so the new will replaces the previous one, and the rebuilt transactions are signed and sent to their will-executors.\n" +"An on-chain invalidation transaction is only built when the anticipated delivery date would fall before the Check Alive threshold, or when the threshold is already in the past.\n" +"When disabled (default), the will is only rebuilt on Check / Prepare / wallet close." +msgstr "" +"Quando arriva una nuova transazione nel portafogli, ricrea automaticamente il testamento come fa la procedura guidata alla chiusura del portafogli: la data di consegna viene anticipata di un giorno così il nuovo testamento sostituisce il precedente, e le transazioni ricreate vengono firmate e inviate ai rispettivi Will-Executor.\n" +"Una transazione di invalidazione del piano sulla blockchain viene creata solo se la data di consegna anticipata cadrebbe prima della data della verifica se sei vivo, o se quella data è già passata.\n" +"Se disattivo (predefinito), il testamento viene ricreato solo con Controlla / Prepara / chiusura del portafogli." + +#: bal/gui/qt/plugin.py:938 +msgid "QR Code Size" +msgstr "Dimensione del codice QR" + +#: bal/gui/qt/plugin.py:940 +msgid "" +"Payload size of a single QR code when exporting a will via QR.\n" +"\n" +"Larger QR codes hold more data (fewer shots) but are easier to scan with a high-resolution camera; smaller QR codes scan fine even with low-resolution cameras but require more shots.\n" +"The same selector is available inside the export dialog." +msgstr "" +"Quantità di dati in un singolo codice QR quando si esporta un testamento via QR.\n" +"\n" +"I codici QR più grandi contengono più dati (meno scatti) ma si leggono meglio con una fotocamera ad alta risoluzione; quelli più piccoli si leggono bene anche con fotocamere a bassa risoluzione ma richiedono più scatti.\n" +"Lo stesso selettore è disponibile anche nella finestra di esportazione." + +#: bal/gui/qt/plugin.py:1021 +msgid "Reset to Default Setting" +msgstr "Ripristina le impostazioni predefinite" + +#: bal/gui/qt/plugin.py:1022 +msgid "Reset these settings to their default values" +msgstr "Riporta queste impostazioni ai valori predefiniti" + +#: bal/gui/qt/plugin.py:1029 +msgid "Open the Bitcoin After Life support website" +msgstr "Apri il sito di supporto di Bitcoin After Life" + +#: bal/gui/qt/plugin.py:1041 +msgid "Support:" +msgstr "Supporto:" + +#: bal/gui/qt/plugin.py:1098 +#, python-brace-format +msgid "BAL - {}" +msgstr "BAL - {}" + +#: bal/gui/qt/theme.py:93 +msgid "Not on server" +msgstr "Non sul server" + +#: bal/gui/qt/theme.py:95 +msgid "Confirmed on server" +msgstr "Confermato sul server" + +#: bal/gui/qt/theme.py:97 +msgid "Send failed" +msgstr "Invio fallito" + +#: bal/gui/qt/theme.py:99 +msgid "Sent (not checked)" +msgstr "Inviato (non verificato)" + +#: bal/gui/qt/theme.py:101 +msgid "Signed (not sent)" +msgstr "Firmato (non inviato)" + +#: bal/gui/qt/theme.py:102 +msgid "Not sent" +msgstr "Non inviato" + +#: bal/gui/qt/theme.py:117 +msgid "No will-executor" +msgstr "Nessun Will-Executor" + +#: bal/gui/qt/widgets.py:108 +msgid "Mining fee rate in sat/vByte used for the will transactions" +msgstr "Commissione di mining in sat/vByte usata per le transazioni del testamento" + +#: bal/gui/qt/widgets.py:123 +msgid "" +"mining fees expressed in sats/vbyte to be used in the Bitcoin transaction.\n" +"Higher value ensure your transaction will be confirmed" +msgstr "" +"commissioni di mining in sat/vbyte da usare nella transazione Bitcoin.\n" +"Un valore più alto assicura che la transazione venga confermata" + +#: bal/gui/qt/widgets.py:127 +msgid "Miner fee, click for more information" +msgstr "Commissione dei miner, clicca per maggiori informazioni" + +#: bal/gui/qt/widgets.py:213 +msgid "" +"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" +msgstr "" +"se scegli Raw, puoi inserire diverse opzioni in base al suffisso:\n" +" - d: numero di giorni dopo oggi (es.: 1d significa domani)\n" +" - y: numero di anni dopo oggi (es.: 1y significa tra un anno)\n" + +#. from Electrum it_IT +#: bal/gui/qt/widgets.py:234 +msgid "Raw" +msgstr "Raw" + +#. from Electrum it_IT +#: bal/gui/qt/widgets.py:234 +msgid "Date" +msgstr "Data" + +#: bal/gui/qt/widgets.py:633 +msgid "<b>CHECK ALIVE</b><br><br>In DATA mode:<br>set the date for the “check alive” parameter.<br>When you open the wallet, if the “check alive” date has passed, the plugin will ask if you want to postpone the inheritance, since it assumes you still have control of the wallet and that you are still alive.<br><br>In RAW mode:<br>When less than this time is missing, ask to invalidate.<br>If you fail to invalidate during this time, your transactions will be delivered to your heirs.<br><br>if you choose Raw, you can insert various options based on suffix:<br> - d: number of days after current day(ex: 1d means tomorrow)<br> - y: number of years after current day(ex: 1y means one year from today)<br><br><b>Note:</b> the date/time is expressed in your computer's <b>Local time</b> (not UTC time).<br>" +msgstr "<b>VERIFICA SE SEI VIVO</b><br><br>In modalità DATA:<br>imposta la data della “verifica se sei vivo”.<br>Quando apri il portafogli, se quella data è passata, il plugin ti chiede se vuoi posticipare l'eredità, perché presume che tu abbia ancora il controllo del portafogli e che tu sia ancora vivo.<br><br>In modalità RAW:<br>quando manca meno di questo tempo, chiede di invalidare il piano.<br>Se non invalidi il piano entro questo tempo, le tue transazioni verranno consegnate ai tuoi eredi.<br><br>se scegli Raw, puoi inserire diverse opzioni in base al suffisso:<br> - d: numero di giorni dopo oggi (es.: 1d significa domani)<br> - y: numero di anni dopo oggi (es.: 1y significa tra un anno)<br><br><b>Nota:</b> la data e l'ora sono espresse nell'<b>ora locale</b> del tuo computer (non in ora UTC).<br>" + +#: bal/gui/qt/widgets.py:651 +msgid "Check Alive" +msgstr "Verifica se sei vivo" + +#: bal/gui/qt/widgets.py:667 +msgid "<b>DELIVERY TIME</b><br><br>Set Locktime for transactions.<br>Any time is needed transaction will be anticipated by 1day<br><br>(ONLY IN ADVANCED MODE)<br>if you choose Raw, you can insert various options based on suffix:<br> - d: number of days after current day(ex: 1d means tomorrow)<br> - y: number of years after currrent day(ex: 1y means one year from today)<br><br><b>Note:</b> the date/time is expressed in your computer's <b>Local time</b> (not UTC time).<br>" +msgstr "<b>DATA DI CONSEGNA</b><br><br>Imposta il locktime delle transazioni.<br>Ogni volta che serve, la transazione viene anticipata di 1 giorno<br><br>(SOLO IN MODALITÀ AVANZATA)<br>se scegli Raw, puoi inserire diverse opzioni in base al suffisso:<br> - d: numero di giorni dopo oggi (es.: 1d significa domani)<br> - y: numero di anni dopo oggi (es.: 1y significa tra un anno)<br><br><b>Nota:</b> la data e l'ora sono espresse nell'<b>ora locale</b> del tuo computer (non in ora UTC).<br>" + +#: bal/gui/qt/widgets.py:684 +msgid "Delivery Time, click for more information" +msgstr "Data di consegna, clicca per maggiori informazioni" + +#: bal/gui/qt/widgets.py:723 +msgid "Export reminder dates to your calendar (.ics)" +msgstr "Esporta le date dei promemoria nel tuo calendario (.ics)" + +#: bal/gui/qt/widgets.py:835 +msgid "" +"Enter the date on which you want the inheritance (or backup)\n" +"of your Electrum wallet to take effect." +msgstr "" +"Inserisci la data in cui vuoi che l'eredità\n" +"del tuo portafogli Electrum abbia effetto." + +#: bal/gui/qt/widgets.py:866 +msgid "" +"Please note: Do not reduce the miner fees\n" +"unless you know what you’re doing" +msgstr "" +"Attenzione: non ridurre le commissioni dei miner\n" +"se non sai cosa stai facendo" + +#: bal/gui/qt/widgets.py:1210 +#, python-brace-format +msgid "{} or perc value" +msgstr "{} o valore percentuale" + +#: bal/gui/qt/widgets.py:1333 +msgid "Creation Time" +msgstr "Data di creazione" + +#: bal/gui/qt/widgets.py:1343 +msgid "Transaction fees" +msgstr "Commissioni della transazione" + +#: bal/gui/qt/widgets.py:1353 +msgid "Heirs:" +msgstr "Eredi:" + +#: bal/gui/qt/widgets.py:1381 +msgid "Willexecutor:" +msgstr "Will-Executor:" + +#: bal/gui/qt/window.py:176 +msgid "&Heirs" +msgstr "&Eredi" + +#: bal/gui/qt/window.py:182 +msgid "&Will" +msgstr "&Testamento" + +#: bal/gui/qt/window.py:186 +msgid "&Will-Executors" +msgstr "&Will-Executor" + +#: bal/gui/qt/window.py:242 +msgid "Please restart Electrum to activate the BAL plugin" +msgstr "Riavvia Electrum per attivare il plugin BAL" + +#: bal/gui/qt/window.py:286 +msgid "New heir" +msgstr "Nuovo erede" + +#: bal/gui/qt/window.py:308 +msgid "OP_RETURN Message" +msgstr "Messaggio OP_RETURN" + +#: bal/gui/qt/window.py:359 +msgid "Add another heir" +msgstr "Aggiungi un altro erede" + +#: bal/gui/qt/window.py:370 +msgid "Unique name or description about heir" +msgstr "Nome univoco o descrizione dell'erede" + +#: bal/gui/qt/window.py:374 +msgid "Bitcoin address or OP_RETURN: prefix + hex data" +msgstr "Indirizzo Bitcoin oppure prefisso OP_RETURN: + dati esadecimali" + +#: bal/gui/qt/window.py:378 +msgid "Fixed or Percentage amount if end with %" +msgstr "Importo fisso, o percentuale se finisce con %" + +#: bal/gui/qt/window.py:388 +msgid "locktime" +msgstr "locktime" + +#: bal/gui/qt/window.py:441 +msgid "heirs" +msgstr "eredi" + +#: bal/gui/qt/window.py:717 +msgid "Heirs are not defined" +msgstr "Gli eredi non sono definiti" + +#: bal/gui/qt/window.py:832 +#, python-brace-format +msgid "In the inheritance process, the entire wallet will always be fully emptied. Your settings require an adjustment of the amounts.{}" +msgstr "Nel processo di eredità il portafogli viene sempre svuotato del tutto. Le tue impostazioni richiedono una correzione degli importi.{}" + +#: bal/gui/qt/window.py:843 +msgid "CheckAlive is in the past please update it to a date in the future but less than locktime" +msgstr "La data della verifica se sei vivo è nel passato: aggiornala a una data futura ma precedente al locktime" + +#: bal/gui/qt/window.py:848 +msgid "locktime is lower than threshold" +msgstr "il locktime è precedente alla data della verifica se sei vivo" + +#: bal/gui/qt/window.py:862 +msgid " no backup transaction or willexecutor selected" +msgstr " nessuna transazione senza Will-Executor attiva o nessun Will-Executor selezionato" + +#: bal/gui/qt/window.py:902 +msgid "" +"This inheritance was already signed/sent to will-executors and you are postponing it.\n" +"\n" +"The previously committed coins must be invalidated on-chain FIRST, otherwise a will-executor could broadcast the old (earlier) transaction and execute the inheritance too early.\n" +"\n" +"Please sign and broadcast the invalidation transaction now, then press 'Prepare' again to create the new (postponed) inheritance." +msgstr "" +"Questa eredità era già stata firmata e inviata ai Will-Executor e tu la stai posticipando.\n" +"\n" +"PRIMA bisogna invalidare il piano sulla blockchain per le monete già impegnate, altrimenti un Will-Executor potrebbe trasmettere la vecchia transazione (con la data precedente) ed eseguire l'eredità troppo presto.\n" +"\n" +"Firma ora la transazione di invalidazione e trasmetti il piano, poi premi di nuovo 'Prepara' per creare la nuova eredità (posticipata)." + +#: bal/gui/qt/window.py:923 +msgid "Heirs changed:" +msgstr "Eredi cambiati:" + +#: bal/gui/qt/window.py:925 +msgid "Will-Executor not present:" +msgstr "Will-Executor assente:" + +#: bal/gui/qt/window.py:927 +msgid "Will-Executor changed" +msgstr "Will-Executor cambiato" + +#: bal/gui/qt/window.py:929 +msgid "Txfees are changed" +msgstr "Le commissioni sono cambiate" + +#: bal/gui/qt/window.py:937 +msgid "" +"Found CHANGES to the DATE or the HEIRS,\n" +"a NEW WILL must be prepared." +msgstr "" +"Trovate MODIFICHE alla DATA o agli EREDI,\n" +"bisogna preparare un NUOVO TESTAMENTO." + +#: bal/gui/qt/window.py:943 +msgid "will have to be built" +msgstr "il testamento dovrà essere ricreato" + +#: bal/gui/qt/window.py:963 +#, python-brace-format +msgid "" +"Error:{}\n" +" {}" +msgstr "" +"Errore:{}\n" +" {}" + +#: bal/gui/qt/window.py:965 +msgid "Please, check your heirs, locktime and threshold!" +msgstr "Controlla i tuoi eredi, il locktime e la data della verifica se sei vivo!" + +#: bal/gui/qt/window.py:980 +msgid "" +"Your inheritance has been rebuilt and now needs to be signed again.\n" +"\n" +"Next step (manual):\n" +" 1. Press 'Sign' to sign the new transaction." +msgstr "" +"La tua eredità è stata ricreata e ora va firmata di nuovo.\n" +"\n" +"Prossimo passo (manuale):\n" +" 1. Premi 'Firma' per firmare la nuova transazione." + +#: bal/gui/qt/window.py:987 +msgid "" +"Your inheritance has been rebuilt and now needs to be signed and re-sent to the will-executors.\n" +"\n" +"Next steps (manual):\n" +" 1. Press 'Sign' to sign the new transaction.\n" +" 2. Press 'Broadcast' to send it to the will-executors." +msgstr "" +"La tua eredità è stata ricreata e ora va firmata e inviata di nuovo ai Will-Executor.\n" +"\n" +"Prossimi passi (manuali):\n" +" 1. Premi 'Firma' per firmare la nuova transazione.\n" +" 2. Premi 'Trasmetti il piano' per inviarla ai Will-Executor." + +#. from Electrum it_IT +#: bal/gui/qt/window.py:1024 +msgid "Electrum was unable to deserialize the transaction:" +msgstr "Electrum non ha potuto deserializzare la transazione:" + +#: bal/gui/qt/window.py:1038 +msgid "no tx" +msgstr "nessuna transazione" + +#: bal/gui/qt/window.py:1053 +msgid "Please sign and broadcast this transaction to invalidate current will" +msgstr "Firma questa transazione e trasmetti il piano per invalidare il piano del testamento attuale" + +#: bal/gui/qt/window.py:1060 +msgid "No transactions to invalidate" +msgstr "Nessuna transazione da invalidare" + +#: bal/gui/qt/window.py:1077 +msgid "Calculating Transactions" +msgstr "Calcolo delle transazioni" + +#: bal/gui/qt/window.py:1093 +#, python-brace-format +msgid "signed: {}" +msgstr "firmate: {}" + +#: bal/gui/qt/window.py:1094 +#, python-brace-format +msgid "signing: {}" +msgstr "in firma: {}" + +#: bal/gui/qt/window.py:1564 +msgid "Signing transactions..." +msgstr "Firma delle transazioni..." + +#: bal/gui/qt/window.py:1578 +msgid "Some transaction was not broadcasted" +msgstr "Alcune transazioni non sono state trasmesse" + +#: bal/gui/qt/window.py:1582 +msgid "All transactions are broadcasted to respective Will-Executors" +msgstr "Tutte le transazioni sono state trasmesse ai rispettivi Will-Executor" + +#: bal/gui/qt/window.py:1601 +msgid "Selecting Will-Executors" +msgstr "Selezione dei Will-Executor" + +#: bal/gui/qt/window.py:1616 +msgid "Broadcasting Transactions to Will-Executors:" +msgstr "Trasmissione del piano ai Will-Executor:" + +#: bal/gui/qt/window.py:1623 bal/gui/qt/window.py:2299 +msgid "waiting..." +msgstr "in attesa..." + +#: bal/gui/qt/window.py:1639 +msgid "checking..." +msgstr "controllo in corso..." + +#: bal/gui/qt/window.py:1670 +#, python-brace-format +msgid "checking {} - {} : {}" +msgstr "controllo {} - {} : {}" + +#: bal/gui/qt/window.py:1679 +#, python-brace-format +msgid "checked {} - {} : {}" +msgstr "controllato {} - {} : {}" + +#: bal/gui/qt/window.py:1875 bal/gui/qt/window.py:1913 +#: bal/gui/qt/window.py:1937 +#, python-brace-format +msgid "Invalid will file: {}" +msgstr "File del testamento non valido: {}" + +#: bal/gui/qt/window.py:1894 bal/gui/qt/window.py:1945 +msgid "will" +msgstr "testamento" + +#: bal/gui/qt/window.py:1990 +msgid "Checking transactions" +msgstr "Controllo delle transazioni" + +#: bal/gui/qt/window.py:2039 +msgid "Check Transaction" +msgstr "Controlla la transazione" + +#: bal/gui/qt/window.py:2149 +msgid "" +"Could not download the will-executors list.\n" +"\n" +"This is usually caused by your internet connection or a firewall, not by the plugin. Please check your connection (a VPN often helps) and try again." +msgstr "" +"Impossibile scaricare l'elenco dei Will-Executor.\n" +"\n" +"Di solito la causa è la connessione a internet o un firewall, non il plugin. Controlla la connessione (spesso una VPN aiuta) e riprova." + +#: bal/gui/qt/window.py:2159 +msgid "" +"Could not download the will-executors list over Tor.\n" +"\n" +"Electrum is connected through Tor and the connection is taking too long. Your Tor connection may be slow. Please try again, or use a VPN (or temporarily disable Tor) for a faster connection." +msgstr "" +"Impossibile scaricare l'elenco dei Will-Executor tramite Tor.\n" +"\n" +"Electrum è connesso tramite Tor e la connessione sta impiegando troppo tempo. La tua connessione Tor potrebbe essere lenta. Riprova, oppure usa una VPN (o disattiva Tor per un momento) per una connessione più veloce." + +#: bal/gui/qt/window.py:2169 +msgid "Downloading will-executors list..." +msgstr "Download dell'elenco dei Will-Executor..." + +#: bal/gui/qt/window.py:2231 +#, python-brace-format +msgid "" +"No active will-executor servers found for the {chain} network.\n" +"\n" +"The welist server at {url} responded but returned no will-executors for this chain." +msgstr "" +"Nessun server Will-Executor attivo trovato per la rete {chain}.\n" +"\n" +"Il server welist {url} ha risposto, ma non ha restituito Will-Executor per questa rete." + +#: bal/gui/qt/window.py:2239 +#, python-brace-format +msgid "" +"Could not reach the configured welist server.\n" +"\n" +"Server: {url}\n" +"Error: {reason}\n" +"\n" +"Please verify the welist server URL in the plugin settings." +msgstr "" +"Impossibile raggiungere il server welist configurato.\n" +"\n" +"Server: {url}\n" +"Errore: {reason}\n" +"\n" +"Controlla l'URL del server welist nelle impostazioni del plugin." + +#: bal/gui/qt/window.py:2249 +#, python-brace-format +msgid "No active will-executor found for the {chain} network." +msgstr "Nessun Will-Executor attivo trovato per la rete {chain}." + +#: bal/gui/qt/window.py:2285 +msgid "Ping Will-Executors:" +msgstr "Ping ai Will-Executor:" + +#: bal/gui/qt/window.py:2345 +msgid "Ping Will-Executors" +msgstr "Ping ai Will-Executor" + diff --git a/build_zip.py b/build_zip.py index 6c06b77..b712c58 100644 --- a/build_zip.py +++ b/build_zip.py @@ -9,7 +9,12 @@ Electrum loads external plugins from a ``.zip`` using Python's ``zipimport``. * uses standard DEFLATE compression (well supported by ``zipimport``); * emits entries in a deterministic, sorted order so the archive is reproducible (stable SHA-256); - * skips ``__pycache__`` directories and compiled ``*.pyc``/``*.pyo`` files. + * skips ``__pycache__`` directories and compiled ``*.pyc``/``*.pyo`` files; + * compiles each translation catalog ``bal/locale/<lang>/LC_MESSAGES/bal.po`` + into ``bal.mo`` inside the archive (the ``.mo`` files are not kept in + git, and the ``.po``/``.pot`` sources are not shipped). This needs Babel + (``pip install babel``); without it the build stops, so a release can + never ship without its translations by mistake. The archive keeps the top-level ``bal/`` directory so that the package is importable as ``bal`` (and Electrum derives ``dirname='bal'`` from the path of @@ -23,14 +28,36 @@ Prints the resulting size and SHA-256 so the download can be integrity-checked. """ import hashlib +import io import os import sys +import time import zipfile SRC_ROOT = "bal" DEFAULT_OUT = "bal-electrum-plugin.zip" +def compile_catalog(po_path: str) -> bytes: + """Return the compiled ``.mo`` bytes of the gettext catalog ``po_path``. + + Fuzzy (unreviewed) entries are left out, as ``msgfmt`` does. + """ + try: + from babel.messages.mofile import write_mo + from babel.messages.pofile import read_po + except ImportError: + raise SystemExit( + "ERROR: Babel is needed to compile the translations " + "(pip install babel)" + ) from None + with open(po_path, "rb") as f: + catalog = read_po(f) + buf = io.BytesIO() + write_mo(buf, catalog, use_fuzzy=False) + return buf.getvalue() + + def build(out_path: str) -> None: if os.path.exists(out_path): os.remove(out_path) @@ -40,17 +67,34 @@ def build(out_path: str) -> None: # prune cache dirs in place so os.walk does not descend into them dirnames[:] = [d for d in dirnames if d != "__pycache__"] for fn in filenames: - if fn.endswith((".pyc", ".pyo")): + # .mo files are rebuilt from the .po sources below; the .po/.pot + # sources themselves are not shipped. + if fn.endswith((".pyc", ".pyo", ".po", ".pot", ".mo")): continue files.append(os.path.join(dirpath, fn)) + catalogs = {} # archive path of the .mo -> source .po + for dirpath, _dirnames, filenames in os.walk(os.path.join(SRC_ROOT, "locale")): + for fn in filenames: + if fn.endswith(".po"): + po = os.path.join(dirpath, fn) + catalogs[po[: -len(".po")] + ".mo"] = po files.sort() with zipfile.ZipFile( out_path, "w", compression=zipfile.ZIP_DEFLATED, compresslevel=6 ) as z: - for f in files: + entries = [(f, None) for f in files] + [ + (mo, po) for mo, po in catalogs.items() + ] + for f, po in sorted(entries): arc = f.replace(os.sep, "/") # forward slashes inside the archive - z.write(f, arc) + if po is None: + z.write(f, arc) + else: + # Same timestamp rule as z.write(): the source file's mtime. + info = zipfile.ZipInfo(arc, time.localtime(os.path.getmtime(po))[:6]) + info.compress_type = zipfile.ZIP_DEFLATED + z.writestr(info, compile_catalog(po)) # Integrity + summary with zipfile.ZipFile(out_path) as z: @@ -63,7 +107,8 @@ def build(out_path: str) -> None: data = open(out_path, "rb").read() print(f"built : {out_path}") - print(f"files : {len(files)}") + print(f"files : {len(files) + len(catalogs)}") + print(f"langs : {', '.join(sorted(os.path.basename(os.path.dirname(os.path.dirname(m))) for m in catalogs)) or '-'}") print(f"size : {len(data)} bytes") print(f"sha256: {hashlib.sha256(data).hexdigest()}") diff --git a/pyproject.toml b/pyproject.toml index 97a6aae..879f935 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,7 +3,7 @@ line-length = 88 target-version = "py312" [tool.ruff.lint] -select =["E", "W", "F", "I", "N", "B"] +select =["E", "W", "F", "I", "N", "B", "INT"] # INT: gettext rules (see bal/i18n.py) ignore = ["E501"] [tool.ruff.lint.pep8-naming] diff --git a/tests/test_i18n.py b/tests/test_i18n.py new file mode 100644 index 0000000..e3cd9f5 --- /dev/null +++ b/tests/test_i18n.py @@ -0,0 +1,309 @@ +"""Tests for bal/i18n.py, BAL's translation layer (PLAN_I18N.md, phase 1). + +The BAL catalogs are built in memory (see ``_mo_bytes``), so these tests need +no compiled .mo file and no Babel. Electrum's translator is replaced by a +small dictionary, so the tests do not depend on Electrum's own catalogs. + +Run standalone (``python3 tests/test_i18n.py``) or with pytest. +""" + +import array +import os +import struct +import sys +from contextlib import contextmanager +from types import SimpleNamespace + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) + +from bal import i18n # noqa: E402 + +IT_MO = "locale/it_IT/LC_MESSAGES/bal.mo" + + +def _mo_bytes(messages): + """Return a GNU .mo catalog (UTF-8) holding ``messages`` {msgid: msgstr}. + + Same layout as CPython's Tools/i18n/msgfmt.py: header, the sorted msgid + and msgstr index tables, then the NUL-terminated strings. + """ + entries = {"": "Content-Type: text/plain; charset=UTF-8\n"} + entries.update(messages) + keys = sorted(entries) + ids = strs = b"" + offsets = [] + for key in keys: + msgid, msgstr = key.encode("utf-8"), entries[key].encode("utf-8") + offsets.append((len(ids), len(msgid), len(strs), len(msgstr))) + ids += msgid + b"\0" + strs += msgstr + b"\0" + keystart = 7 * 4 + 16 * len(keys) + valuestart = keystart + len(ids) + koffsets, voffsets = [], [] + for o1, l1, o2, l2 in offsets: + koffsets += [l1, o1 + keystart] + voffsets += [l2, o2 + valuestart] + header = struct.pack( + "Iiiiiii", 0x950412DE, 0, len(keys), 7 * 4, 7 * 4 + len(keys) * 8, 0, 0 + ) + return header + array.array("i", koffsets + voffsets).tobytes() + ids + strs + + +class FakePlugin: + """Stands in for Electrum's BasePlugin: serves read_file() from a dict.""" + + def __init__(self, files=None): + self.files = files or {} + self.requested = [] + + def read_file(self, filename): + self.requested.append(filename) + if filename not in self.files: + raise KeyError(filename) # what a missing entry in the zip raises + return self.files[filename] + + +@contextmanager +def _i18n_state(electrum=None, files=None, lang=None): + """Fake Electrum's translations, load a fake BAL catalog, restore after. + + Restoring matters: bal.i18n keeps module-level state, shared with every + other test of the same pytest run. + """ + saved = (i18n._electrum_gettext, i18n._catalog) + table = electrum or {} + i18n._electrum_gettext = lambda msg: table.get(msg, msg) + plugin = FakePlugin(files) + try: + i18n.set_language(plugin, lang) + yield plugin + finally: + i18n._electrum_gettext, i18n._catalog = saved + i18n._rejected.clear() + + +def test_empty_string_is_never_translated(): + # For gettext "" is the key of the catalog header. + with _i18n_state(files={IT_MO: _mo_bytes({})}, lang="it_IT"): + assert i18n._catalog is not None + assert i18n._("") == "" + + +def test_electrum_translation_wins_over_bal_catalog(): + catalog = _mo_bytes({"Cancel": "Cancella", "Heirs": "Eredi"}) + with _i18n_state( + electrum={"Cancel": "Annulla"}, files={IT_MO: catalog}, lang="it_IT" + ): + assert i18n._("Cancel") == "Annulla" # Electrum has it: Electrum wins + assert i18n._("Heirs") == "Eredi" # only BAL has it + + +def test_english_source_when_nobody_translates(): + with _i18n_state(files={IT_MO: _mo_bytes({"Heirs": "Eredi"})}, lang="it_IT"): + assert i18n._("Not in any catalog") == "Not in any catalog" + + +def test_translation_with_different_fields_is_rejected(): + catalog = _mo_bytes( + { + "{} heirs": "eredi", # field dropped: rejected + "Fee: {}": "Commissione: {}", + "{0} of {1}": "{1} di {0}", # re-ordered: allowed + "Hello {name}": "Ciao {nome}", # field renamed: rejected + } + ) + with _i18n_state(files={IT_MO: catalog}, lang="it_IT"): + assert i18n._("{} heirs") == "{} heirs" + assert i18n._("Fee: {}") == "Commissione: {}" + assert i18n._("{0} of {1}") == "{1} di {0}" + assert i18n._("Hello {name}") == "Hello {name}" + + +def test_keeps_format_fields(): + assert i18n.keeps_format_fields("Amount: {}", "Importo: {}") + assert not i18n.keeps_format_fields("Amount: {}", "Importo") + assert not i18n.keeps_format_fields("Amount: {}", "Importo: {} {}") + assert not i18n.keeps_format_fields("Amount: {}", "Importo: {") # malformed + assert i18n.keeps_format_fields("$wallet_name", "$wallet_name") + + +def test_english_loads_no_catalog(): + with _i18n_state(files={IT_MO: _mo_bytes({"Heirs": "Eredi"})}, lang="en_UK") as p: + assert i18n._catalog is None + assert p.requested == [] + assert i18n._("Heirs") == "Heirs" + + +def test_no_language_loads_no_catalog(): + for lang in (None, ""): + with _i18n_state(files={IT_MO: _mo_bytes({"Heirs": "Eredi"})}, lang=lang): + assert i18n._catalog is None + + +def test_missing_catalog_falls_back_to_english(): + with _i18n_state(files={}, lang="it_IT") as plugin: + assert i18n._catalog is None + assert plugin.requested == [IT_MO, "locale/it/LC_MESSAGES/bal.mo"] + assert i18n._("Heirs") == "Heirs" + + +def test_broken_catalog_falls_back_to_english(): + with _i18n_state(files={IT_MO: b"this is not a catalog"}, lang="it_IT"): + assert i18n._catalog is None + assert i18n._("Heirs") == "Heirs" + + +def test_language_code_without_country_is_tried(): + files = {"locale/it/LC_MESSAGES/bal.mo": _mo_bytes({"Heirs": "Eredi"})} + with _i18n_state(files=files, lang="it_IT"): + assert i18n._("Heirs") == "Eredi" + + +def test_n_marks_without_translating(): + with _i18n_state(files={IT_MO: _mo_bytes({"Heirs": "Eredi"})}, lang="it_IT"): + assert i18n.N_("Heirs") == "Heirs" + assert i18n._(i18n.N_("Heirs")) == "Eredi" + + +def test_init_from_config_uses_electrum_language_setting(): + config = SimpleNamespace(LOCALIZATION_LANGUAGE="it_IT") + plugin = FakePlugin({IT_MO: _mo_bytes({"Heirs": "Eredi"})}) + with _i18n_state(): + i18n.init_from_config(plugin, config) + assert i18n._("Heirs") == "Eredi" + + +def test_init_from_config_without_setting_uses_system_language(): + import electrum.gui.default_lang as default_lang + + saved = default_lang.get_default_language + default_lang.get_default_language = lambda gui_name=None: "it_IT" + config = SimpleNamespace(LOCALIZATION_LANGUAGE="") + plugin = FakePlugin({IT_MO: _mo_bytes({"Heirs": "Eredi"})}) + try: + with _i18n_state(): + i18n.init_from_config(plugin, config) + assert i18n._("Heirs") == "Eredi" + finally: + default_lang.get_default_language = saved + + +def test_init_from_config_never_raises(): + plugin = FakePlugin({IT_MO: _mo_bytes({"Heirs": "Eredi"})}) + with _i18n_state(): + i18n.init_from_config(plugin, object()) # config without the setting + assert i18n._catalog is None + + +# --- Stored data stays language-neutral (PLAN_I18N.md, phase 2) ------------- + + +def test_status_history_is_stored_in_english(): + from bal.core.will import WillItem + + catalog = _mo_bytes({"Signed": "Firmato", "Valid": "Valida"}) + with _i18n_state(files={IT_MO: catalog}, lang="it_IT"): + # A bare item: set_status() only needs its status fields. + item = WillItem.__new__(WillItem) + item.status = "" + item.STATUS = WillItem.copy_status_table(WillItem.STATUS_DEFAULT) + item.set_status("COMPLETE", True) + item.set_status("VALID", False) + assert item.status == ".Signed.NOT Valid" + + +def test_status_history_is_translated_for_display(): + from bal.core.will import format_status_history + + catalog = _mo_bytes( + { + "Signed": "Firmato", + "Valid": "Valida", + "NOT {}": "NON {}", + "New": "Nuovo", + "Replaced": "Sostituita", + } + ) + with _i18n_state(files={IT_MO: catalog}, lang="it_IT"): + assert format_status_history(".Signed.NOT Valid") == ".Firmato.NON Valida" + # A real history saved by an older version (tests/samanta7), which + # stored some labels already translated: known English labels are + # translated, the other tokens are kept as they are. + assert ( + format_status_history("New.Firmato.Pushed.Checked.Replaced") + == "Nuovo.Firmato.Pushed.Checked.Sostituita" + ) + assert format_status_history(".SignedERROR!!!") == ".FirmatoERROR!!!" + with _i18n_state(): # English GUI: shown exactly as stored + assert format_status_history("New.Firmato.Pushed") == "New.Firmato.Pushed" + + +class _FakeConfig(dict): + """Minimal stand-in for Electrum's SimpleConfig (get / set_key).""" + + def get(self, key, default=None): + return dict.get(self, key, default) + + def set_key(self, key, value, save=True): + self[key] = value + + +def test_translatable_config_default_follows_language(): + from bal.core.plugin_base import BalConfig + + config = _FakeConfig() + summary = BalConfig(config, "summary", "Will of $wallet_name", translatable=True) + plain = BalConfig(config, "label", "BAL label") + catalog = _mo_bytes( + {"Will of $wallet_name": "Testamento di $wallet_name", "BAL label": "X"} + ) + with _i18n_state(files={IT_MO: catalog}, lang="it_IT"): + assert summary.get() == "Testamento di $wallet_name" # nothing stored + assert summary.localized_default() == "Testamento di $wallet_name" + summary.set("Testamento di $wallet_name") # e.g. the reset button + assert config["summary"] == "Will of $wallet_name" # stored in English + assert summary.get() == "Testamento di $wallet_name" + summary.set("My own text") # the user's text is kept as it is + assert summary.get() == "My own text" + assert plain.get() == "BAL label" # not translatable: never translated + with _i18n_state(): + summary.set("Will of $wallet_name") + assert summary.get() == "Will of $wallet_name" + + +def test_calendar_reminder_suffix_is_translatable(): + from datetime import datetime, timedelta + + from bal.core.reminders import build_ics_reminders + + now = datetime(2026, 1, 1) + kwargs = dict( + locktime=now + timedelta(days=400), basic_mode=True, description="d", + summary="s", wallet_name="w", heirs_details="", version="0", now=now, + ) + assert "s (reminder 1/" in build_ics_reminders(**kwargs) # CLI default + ics = build_ics_reminders(reminder_suffix="(promemoria {idx}/{total})", **kwargs) + assert "s (promemoria 1/" in ics + + +def test_qr_preset_labels_match_chunk_presets(): + # The labels are marked for translation in common.py because + # qrtransfer.py must stay free of Electrum imports (Android copy). + from bal.core.qrtransfer import CHUNK_PRESETS + from bal.gui.qt.common import QR_PRESET_LABELS + + assert tuple(label for label, _budget in CHUNK_PRESETS) == QR_PRESET_LABELS + + +def test_real_translator_is_electrums(): + # Outside the fakes above, BAL asks Electrum's real translator first. + from electrum.i18n import _ as electrum_gettext + + assert i18n._electrum_gettext is electrum_gettext + + +if __name__ == "__main__": + tests = [v for k, v in sorted(globals().items()) if k.startswith("test_")] + for test in tests: + test() + print(f"[OK] All {len(tests)} i18n tests passed") diff --git a/tests/test_translations.py b/tests/test_translations.py new file mode 100644 index 0000000..c288e43 --- /dev/null +++ b/tests/test_translations.py @@ -0,0 +1,135 @@ +"""Safety checks on BAL's translation catalogs (PLAN_I18N.md, section 4.4). + +Runs on every ``bal/locale/<lang>/LC_MESSAGES/bal.po`` (the sources that +build_zip.py compiles into the zip). A translation is shown to the user as +if BAL wrote it, so a wrong or malicious one must fail here, before a zip is +built: + +* the catalog must parse and compile; +* the ``{}`` replacement fields must match the English text (the same rule + bal.i18n applies at run time), and so must the ``$tokens`` of the calendar + texts (``$wallet_name``, ``$heirs_complete``), which Electrum does not check; +* no Bitcoin address, e-mail address, URL or long letters-and-digits word may + appear in a translation unless the English text has the same one: the + patterns are copied from Electrum's ``electrum-locale/update.py`` (MIT + licence, Copyright (C) The Electrum developers), which rejects translations + that try to slip in an address or a link. + +Run standalone (``python3 tests/test_translations.py``, prints a per-language +summary) or with pytest. Needs Babel, like build_zip.py. +""" + +import io +import os +import re +import sys +from pathlib import Path + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir)) + +from babel.messages.mofile import write_mo # noqa: E402 +from babel.messages.pofile import read_po # noqa: E402 + +from bal.i18n import keeps_format_fields # noqa: E402 + +LOCALE_DIR = Path(__file__).resolve().parent.parent / "bal" / "locale" + +# From electrum-locale/update.py (see the module docstring). +SUSPICIOUS = { + "Bitcoin address": re.compile("([13]|bc1)[a-zA-Z0-9]{30,}"), + "e-mail address": re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+"), + "URL": re.compile(r"\S+\.\S*\w+\S*/\S+"), + "URL scheme": re.compile(r"http(s){0,1}://"), + "letters-and-digits word": re.compile( + r"(?a)(?=\w{16,})((\w*[a-zA-Z]\w*[0-9]\w*)|(\w*[0-9]\w*[a-zA-Z]\w*))" + ), +} +TOKEN = re.compile(r"\$\w+") + + +def _catalogs(): + return sorted(LOCALE_DIR.glob("*/LC_MESSAGES/bal.po")) + + +def _messages(po_path): + """Yield the translated, non-fuzzy (msgid, msgstr) pairs of a catalog.""" + with open(po_path, "rb") as f: + catalog = read_po(f) + for m in catalog: + if m.id and isinstance(m.id, str) and m.string and not m.fuzzy: + yield m.id, m.string + + +def check_catalog(po_path): + """Return a list of problems found in one catalog (empty = fine).""" + problems = [] + for msgid, msgstr in _messages(po_path): + if not keeps_format_fields(msgid, msgstr): + problems.append("{} fields differ: {!r}".format("{}", msgid)) + if sorted(TOKEN.findall(msgid)) != sorted(TOKEN.findall(msgstr)): + problems.append("$tokens differ: {!r}".format(msgid)) + for name, regex in SUSPICIOUS.items(): + for match in regex.finditer(msgstr): + if match.group(0) not in msgid: + problems.append( + "{} {!r} not in the English text: {!r}".format( + name, match.group(0), msgid + ) + ) + return problems + + +def summary(po_path): + """Return (translated, untranslated, fuzzy) counts of a catalog.""" + with open(po_path, "rb") as f: + catalog = read_po(f) + msgs = [m for m in catalog if m.id] + fuzzy = sum(1 for m in msgs if m.fuzzy) + translated = sum(1 for m in msgs if m.string and not m.fuzzy) + return translated, len(msgs) - translated - fuzzy, fuzzy + + +def test_there_is_an_italian_catalog(): + assert LOCALE_DIR / "it_IT" / "LC_MESSAGES" / "bal.po" in _catalogs() + + +def test_catalogs_compile(): + for po in _catalogs(): + with open(po, "rb") as f: + catalog = read_po(f) + buf = io.BytesIO() + write_mo(buf, catalog) + assert buf.getvalue(), po + + +def test_translations_are_safe(): + problems = [ + "{}: {}".format(po.parent.parent.name, p) + for po in _catalogs() + for p in check_catalog(po) + ] + assert not problems, "\n".join(problems) + + +def test_suspicious_patterns_are_detected(): + # A translation that adds an address or a link must be caught. + regexes = SUSPICIOUS.values() + assert any(r.search("invia a bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq") for r in regexes) + assert any(r.search("scarica da https://example.com") for r in regexes) + assert any(r.search("scrivi a truffa@example.com") for r in regexes) + assert not any(r.search("Firma il testamento") for r in regexes) + + +if __name__ == "__main__": + test_there_is_an_italian_catalog() + test_catalogs_compile() + test_suspicious_patterns_are_detected() + for po in _catalogs(): + translated, untranslated, fuzzy = summary(po) + print( + "{}: {} translated, {} untranslated, {} fuzzy".format( + po.parent.parent.name, translated, untranslated, fuzzy + ) + ) + test_translations_are_safe() + print("[OK] translation catalogs are safe")