fix: add `ButtonOptionWithoutTranslation`
What changed, and why it matters
This commit fixes a localization bug in a Bitcoin seed backup verification feature. It prevents BIP-39 seed words (which must stay in English) from being translated into other languages when displayed as quiz answers. Previously, translating these words could confuse users or make backup verification fail. There is no direct security exploit here, but it removes a usability/reliability issue in a security-sensitive workflow.
No urgent action required. Review other screens that display BIP-39 words or other non-translatable values to ensure they also use ButtonOptionWithoutTranslation or equivalent protection.
Security signals we found
Fixes incorrect translation of security-critical data (BIP-39 seed words)
Tightens type check from exact equality to isinstance
No cryptographic, memory-safety, or authentication changes
Evidence from the diff
The patch introduces a new ButtonOptionWithoutTranslation dataclass subclassing ButtonOption. ButtonListScreen now checks whether each option is an instance of ButtonOptionWithoutTranslation and, if so, skips wrapping button_label/active_button_label in the gettext _() translation call. SeedWordsBackupTestView is updated to use ButtonOptionWithoutTranslation for BIP-39 mnemonic word options, ensuring seed words remain untranslated during the backup test. The change also tightens the type check from exact type equality to isinstance, which is a minor code-quality improvement.
Changed components
src/seedsigner/gui/screens/screen.pysrc/seedsigner.views.seed_views.SeedWordsBackupTestViewInspect captured patch +26 / −8
diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py
index f035d24..4b03d5b 100644
--- a/src/seedsigner/gui/screens/screen.py
+++ b/src/seedsigner/gui/screens/screen.py
@@ -283,6 +283,15 @@ class ButtonOption:
+@dataclass
+class ButtonOptionWithoutTranslation(ButtonOption):
+ """
+ Same as ButtonOption but does NOT translate button_label or active_button_label.
+ The labels are also not extracted for translation by babel.
+ """
+
+
+
@dataclass
class ButtonListScreen(BaseTopNavScreen):
button_data: list[ButtonOption] = None
@@ -341,13 +350,22 @@ class ButtonListScreen(BaseTopNavScreen):
self.buttons: List[Button] = []
for i, button_option in enumerate(self.button_data):
- if type(button_option) != ButtonOption:
+ if not isinstance(button_option, ButtonOption):
raise Exception("Refactor to ButtonOption approach needed!")
+ if isinstance(button_option, ButtonOptionWithoutTranslation):
+ # Don't wrap labels in _()
+ button_label = button_option.button_label
+ active_button_label = button_option.active_button_label
+ else:
+ # Wrap labels in _() for just-in-time translations
+ button_label = _(button_option.button_label)
+ active_button_label = _(button_option.active_button_label)
+
# TODO: Refactor `Button` to optionally use ButtonOption directly?
button_kwargs = dict(
- text=_(button_option.button_label), # Wrap here for just-in-time translations
- active_text=_(button_option.active_button_label), # Wrap here for just-in-time translations
+ text=button_label,
+ active_text=active_button_label,
icon_name=button_option.icon_name,
icon_color=button_option.icon_color if button_option.icon_color else GUIConstants.BUTTON_FONT_COLOR,
is_icon_inline=True,
diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py
index e35c5b8..31f5ba4 100644
--- a/src/seedsigner/views/seed_views.py
+++ b/src/seedsigner/views/seed_views.py
@@ -10,7 +10,7 @@ from embit.descriptor import Descriptor
from seedsigner.gui.components import FontAwesomeIconConstants, SeedSignerIconConstants
from seedsigner.gui.screens import (RET_CODE__BACK_BUTTON, ButtonListScreen,
WarningScreen, DireWarningScreen, seed_screens)
-from seedsigner.gui.screens.screen import ButtonOption
+from seedsigner.gui.screens.screen import ButtonOption, ButtonOptionWithoutTranslation
from seedsigner.models.encode_qr import CompactSeedQrEncoder, GenericStaticQrEncoder, SeedQrEncoder, SpecterXPubQrEncoder, StaticXpubQrEncoder, UrXpubQrEncoder
from seedsigner.models.qr_type import QRType
from seedsigner.models.seed import Seed
@@ -1301,10 +1301,10 @@ class SeedWordsBackupTestView(View):
while self.cur_index in self.confirmed_list:
self.cur_index = int(random.random() * len(self.mnemonic_list))
- real_word = ButtonOption(self.mnemonic_list[self.cur_index])
- fake_word1 = ButtonOption(bip39.WORDLIST[int(random.random() * 2047)])
- fake_word2 = ButtonOption(bip39.WORDLIST[int(random.random() * 2047)])
- fake_word3 = ButtonOption(bip39.WORDLIST[int(random.random() * 2047)])
+ real_word = ButtonOptionWithoutTranslation(self.mnemonic_list[self.cur_index])
+ fake_word1 = ButtonOptionWithoutTranslation(bip39.WORDLIST[int(random.random() * 2047)])
+ fake_word2 = ButtonOptionWithoutTranslation(bip39.WORDLIST[int(random.random() * 2047)])
+ fake_word3 = ButtonOptionWithoutTranslation(bip39.WORDLIST[int(random.random() * 2047)])
button_data = [real_word, fake_word1, fake_word2, fake_word3]
random.shuffle(button_data)
Why this scored 18/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.