refactor(l10n): make multisig wallet policy display translatable
What changed, and why it matters
This commit is a straightforward user-interface refactor that makes multisig wallet policy text translatable. It replaces hard-coded English strings like '2 of 3' with localizable template strings, adds a small helper to extract threshold and participant counts from descriptors, and updates tests. There is no security-relevant change and no indication of a vulnerability being fixed.
No security action needed. Treat as a normal localization refactor.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The change introduces get_multisig_policy() in embit_utils.py to return (threshold, n) from a basic multisig descriptor instead of parsing embit’s brief_policy string. Two views now use (“{threshold} of {n}”) and (“{threshold} / {n} multisig”) for localization. The commit removes a TODO about localization and updates the translation template and unit tests. No cryptographic, input-validation, or access-control behavior is altered.
Changed components
src/seedsigner/helpers/embit_utils.pysrc/seedsigner/views/seed_views.pysrc/seedsigner/views/tools_views.pysrc/seedsigner/gui/screens/tools_screens.pyl10n/messages.pottests/test_embit_utils.pyInspect captured patch +59 / −6
diff --git a/l10n/messages.pot b/l10n/messages.pot
index 9a91fe2..cdbbb32 100644
--- a/l10n/messages.pot
+++ b/l10n/messages.pot
@@ -8,7 +8,7 @@ msgid ""
msgstr ""
"Project-Id-Version: seedsigner 0.8.6\n"
"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n"
-"POT-Creation-Date: 2026-02-16 21:42+0000\n"
+"POT-Creation-Date: 2026-02-19 07:41+0000\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"
@@ -1340,6 +1340,11 @@ msgstr ""
msgid "Verify addr"
msgstr ""
+#. Multisig policy display showing signing threshold (e.g. "2 of 3")
+#: src/seedsigner/views/seed_views.py
+msgid "{threshold} of {n}"
+msgstr ""
+
#: src/seedsigner/views/seed_views.py
msgid "Signing messages for custom derivation paths not supported"
msgstr ""
@@ -1466,6 +1471,11 @@ msgstr ""
msgid "Change addresses"
msgstr ""
+#. Multisig wallet policy display (e.g. "2 / 3 multisig")
+#: src/seedsigner/views/tools_views.py
+msgid "{threshold} / {n} multisig"
+msgstr ""
+
#. a status message that our payment addresses are being calculated
#: src/seedsigner/views/tools_views.py
msgid "Calculating addrs..."
diff --git a/src/seedsigner/gui/screens/tools_screens.py b/src/seedsigner/gui/screens/tools_screens.py
index 4264294..f453ba3 100644
--- a/src/seedsigner/gui/screens/tools_screens.py
+++ b/src/seedsigner/gui/screens/tools_screens.py
@@ -489,7 +489,7 @@ class ToolsAddressExplorerAddressTypeScreen(ButtonListScreen):
self.components.append(IconTextLine(
# TRANSLATOR_NOTE: a label for a BIP-380-ish Output Descriptor
label_text=_("Wallet descriptor"),
- value_text=self.wallet_descriptor_display_name, # TODO: English text from embit (e.g. "1 / 2 multisig"); make l10 friendly
+ value_text=self.wallet_descriptor_display_name,
is_text_centered=True,
screen_x=GUIConstants.EDGE_PADDING,
screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING,
diff --git a/src/seedsigner/helpers/embit_utils.py b/src/seedsigner/helpers/embit_utils.py
index ed30471..8055379 100644
--- a/src/seedsigner/helpers/embit_utils.py
+++ b/src/seedsigner/helpers/embit_utils.py
@@ -104,6 +104,14 @@ def get_multisig_address(descriptor: Descriptor, index: int = 0, is_change: bool
+def get_multisig_policy(descriptor: Descriptor) -> tuple:
+ """Extract (threshold, n) from a basic multisig descriptor."""
+ if not descriptor.is_basic_multisig:
+ raise ValueError(f"Expected a basic multisig descriptor, got: {descriptor.brief_policy}")
+ return (str(descriptor.miniscript.args[0]), str(len(descriptor.keys)))
+
+
+
def get_embit_network_name(settings_name):
""" Convert SeedSigner SettingsConstants for `network` to embit's NETWORK key """
lookup = {
diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py
index 4795896..9087359 100644
--- a/src/seedsigner/views/seed_views.py
+++ b/src/seedsigner/views/seed_views.py
@@ -2068,8 +2068,10 @@ class MultisigWalletDescriptorView(View):
fingerprint = hexlify(key.fingerprint).decode()
fingerprints.append(fingerprint)
- policy = descriptor.brief_policy.split("multisig")[0].strip()
- # policy = " / ".join(policy.split(" of ")) # i18n w/o l10n since coming from non-l10n embit
+ from seedsigner.helpers.embit_utils import get_multisig_policy
+ threshold, n = get_multisig_policy(descriptor)
+ # TRANSLATOR_NOTE: Multisig policy display showing signing threshold (e.g. "2 of 3")
+ policy = _("{threshold} of {n}").format(threshold=threshold, n=n)
button_data = [self.OK]
if self.controller.resume_main_flow:
diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py
index 9261222..c42ea77 100644
--- a/src/seedsigner/views/tools_views.py
+++ b/src/seedsigner/views/tools_views.py
@@ -599,8 +599,12 @@ class ToolsAddressExplorerAddressTypeView(View):
wallet_descriptor_display_name = None
if "wallet_descriptor" in data:
- wallet_descriptor_display_name = data["wallet_descriptor"].brief_policy.replace(" (sorted)", "")
- wallet_descriptor_display_name = " / ".join(wallet_descriptor_display_name.split(" of ")) # i18n w/o l10n since coming from non-l10n embit
+ from seedsigner.helpers.embit_utils import get_multisig_policy
+ threshold, n = get_multisig_policy(data["wallet_descriptor"])
+ # TRANSLATOR_NOTE: Multisig wallet policy display (e.g. "2 / 3 multisig")
+ wallet_descriptor_display_name = _("{threshold} / {n} multisig").format(
+ threshold=threshold, n=n
+ )
script_type = data["script_type"] if "script_type" in data else None
diff --git a/tests/test_embit_utils.py b/tests/test_embit_utils.py
index bdc57db..7eaf183 100644
--- a/tests/test_embit_utils.py
+++ b/tests/test_embit_utils.py
@@ -345,6 +345,35 @@ def test_get_multisig_address():
func(descriptor=descriptor, index=args[1], is_change=args[2], embit_network=args[3])
+def test_get_multisig_policy():
+ """
+ tests seedsigner.helpers.embit_utils.get_multisig_policy()
+ """
+ from embit.descriptor import Descriptor
+
+ # Reuses the same 2-of-3 multisig descriptors from test_get_multisig_address
+ vectors_descriptor_expected = {
+ # native segwit 2-of-3
+ "wsh(sortedmulti(2,[8d55ff0d/48h/1h/0h/2h]tpubDDxNVWk924RTUhdkVB2uLHw1hGMPNMGufpZefhkkswjbZppVZcuMdjYKQN4ewUog9vbL6RBLFPRWcgTGT7kYP79N6thyJ43ELUs4N2szXMg/{0,1}/*,[73c5da0a/48h/1h/0h/2h]tpubDFH9dgzveyD8zTbPUFuLrGmCydNvxehyNdUXKJAQN8x4aZ4j6UZqGfnqFrD4NqyaTVGKbvEW54tsvPTK2UoSbCC1PJY8iCNiwTL3RWZEheQ/{0,1}/*,[0be174ee/48h/1h/0h/2h]tpubDEsePyLPkbxbrDiZSTTWdsviiNtiQjrvvzZnkLtG72QYLBygEsXePRsTdXi8DeMA7taCuuvoEBjUAfFrsNZeQJqfvG9fFoujYWbFPYUn7ux/{0,1}/*))#zw6cnrlk": ("2", "3"),
+ # nested segwit 2-of-3
+ "sh(wsh(sortedmulti(2,[73c5da0a/48h/1h/1h/0h/1h]tpubDFH9dgzveyD8yHQb8VrpG8FYAuwcLMHMje2CCcbBo1FpaGzYVtJeYYxcYgRqSTta5utUFts8nPPHs9C2bqoxrey5jia6Dwf9mpwrPq7YvcJ/{0,1}/*,[0be174ee/48h/1h/0h/1h]tpubDEsePyLPkbxbnj6XuKvWwdERHaKkikZxaGJ9sJqmM7okbZXgkNSFiGU6GX6qEes6kD8f9Z9FosYB9UEnBSgBEyEwwJhj4uUcFE1WE8VtKoh/{0,1}/*,[8d55ff0d/48h/1h/0h/1h]tpubDDxNVWk924RTT3vyGLHdSDoZ2JUVX7jUsPcwCQ9MrKHAtJrW5zECTF9rFHCvqu526E4PjHp61hBknts2c5aGexvX7hvCZ8TGPvQFdzxxy59/{0,1}/*)))#2ujlfp73": ("2", "3"),
+ # legacy p2sh 2-of-3
+ "sh(sortedmulti(2,[8d55ff0d/45h]tpubDANogJ2yfnizHwX7fSi5kUVzybyuPXDhgHB2TR9TUvkSLZFW73cRq4STKFDpx7qjJJiisyq82tbu4CeiYtmKEmT1xoCq9P8BPvXV31HUh6d/{0,1}/*,[0be174ee/45h]tpubDBkeVF2tDNT1Pz7L47iJeBB6RokU12LX6x4E6Ph8T89hmjQfB77q1AMyGwL8qpREVGq9sCJEbWwmnemwNTxnpxGn1di7BGy8jx9wEi5Vahu/{0,1}/*,[73c5da0a/45h]tpubDBKsGC1UqBDNvx9aivFmxZNgeZTUnmsCFGhWrqkLzucUCDePvbWWm3n8tAaAwMmxBG2ihdKCG9fzBdUnMxKx5PrkiqSZFi6Vkv6msUs9ddN/{0,1}/*))#p5t8sa8c": ("2", "3"),
+ }
+
+ for desc_str, (expected_threshold, expected_n) in vectors_descriptor_expected.items():
+ descriptor = Descriptor.from_string(desc_str)
+ threshold, n = embit_utils.get_multisig_policy(descriptor)
+ assert threshold == expected_threshold
+ assert n == expected_n
+
+ # Non-multisig descriptor should raise ValueError
+ with pytest.raises(ValueError):
+ embit_utils.get_multisig_policy(Descriptor.from_string(
+ "wpkh([73c5da0a/84h/1h/0h]tpubDC5FSnBiZDMmhiuCmWAYsLwgLYrrT9rAqvTySfuCCrgsWz8wxMXUS9Tb9iVMvcRbvFcAHGkMD5Kx8koh4GquNGNTfohfk7pgjhaPCdXpoba/{0,1}/*)#2aj6cvca"
+ ))
+
+
def test_parse_derivation_path():
# Shouldn't care if input uses "'" or "h"
derivation_path = "m/84'/0'/0'/0/0"
Why this scored 15/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.