From f64f3945a8915f61574a48ad6b9c10790156a6b2 Mon Sep 17 00:00:00 2001 From: kdmukai Date: Fri, 1 Nov 2024 17:03:18 -0500 Subject: [PATCH] Full l10n integration; Initial possible v0.8.5 --- babel/README.md | 75 --- i18n_notes.txt | 72 --- l10n/README.md | 143 +++++ {babel => l10n}/messages.pot | 145 ++++- .../requirements-l10n.txt | 0 pyproject.toml | 2 +- settings_definition.json | 515 ++++++++++++++++++ setup.cfg | 8 +- setup.py | 3 + src/seedsigner/controller.py | 2 +- src/seedsigner/gui/screens/psbt_screens.py | 7 +- src/seedsigner/gui/screens/scan_screens.py | 4 +- src/seedsigner/gui/screens/screen.py | 53 +- src/seedsigner/gui/screens/seed_screens.py | 1 - .../gui/screens/settings_screens.py | 9 +- src/seedsigner/gui/screens/tools_screens.py | 16 +- src/seedsigner/models/settings_definition.py | 43 +- .../resources/seedsigner-translations | 2 +- src/seedsigner/views/psbt_views.py | 27 +- src/seedsigner/views/scan_views.py | 3 +- src/seedsigner/views/seed_views.py | 214 +++----- src/seedsigner/views/settings_views.py | 11 +- src/seedsigner/views/tools_views.py | 125 ++--- src/seedsigner/views/view.py | 30 +- tests/screenshot_generator/generator.py | 4 +- 25 files changed, 1019 insertions(+), 495 deletions(-) delete mode 100644 babel/README.md delete mode 100644 i18n_notes.txt create mode 100644 l10n/README.md rename {babel => l10n}/messages.pot (91%) rename babel/requirements-babel.txt => l10n/requirements-l10n.txt (100%) create mode 100644 settings_definition.json create mode 100644 setup.py diff --git a/babel/README.md b/babel/README.md deleted file mode 100644 index 557a8c0d..00000000 --- a/babel/README.md +++ /dev/null @@ -1,75 +0,0 @@ -# Babel Multilanguage Support - -### "Wrapping" text for translation -All you have to do in your code is wrap each piece of English text with the `gettext` shorthand `_()`: -* Wrap python strings: `error="No device was selected."` becomes `error=_("No device was selected.")` -* Use `.format()` to wrap strings with variable injections: - ``` - mystr = f"My dad's name is {dad.name} and my name is {self.name}." - mystr = _("My dad's name is {} and my name is {}").format(dad.name, self.name) - ``` - - If there are a lot of variables to inject, placeholder names can be used: - ``` - mystr = _("My dad's name is {dad_name} and my name is {my_name}").format(dad_name=dad.name, my_name=self.name) - ``` - - -## Set up `babel` -Install the additional dependency in your local dev environment -```bash -pip install -r babel/requirements-babel.txt -``` - -Make sure that your local repo has fetched the `seedsigner-translations` submodule. It's configured to add it in src/seedsigner/resources. -```bash -# Need --remote in order to respect the target branch listed in .gitmodules -git submodule update --remote -``` - - -### Pre-configured `babel` commands -The `setup.cfg` file in the project root specifies params for the various `babel` commands discussed below. - - -### Rescanning for text that needs translations -Re-generate the `messages.pot` file: - -```bash -python setup.py extract_messaages -``` - -This will rescan all wrapped text, picking up new strings as well as updating existings strings that have been edited. - -_TODO: Github Action to auto-generate messages.pot and fail a PR update if the PR has an out of date messages.pot._ - - - -### Making new text available to translators -Upload the master `messages.pot` to Transifex. It will automatically update each language with the new or changed source strings. - -_TODO: Look into Transifex options to automatically pull updates._ - - - -### Once new translations are complete -The translation file for each language will need to be downloaded via Transifex's "Download for use" option (sends you a `messages.po` file for that language). - -This updated `messages.po` will need to be added to the seedsigner-translations repo in l10n/`{TARGET_LOCALE}`/LC_MESSAGES. - - -### Compile all the translations -The `messages.po` files must be compiled into `*.mo` files: - -```bash -python compile_messages - -# Or target a specific language code: -python compile_messages -l es -``` - - -## Keep the seedsigner-translations repo up to date -The *.po files for each language and their compiled *.mo files should all be kept up to date in the seedsigner-translations repo. - -_TODO: Github Actions automation to regenerate / verify that the *.mo files have been updated after *.po changes._ diff --git a/i18n_notes.txt b/i18n_notes.txt deleted file mode 100644 index 0b6a2db0..00000000 --- a/i18n_notes.txt +++ /dev/null @@ -1,72 +0,0 @@ -# i18n notes -# -# Format is "Pattern filename space-separated-list-of-line-numbers-with-pattern-prefixes" as of 20 November 2022 -# -# kdmukai/intial_multilanguage branch already has plenty of work -# * copied kdmukai's branch -# * merged dev in into his branch. -# * following his lead to change text literals and f-string substitutions. -# * NOTE: TODO: add TranslatorNotes once I understand when they are needed!!! -# -# Common coding patterns noted: (source code lines below are prefixed w/[LFDECP) to identify pattern) -# -# * L) Literal text strings. -# -# * F) F-strings with placeholders. -# -# * V) variables with values set to a previously translated constant, ie _(display_name) -# -# * D) Default variables in baseclasses like "Button Label": is it necessary? user should never see these, else complain? -# -# * E) Exception messages: is it really necessary? only if frequent, else let user complain? -# -# * P) Proper names, partner logos, Coordinators... is it really of any concern at all? -# -# * print() debug messages to the C)onsole or /dev/null do not need translations? Not even included here. -# -# -# While reading the notes below: -# Each file and line-number are preceded by the type of i18n change needed, if truly 'needed' -# A preceding # indicates there is nothing left to do, it's already i18n-ized. -# A preceding ? indicates that I should give this another look, or a deeper look. -# A preceding [DEP] might indicate nothing to do... if we really don't want to i18n these patterns. -# - - -E seedsigner/controller.py E117 E161 E168 - -ED seedsigner/gui/components.py E285 E324 E457 E498 E591 D1096 D1311 -E seedsigner/gui/keyboard.py E204 E207 E485 E587 -#L seedsigner/gui/renderer.py #L144 - -DE#LF seedsigner/gui/screens/screen.py D193 E524 #L756 #L771 #L885 #L888 #L900 #F1112 -#LF seedsigner/gui/screens/psbt_screens.py #L526 #L543? #L553 #L30 #L32 #L89 #L91 #L92 #L94 #F96 #F97 #F100 #L128 #L132 #L141 #L144 #L146 #F148 #L150 #L155 #L466 #L467 #L473 #L485 #L560 #F571 #FL667 #L677 #L689 #L703 -#L seedsigner/gui/screens/scan_screens.py #L22 #L138 #L139 #L140 #L156 -?#L seedsigner/gui/screens/settings_screens.py (_().replace() hmmm ?#L294) #L26 #L59 #L130 #L141 #L162 #L176 #L222 #L289 -#LF seedsigner/gui/screens/tools_screens.py #L41 #L72 #L99 #L119 #F150 #L161 #F167 #F177 #F189 #F193 #F200 #F241 #L274 #F311 #L352 #L354 #L369 #L385 #L393 #F402 #F410 #F418 -#L seedsigner/gui/screens/main_menu_screens.py #L12 #L24 #L29 #L39 #L44 -?#L seedsigner/gui/screens/seed_screens.py (why colon ?#L1033) #L411 #L424 #L555 #L561 #L571 #L587 #L593 #L597 #L606 #L616 #L625 #L638 #L1026 #L1121 #L1123 #L1131 #L1133 #L1147 #L1149 #L1226 #L1362 #L1419 #L1483 #L1500 #L1505 #L1518 #L1524 #L1532 - -E seedsigner/hardware/camera.py E36 E65 -E seedsigner/hardware/ST7789.py E154 - -E seedsigner/helpers/embit_utils.py E25 E35 E43 E45 E47 E91 E95 -E seedsigner/helpers/mnemonic_generation.py E22 -? seedsigner/helpers/ur2/DID-NOT-LOOK - -E#LF seedsigner/views/psbt_views.py E30 #L36 #L37 #L38 #L55 #L94 #L166 #L167 #L168 #L185 #L186 #L187 #L253 #259 #L262 #L331 #L335 #L336 #L338 #L339 #L358 #L360 #L441 #F443 #L445 #FL447 #L451 #L453 #L478 #L530 #L532 #L533 #L534 -#L seedsigner/views/screensaver.py #L74 -E#LF seedsigner/views/seed_views.py (Why this one ?#E1464) #L1637 #L54 #L57 #L78 #L79 #L80 #L81 #L90 #F127 #L187 #L188 #L192 #L194 #L229 #L230 #L260 #L300 #L301 #L342 #L343 #L348 #F350 #L384 #L385 #L386 #L387 #L388 #L389 #L390 #L420 #L476 #L477 #L510 #L511 #L515 #L554 #L556 #L641 #L683 #L684 #L693 #L830 #L855 #L856 #F836 #L902 #L903 #F952 #L992 #L993 #L997 #L999 #F1000 #L1018 #L1020 #L1021 #L1022 #L1044 #L1045 #L1049 #L1050 #L1071 #L1120 #L1121 #L1213 #L1214 #L1218 #L1248 #L1257 #L1258 #L1259 #L1221 #L1268 #L1269 #L1270 #L1272 #L1280 #L1281 #L1282 #L1284 #L1353 #L1354 #L1391 #L1392 #L1393 #L1396 #L1406 #L1413 #L1510 #L1511 #F1639 #L1642 #L1652 #L1653 #L1682 #L1683 #L1684 #L1685 -#V seedsigner/views/settings_views.py #V26 #L20 #L21 #L36 #L39 #L46 #L54 -E#LF seedsigner/views/tools_views.py E605 E616 #L29 #L30 #L31 #L32 #L35 #L112 #L113 #L117 #L184 #L185 #L189 #L238 #L239 #L243 #L275 #L276 #L277 #L376 #L379 #L401 #L402 #L430 #L431 #L432 #L433 #L451 #L538 #L539 #L589 #F628 #FL631 -E#L seedsigner/views/view.py E66 #L124 #L125 #L126 #L127 #L142 #L145 - -E seedsigner/models/encode_qr.py E46 E104 E143 E146 E150 E153 E273 E356 -E seedsigner/models/psbt_parser.py E96 E226 E229 E232 E241 E245 E255 E269 -E seedsigner/models/seed.py E26 E42 -E seedsigner/models/seed_storage.py E68 -P#L seedsigner/models/settings_definition.py P37 P38 P39 P40 #V288 #L14 #L15 #L18 #L19 #L20 #L23 #L26 #L29 #L74 #L55 #L76 #L77 #L96 #L97 #L98 #L106 #L107 #L108 #L124 #L125 #L134 #L135 #L136 #L137 #L357 #L365 #L373 #L374 #L379 #L386 #L395 #L403 #L411 #L417 #L425 #L433 #L439 #L447 #L455 #L461 #L467 #L473 -E seedsigner/models/settings.py E112 E134 E150 E153 E162 E165 -E seedsigner/models/singleton.py E6 E24 E31 -E seedsigner/models/threads.py E23 - diff --git a/l10n/README.md b/l10n/README.md new file mode 100644 index 00000000..645f93fa --- /dev/null +++ b/l10n/README.md @@ -0,0 +1,143 @@ +# Localization (l10n) Tech Notes + +## "Wrapping" text for translation +All you have to do in your code is wrap each piece of English text with the `gettext` shorthand `_()`: +* Wrap python strings: `error="No device was selected."` becomes `error=_("No device was selected.")` +* Use `.format()` to wrap strings with variable injections: + ```python + mystr = f"My dad's name is {dad.name} and my name is {self.name}." + mystr = _("My dad's name is {} and my name is {}").format(dad.name, self.name) + ``` + + If there are a lot of variables to inject, placeholder names can be used: + ```python + mystr = _("My dad's name is {dad_name} and my name is {my_name}").format(dad_name=dad.name, my_name=self.name) + ``` +* Use `ngettext` to dynamically handle singular vs plural forms based on an integer quantity: + ```python + n = 1 + print(ngettext("apple", "apples", n)) + >> apple + + n = 5 + print(ngettext("apple", "apples", n)) + >> apples + ``` + +Transifex will ask translators to provide the singular and plural forms. + +Note: Some languages like Arabic have multiple plural forms (e.g. 2-3 is one form, > 3 is another). + + +## Understanding `gettext` wrapping +Note that this wrapping serves TWO purposes: +1. Pre-translation: This is how we identify text that translators need to translate. Any strings wrapped with `_()` (or related calls like `ngettext`) will appear in translators' Transifex UI. +2. Post-translation: It returns the locale-specific translation for that source string (defaults to the English string itself if no translation is available). + +In general, most SeedSigner code uses the `_()` wrapping only to identify the strings that need translation. + +The second step -- fetching the translated string -- mostly happens in the base `Screen` classes. They ask for the translation just before the text is drawn on screen. + +Basic rules: +* Wrap raw English strings so they'll be picked up for translation. +* In general, don't go out of your way to pass already translated text to `Screen` classes. + * The `Screen` itself should do most of the `_()` wrapping to translate its input attributes for final display. +* Basic gui `Component` classes (e.g. `Button`, `TextArea`) should ideally NOT wrap any text. + * Ensure that they are supplied with wrapped / already translated text. + +Basic example: +```python +@dataclass +class SomeClass: + title: str = _("My Title") +``` + +In this case `title` is wrapped in order to get "My Title" into the translators' list. Note that because `title` is a class-level attribute, its string value will be determined at import time and +will NOT be re-evaluated when the class is eventually used. That means that the locale-specific translation will NOT be returned later: + +```python +from blah import SomeClass + +print(SomeClass.title) +>> My Title + +# set locale to some other language and then... +os.environ['LANGUAGE'] = 'es' +my_instance = SomeClass() +print(my_instance.title) +>> My Title +``` + +To get the translated value, we'd have to pass the attribute itself into `_()`. This feels redundant since the original string was already wrapped in the class definition, but we have to +work around the realities of python imports, etc. This time the wrapping is saying, "Hey, take the value you're holding in that attribute/variable and see if it has an entry in our locale-specific +translation library." +```python +os.environ['LANGUAGE'] = 'es' +print(_(SomeClass.title)) +>> Mi Titulo + +my_instance = SomeClass() +print(_(my_instance.title)) +>> Mi Titulo +``` + + +## Set up localization dependencies +```bash +pip install -r l10n/requirements-l10n.txt +``` + +Make sure that your local repo has fetched the `seedsigner-translations` submodule. It's configured to add it in src/seedsigner/resources. +```bash +# Need --remote in order to respect the target branch listed in .gitmodules +git submodule update --remote +``` + + +### Pre-configured `babel` commands +The `setup.cfg` file in the project root specifies params for the various `babel` commands discussed below. + + +### Rescanning for text that needs translations +Re-generate the `messages.pot` file: + +```bash +python setup.py extract_messaages +``` + +This will rescan all wrapped text, picking up new strings as well as updating existings strings that have been edited. + +_TODO: Github Action to auto-generate messages.pot and fail a PR update if the PR has an out of date messages.pot._ + + +### Making new text available to translators +Upload the master `messages.pot` to Transifex. It will automatically update each language with the new or changed source strings. + +_TODO: Look into Transifex options to automatically pull updates._ + + +### Once new translations are complete +The translation file for each language will need to be downloaded via Transifex's "Download for use" option (sends you a `messages.po` file for that language). + +This updated `messages.po` will need to be added to the seedsigner-translations repo in l10n/`{TARGET_LOCALE}`/LC_MESSAGES. + + +### Compile all the translations +The `messages.po` files must be compiled into `*.mo` files: + +```bash +python setup.py compile_catalog + +# Or target a specific language code: +python setup.py compile_catalog -l es +``` + +### Unused babel commands +Transifex eliminates the need for the `init_catalog` and `update_catalog` commands. + + +## Keep the seedsigner-translations repo up to date +The *.po files for each language and their compiled *.mo files should all be kept up to date in the seedsigner-translations repo. + +_TODO: Github Actions automation to regenerate / verify that the *.mo files have been updated after *.po changes._ + diff --git a/babel/messages.pot b/l10n/messages.pot similarity index 91% rename from babel/messages.pot rename to l10n/messages.pot index c05c6293..9bba17ec 100644 --- a/babel/messages.pot +++ b/l10n/messages.pot @@ -6,9 +6,9 @@ #, fuzzy msgid "" msgstr "" -"Project-Id-Version: seedsigner 0.7.0\n" +"Project-Id-Version: seedsigner 0.8.5\n" "Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" -"POT-Creation-Date: 2024-10-11 14:02-0500\n" +"POT-Creation-Date: 2024-11-01 16:48-0500\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -32,6 +32,7 @@ msgid "btc" msgstr "" #: src/seedsigner/gui/components.py src/seedsigner/gui/screens/psbt_screens.py +#: src/seedsigner/models/settings_definition.py msgid "sats" msgstr "" @@ -113,6 +114,11 @@ msgstr "" msgid "fee" msgstr "" +#. Technical term, should probably NOT be translated in most languages +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "OP_RETURN" +msgstr "" + #. Label for a change output in the PSBT Overview flow diagram #. Used in a sentence describing the address type (change or receive) #: src/seedsigner/gui/screens/psbt_screens.py @@ -166,6 +172,11 @@ msgstr "" msgid "Address verified!" msgstr "" +#. Shown when displaying OP_RETURN as non-human-readable hexadecimal data +#: src/seedsigner/gui/screens/psbt_screens.py +msgid "raw hex data" +msgstr "" + #: src/seedsigner/gui/screens/psbt_screens.py msgid "Sign PSBT" msgstr "" @@ -175,11 +186,13 @@ msgid "Click to approve this transaction" msgstr "" #: src/seedsigner/gui/screens/scan_screens.py -msgid "< back | " +#: src/seedsigner/gui/screens/tools_screens.py +msgid "back" msgstr "" +#. Inserts the percentage value of the animated QR scan progress #: src/seedsigner/gui/screens/scan_screens.py -msgid "{}% Complete" +msgid "{}%" msgstr "" #. Increase QR code screen brightness @@ -235,6 +248,10 @@ msgstr "" msgid "Please wait about 30 seconds before disconnecting power." msgstr "" +#: src/seedsigner/gui/screens/screen.py +msgid "Just Unplug It" +msgstr "" + #: src/seedsigner/gui/screens/screen.py msgid "It is safe to disconnect power at any time." msgstr "" @@ -310,6 +327,7 @@ msgid "Xpub" msgstr "" #: src/seedsigner/gui/screens/seed_screens.py +#: src/seedsigner/models/settings_definition.py #: src/seedsigner/views/seed_views.py msgid "BIP-39 Passphrase" msgstr "" @@ -467,15 +485,27 @@ msgstr "" msgid "Settings QR" msgstr "" +#: src/seedsigner/gui/screens/settings_screens.py +msgid "Settings updated..." +msgstr "" + #: src/seedsigner/gui/screens/settings_screens.py #: src/seedsigner/views/settings_views.py src/seedsigner/views/view.py msgid "Home" msgstr "" -#. Instructions when reviewing an image; "reshoot" must point left, "accept" -#. must point right #: src/seedsigner/gui/screens/tools_screens.py -msgid " < reshoot | accept > " +msgid "click joystick" +msgstr "" + +#. A prompt to the user to either accept or reshoot the image +#: src/seedsigner/gui/screens/tools_screens.py +msgid "reshoot" +msgstr "" + +#. A prompt to the user to either accept or reshoot the image +#: src/seedsigner/gui/screens/tools_screens.py +msgid "accept" msgstr "" #. current roll number vs total rolls (e.g. roll 7 of 50) @@ -510,7 +540,7 @@ msgstr "" msgid "Tails = 0" msgstr "" -#. A labeled presentation of a user-supplied value +#. The additional entropy the user supplied (e.g. coin flips) #: src/seedsigner/gui/screens/tools_screens.py msgid "Your input: \"{}\"" msgstr "" @@ -562,6 +592,18 @@ msgstr "" msgid "Required" msgstr "" +#: src/seedsigner/models/settings_definition.py +msgid "BTC" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Threshold at 0.01" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "BTC | sats hybrid" +msgstr "" + #: src/seedsigner/models/settings_definition.py msgid "0°" msgstr "" @@ -578,14 +620,17 @@ msgstr "" msgid "270°" msgstr "" +#. QR code density option: Low, Medium, High #: src/seedsigner/models/settings_definition.py msgid "Low" msgstr "" +#. QR code density option: Low, Medium, High #: src/seedsigner/models/settings_definition.py msgid "Medium" msgstr "" +#. QR code density option: Low, Medium, High #: src/seedsigner/models/settings_definition.py msgid "High" msgstr "" @@ -620,7 +665,11 @@ msgid "Native Segwit" msgstr "" #: src/seedsigner/models/settings_definition.py -msgid "Nested Segwit (legacy)" +msgid "Nested Segwit" +msgstr "" + +#: src/seedsigner/models/settings_definition.py +msgid "Legacy" msgstr "" #: src/seedsigner/models/settings_definition.py @@ -631,6 +680,11 @@ msgstr "" msgid "Custom Derivation" msgstr "" +#. Terminology used by Electrum seeds; equivalent to bip39 passphrase +#: src/seedsigner/models/settings_definition.py +msgid "Custom Extension" +msgstr "" + #: src/seedsigner/models/settings_definition.py msgid "Language" msgstr "" @@ -691,6 +745,10 @@ msgstr "" msgid "BIP-85 child seeds" msgstr "" +#: src/seedsigner/models/settings_definition.py +msgid "Native Segwit only" +msgstr "" + #: src/seedsigner/models/settings_definition.py msgid "Message signing" msgstr "" @@ -730,6 +788,17 @@ msgstr "" msgid "Enter 24-word seed" msgstr "" +#: src/seedsigner/views/psbt_views.py src/seedsigner/views/seed_views.py +#: src/seedsigner/views/tools_views.py +msgid "Enter Electrum seed" +msgstr "" + +#. Inserts fingerprint w/"?" to indicate that this seed can't sign the current +#. PSBT +#: src/seedsigner/views/psbt_views.py +msgid "{} (?)" +msgstr "" + #: src/seedsigner/views/psbt_views.py msgid "Select Signer" msgstr "" @@ -775,15 +844,19 @@ msgstr "" msgid "Next Recipient" msgstr "" -#. The amount you're receiving back from the transaction #: src/seedsigner/views/psbt_views.py -msgid "Your Change" +msgid "Skip Verification" msgstr "" #: src/seedsigner/views/psbt_views.py msgid "Verify Multisig Change" msgstr "" +#. The amount you're receiving back from the transaction +#: src/seedsigner/views/psbt_views.py +msgid "Your Change" +msgstr "" + #: src/seedsigner/views/psbt_views.py msgid "Self-Transfer" msgstr "" @@ -971,6 +1044,18 @@ msgstr "" msgid "Edit passphrase" msgstr "" +#: src/seedsigner/views/seed_views.py +msgid "Discard passphrase" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Discard passphrase?" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Your current passphrase entry will be erased" +msgstr "" + #: src/seedsigner/views/seed_views.py msgid "Keep Seed" msgstr "" @@ -984,6 +1069,14 @@ msgstr "" msgid "Discard Seed?" msgstr "" +#: src/seedsigner/views/seed_views.py +msgid "Electrum warning" +msgstr "" + +#: src/seedsigner/views/seed_views.py +msgid "Some features are disabled for Electrum seeds." +msgstr "" + #: src/seedsigner/views/seed_views.py msgid "Verify Addr" msgstr "" @@ -1157,10 +1250,6 @@ msgstr "" msgid "Sig type can't be auto-detected from this address. Please specify:" msgstr "" -#: src/seedsigner/views/seed_views.py -msgid "Can't validate a single sig addr without specifying a seed" -msgstr "" - #. Option when scanning for a matching address; skips ten addresses ahead #: src/seedsigner/views/seed_views.py msgid "Skip 10" @@ -1170,6 +1259,10 @@ msgstr "" msgid "Cancel" msgstr "" +#: src/seedsigner/views/seed_views.py +msgid "Can't validate a single sig addr without specifying a seed" +msgstr "" + #: src/seedsigner/views/seed_views.py msgid "multisig" msgstr "" @@ -1202,15 +1295,15 @@ msgstr "" msgid "Return to PSBT" msgstr "" -#: src/seedsigner/views/seed_views.py -msgid "Signing messages for custom derivation paths not supported" -msgstr "" - #. Inserts mainnet/testnet/regtest and derivation path #: src/seedsigner/views/seed_views.py msgid "Current network setting ({}) doesn't match {}" msgstr "" +#: src/seedsigner/views/seed_views.py +msgid "Signing messages for custom derivation paths not supported" +msgstr "" + #: src/seedsigner/views/settings_views.py msgid "I/O test" msgstr "" @@ -1255,12 +1348,14 @@ msgstr "" msgid "Mnemonic Length?" msgstr "" +#. Inserts the number of dice rolls needed for a 12-word mnemonic #: src/seedsigner/views/tools_views.py -msgid "12 words (50 rolls)" +msgid "12 words ({} rolls)" msgstr "" +#. Inserts the number of dice rolls needed for a 24-word mnemonic #: src/seedsigner/views/tools_views.py -msgid "24 words (99 rolls)" +msgid "24 words ({} rolls)" msgstr "" #: src/seedsigner/views/tools_views.py @@ -1351,6 +1446,10 @@ msgstr "" msgid "Reset / Power" msgstr "" +#: src/seedsigner/views/view.py +msgid "This is still on our to-do list!" +msgstr "" + #: src/seedsigner/views/view.py msgid "Work In Progress" msgstr "" @@ -1359,10 +1458,6 @@ msgstr "" msgid "Not Yet Implemented" msgstr "" -#: src/seedsigner/views/view.py -msgid "This is still on our to-do list!" -msgstr "" - #: src/seedsigner/views/view.py msgid "Back to Main Menu" msgstr "" diff --git a/babel/requirements-babel.txt b/l10n/requirements-l10n.txt similarity index 100% rename from babel/requirements-babel.txt rename to l10n/requirements-l10n.txt diff --git a/pyproject.toml b/pyproject.toml index 25345e0d..536700e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,7 +13,7 @@ description = "Build an offline, airgapped Bitcoin signing device for less than name = "seedsigner" readme = "README.md" requires-python = ">=3.10" -version = "0.8.0" +version = "0.8.5" [project.urls] "Bug Tracker" = "https://github.com/SeedSigner/seedsigner/issues" diff --git a/settings_definition.json b/settings_definition.json new file mode 100644 index 00000000..1d48c29c --- /dev/null +++ b/settings_definition.json @@ -0,0 +1,515 @@ +{ + "settings_entries": [ + { + "category": "system", + "attr_name": "locale", + "abbreviated_name": "lang", + "display_name": "Language", + "visibility": "general", + "type": "select_1", + "help_text": null, + "selection_options": [ + { + "display_name": "\u010de\u0161tina", + "value": "cs" + }, + { + "display_name": "English", + "value": "en" + }, + { + "display_name": "Fran\u00e7ais", + "value": "fr" + }, + { + "display_name": "Deutsch", + "value": "de" + }, + { + "display_name": "Portugu\u00eas", + "value": "pt" + }, + { + "display_name": "\u0440\u0443\u0441\u0441\u043a\u0438\u0439", + "value": "ru" + }, + { + "display_name": "Espa\u00f1ol", + "value": "es" + } + ], + "default_value": "en" + }, + { + "category": "system", + "attr_name": "wordlist_language", + "abbreviated_name": "wordlist_lang", + "display_name": "Mnemonic language", + "visibility": "hidden", + "type": "select_1", + "help_text": null, + "selection_options": [ + { + "display_name": "English", + "value": "en" + } + ], + "default_value": "en" + }, + { + "category": "system", + "attr_name": "persistent_settings", + "abbreviated_name": "persistent", + "display_name": "Persistent settings", + "visibility": "general", + "type": "enabled_disabled", + "help_text": "Store Settings on SD card", + "selection_options": [ + { + "display_name": "Enabled", + "value": "E" + }, + { + "display_name": "Disabled", + "value": "D" + } + ], + "default_value": "D" + }, + { + "category": "wallet", + "attr_name": "coordinators", + "abbreviated_name": "coords", + "display_name": "Coordinator software", + "visibility": "general", + "type": "multiselect", + "help_text": null, + "selection_options": [ + { + "display_name": "BlueWallet", + "value": "bw" + }, + { + "display_name": "Nunchuk", + "value": "nun" + }, + { + "display_name": "Sparrow", + "value": "spa" + }, + { + "display_name": "Specter Desktop", + "value": "spd" + }, + { + "display_name": "Keeper", + "value": "kpr" + } + ], + "default_value": [ + "bw", + "nun", + "spa", + "spd" + ] + }, + { + "category": "system", + "attr_name": "denomination", + "abbreviated_name": "denom", + "display_name": "Denomination display", + "visibility": "general", + "type": "select_1", + "help_text": null, + "selection_options": [ + { + "display_name": "BTC", + "value": "btc" + }, + { + "display_name": "sats", + "value": "sats" + }, + { + "display_name": "Threshold at 0.01", + "value": "thr" + }, + { + "display_name": "BTC | sats hybrid", + "value": "hyb" + } + ], + "default_value": "thr" + }, + { + "category": "features", + "attr_name": "network", + "abbreviated_name": null, + "display_name": "Bitcoin network", + "visibility": "advanced", + "type": "select_1", + "help_text": null, + "selection_options": [ + { + "display_name": "Mainnet", + "value": "M" + }, + { + "display_name": "Testnet", + "value": "T" + }, + { + "display_name": "Regtest", + "value": "R" + } + ], + "default_value": "M" + }, + { + "category": "features", + "attr_name": "qr_density", + "abbreviated_name": null, + "display_name": "QR code density", + "visibility": "advanced", + "type": "select_1", + "help_text": null, + "selection_options": [ + { + "display_name": "Low", + "value": "L" + }, + { + "display_name": "Medium", + "value": "M" + }, + { + "display_name": "High", + "value": "H" + } + ], + "default_value": "M" + }, + { + "category": "features", + "attr_name": "xpub_export", + "abbreviated_name": null, + "display_name": "Xpub export", + "visibility": "advanced", + "type": "enabled_disabled", + "help_text": null, + "selection_options": [ + { + "display_name": "Enabled", + "value": "E" + }, + { + "display_name": "Disabled", + "value": "D" + } + ], + "default_value": "E" + }, + { + "category": "features", + "attr_name": "sig_types", + "abbreviated_name": "sigs", + "display_name": "Sig types", + "visibility": "advanced", + "type": "multiselect", + "help_text": null, + "selection_options": [ + { + "display_name": "Single Sig", + "value": "ss" + }, + { + "display_name": "Multisig", + "value": "ms" + } + ], + "default_value": [ + "ss", + "ms" + ] + }, + { + "category": "features", + "attr_name": "script_types", + "abbreviated_name": "scripts", + "display_name": "Script types", + "visibility": "advanced", + "type": "multiselect", + "help_text": null, + "selection_options": [ + { + "display_name": "Native Segwit", + "value": "nat" + }, + { + "display_name": "Nested Segwit", + "value": "nes" + }, + { + "display_name": "Legacy", + "value": "leg" + }, + { + "display_name": "Taproot", + "value": "tr" + }, + { + "display_name": "Custom Derivation", + "value": "cus" + } + ], + "default_value": [ + "nat", + "nes", + "tr" + ] + }, + { + "category": "features", + "attr_name": "xpub_details", + "abbreviated_name": null, + "display_name": "Show xpub details", + "visibility": "advanced", + "type": "enabled_disabled", + "help_text": null, + "selection_options": [ + { + "display_name": "Enabled", + "value": "E" + }, + { + "display_name": "Disabled", + "value": "D" + } + ], + "default_value": "E" + }, + { + "category": "features", + "attr_name": "passphrase", + "abbreviated_name": null, + "display_name": "BIP-39 passphrase", + "visibility": "advanced", + "type": "select_1", + "help_text": null, + "selection_options": [ + { + "display_name": "Enabled", + "value": "E" + }, + { + "display_name": "Disabled", + "value": "D" + }, + { + "display_name": "Required", + "value": "R" + } + ], + "default_value": "E" + }, + { + "category": "features", + "attr_name": "camera_rotation", + "abbreviated_name": "camera", + "display_name": "Camera rotation", + "visibility": "advanced", + "type": "select_1", + "help_text": null, + "selection_options": [ + { + "display_name": "0\u00b0", + "value": 0 + }, + { + "display_name": "90\u00b0", + "value": 90 + }, + { + "display_name": "180\u00b0", + "value": 180 + }, + { + "display_name": "270\u00b0", + "value": 270 + } + ], + "default_value": 180 + }, + { + "category": "features", + "attr_name": "compact_seedqr", + "abbreviated_name": null, + "display_name": "Compact SeedQR", + "visibility": "advanced", + "type": "enabled_disabled", + "help_text": null, + "selection_options": [ + { + "display_name": "Enabled", + "value": "E" + }, + { + "display_name": "Disabled", + "value": "D" + } + ], + "default_value": "E" + }, + { + "category": "features", + "attr_name": "bip85_child_seeds", + "abbreviated_name": "bip85", + "display_name": "BIP-85 child seeds", + "visibility": "advanced", + "type": "enabled_disabled", + "help_text": null, + "selection_options": [ + { + "display_name": "Enabled", + "value": "E" + }, + { + "display_name": "Disabled", + "value": "D" + } + ], + "default_value": "D" + }, + { + "category": "features", + "attr_name": "electrum_seeds", + "abbreviated_name": "electrum", + "display_name": "Electrum seeds", + "visibility": "advanced", + "type": "enabled_disabled", + "help_text": "Native Segwit only", + "selection_options": [ + { + "display_name": "Enabled", + "value": "E" + }, + { + "display_name": "Disabled", + "value": "D" + } + ], + "default_value": "D" + }, + { + "category": "features", + "attr_name": "message_signing", + "abbreviated_name": null, + "display_name": "Message signing", + "visibility": "advanced", + "type": "enabled_disabled", + "help_text": null, + "selection_options": [ + { + "display_name": "Enabled", + "value": "E" + }, + { + "display_name": "Disabled", + "value": "D" + } + ], + "default_value": "D" + }, + { + "category": "features", + "attr_name": "privacy_warnings", + "abbreviated_name": "priv_warn", + "display_name": "Show privacy warnings", + "visibility": "advanced", + "type": "enabled_disabled", + "help_text": null, + "selection_options": [ + { + "display_name": "Enabled", + "value": "E" + }, + { + "display_name": "Disabled", + "value": "D" + } + ], + "default_value": "E" + }, + { + "category": "features", + "attr_name": "dire_warnings", + "abbreviated_name": "dire_warn", + "display_name": "Show dire warnings", + "visibility": "advanced", + "type": "enabled_disabled", + "help_text": null, + "selection_options": [ + { + "display_name": "Enabled", + "value": "E" + }, + { + "display_name": "Disabled", + "value": "D" + } + ], + "default_value": "E" + }, + { + "category": "features", + "attr_name": "qr_brightness_tips", + "abbreviated_name": null, + "display_name": "Show QR brightness tips", + "visibility": "advanced", + "type": "enabled_disabled", + "help_text": null, + "selection_options": [ + { + "display_name": "Enabled", + "value": "E" + }, + { + "display_name": "Disabled", + "value": "D" + } + ], + "default_value": "E" + }, + { + "category": "features", + "attr_name": "partner_logos", + "abbreviated_name": "partners", + "display_name": "Show partner logos", + "visibility": "advanced", + "type": "enabled_disabled", + "help_text": null, + "selection_options": [ + { + "display_name": "Enabled", + "value": "E" + }, + { + "display_name": "Disabled", + "value": "D" + } + ], + "default_value": "E" + }, + { + "category": "system", + "attr_name": "qr_background_color", + "abbreviated_name": "qr_brightness", + "display_name": "QR background color", + "visibility": "hidden", + "type": "free_entry", + "help_text": null, + "selection_options": null, + "default_value": 62 + } + ] +} \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index fcb7993e..b777f178 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,14 +3,8 @@ add_comments = TRANSLATOR_NOTE: strip_comments = yes add_location = file input_dirs = src -output_file = babel/messages.pot +output_file = l10n/messages.pot -[update_catalog] -ignore_pot_creation_date = yes -no_fuzzy_matching = yes -no_wrap = yes -input_file = babel/messages.pot -output_dir = src/seedsigner/resources/seedsigner-translations/l10n [compile_catalog] use_fuzzy = yes diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..c1057cf5 --- /dev/null +++ b/setup.py @@ -0,0 +1,3 @@ +import setuptools + +setuptools.setup() \ No newline at end of file diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index ab943997..693afaef 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -101,7 +101,7 @@ class Controller(Singleton): rather than at the top in order avoid circular imports. """ - VERSION = "0.8.0" + VERSION = "0.8.5" # Declare class member vars with type hints to enable richer IDE support throughout # the code. diff --git a/src/seedsigner/gui/screens/psbt_screens.py b/src/seedsigner/gui/screens/psbt_screens.py index 7af7e1a9..42326759 100644 --- a/src/seedsigner/gui/screens/psbt_screens.py +++ b/src/seedsigner/gui/screens/psbt_screens.py @@ -154,7 +154,8 @@ class PSBTOverviewScreen(ButtonListScreen): destination_column.append(_("fee")) if self.has_op_return: - destination_column.append("OP_RETURN") + # TRANSLATOR_NOTE: Technical term, should probably NOT be translated in most languages + destination_column.append(_("OP_RETURN")) if self.num_change_outputs > 0: for i in range(0, self.num_change_outputs): @@ -731,8 +732,10 @@ class PSBTOpReturnScreen(ButtonListScreen): text += (decoded_str[i*chars_per_line:(i+1)*chars_per_line]) + "\n" text = text[:-1] + # TRANSLATOR_NOTE: Shown when displaying OP_RETURN as non-human-readable hexadecimal data + hex_label = _("raw hex data") label = TextArea( - text="raw hex data", + text=hex_label, font_color=GUIConstants.LABEL_FONT_COLOR, font_size=GUIConstants.LABEL_FONT_SIZE, screen_y=self.top_nav.height, diff --git a/src/seedsigner/gui/screens/scan_screens.py b/src/seedsigner/gui/screens/scan_screens.py index 56189c5b..a7832848 100644 --- a/src/seedsigner/gui/screens/scan_screens.py +++ b/src/seedsigner/gui/screens/scan_screens.py @@ -57,7 +57,8 @@ class ScanScreen(BaseScreen): # Initialize the base class super().__post_init__() - self.instructions_text = _("< back | ") + self.instructions_text + # TODO: Arrange this with UI elements rather than text + self.instructions_text = "< " + _("back") + " | " + _(self.instructions_text) self.camera = Camera.get_instance() self.camera.start_video_stream_mode(resolution=self.resolution, framerate=self.framerate, format="rgb") @@ -209,7 +210,6 @@ class ScanScreen(BaseScreen): draw.text( xy=(rectangle.width - GUIConstants.EDGE_PADDING, int(rectangle.height / 2)), text=text, - # text=f"100%", fill=GUIConstants.BODY_FONT_COLOR, font=instructions_font, anchor="rm", # right-justified, middle diff --git a/src/seedsigner/gui/screens/screen.py b/src/seedsigner/gui/screens/screen.py index f9a404eb..76dae430 100644 --- a/src/seedsigner/gui/screens/screen.py +++ b/src/seedsigner/gui/screens/screen.py @@ -1,6 +1,6 @@ import time -from dataclasses import dataclass +from dataclasses import dataclass, field from gettext import gettext as _ from PIL import Image, ImageDraw, ImageColor from typing import Any, List, Tuple @@ -206,7 +206,7 @@ class BaseTopNavScreen(BaseScreen): self.top_nav = TopNav( icon_name=self.top_nav_icon_name, icon_color=self.top_nav_icon_color, - text=self.title, + text=_(self.title), # Wrap here for just-in-time translations font_size=self.title_font_size, width=self.canvas_width, height=GUIConstants.TOP_NAV_HEIGHT, @@ -323,7 +323,7 @@ class ButtonListScreen(BaseTopNavScreen): (button_label, icon_name, icon_color, button_label_color, right_icon_name) = button_label button_kwargs = dict( - text=button_label, + text=_(button_label), # Wrap here for just-in-time translations icon_name=icon_name, icon_color=icon_color if icon_color else GUIConstants.BUTTON_FONT_COLOR, is_icon_inline=True, @@ -565,7 +565,7 @@ class LargeButtonScreen(BaseTopNavScreen): button_start_x = GUIConstants.EDGE_PADDING + button_width + GUIConstants.COMPONENT_PADDING button_args = { - "text": button_label, + "text": _(button_label), # Wrap here for just-in-time translations "screen_x": button_start_x, "screen_y": button_start_y, "width": button_width, @@ -855,24 +855,19 @@ class QRDisplayScreen(BaseScreen): @dataclass class LargeIconStatusScreen(ButtonListScreen): - title: str = None + title: str = _("Success!") status_icon_name: str = SeedSignerIconConstants.SUCCESS status_icon_size: int = GUIConstants.ICON_PRIMARY_SCREEN_SIZE status_color: str = GUIConstants.SUCCESS_COLOR status_headline: str = None text: str = "" # The body text of the screen text_edge_padding: int = GUIConstants.EDGE_PADDING - button_data: list = None + button_data: list = field(default_factory=lambda: [_("OK")]) allow_text_overflow: bool = False def __post_init__(self): - if not self.title: - self.title = _("Success!") - - self.is_bottom_list: bool = True - if not self.button_data: - self.button_data = [_("OK")] + self.is_bottom_list = True super().__post_init__() self.status_icon = Icon( @@ -887,7 +882,7 @@ class LargeIconStatusScreen(ButtonListScreen): next_y = self.status_icon.screen_y + self.status_icon.height + int(GUIConstants.COMPONENT_PADDING/2) if self.status_headline: self.warning_headline_textarea = TextArea( - text=self.status_headline, + text=_(self.status_headline), # Wrap here for just-in-time translations width=self.canvas_width, screen_y=next_y, font_color=self.status_color, @@ -897,7 +892,7 @@ class LargeIconStatusScreen(ButtonListScreen): self.components.append(TextArea( height=self.buttons[0].screen_y - next_y, - text=self.text, + text=_(self.text), width=self.canvas_width, edge_padding=self.text_edge_padding, # Don't render all the way up to the far left/right edges screen_y=next_y, @@ -983,36 +978,19 @@ class WarningEdgesMixin: @dataclass class WarningScreen(WarningEdgesMixin, LargeIconStatusScreen): - title: str = None + title: str = _("Caution") status_icon_name: str = SeedSignerIconConstants.WARNING status_color: str = "yellow" - status_headline: str = "Privacy Leak!" # The colored text under the alert icon - - def __post_init__(self): - if not self.title: - self.title = _("Caution") - - if not self.status_headline: - self.status_headline = _("Privacy Leak!") - - if not self.button_data: - self.button_data = [_("I Understand")] - - super().__post_init__() + status_headline: str = _("Privacy Leak!") # The colored text under the alert icon + button_data: list = field(default_factory=lambda: [_("I Understand")]) @dataclass class DireWarningScreen(WarningScreen): - status_headline: str = "Classified Info!" # The colored text under the alert icon + status_headline: str = _("Classified Info!") # The colored text under the alert icon status_color: str = GUIConstants.DIRE_WARNING_COLOR - def __post_init__(self): - if not self.status_headline: - self.status_headline = _("Classified Info!") - - super().__post_init__() - @dataclass @@ -1048,13 +1026,12 @@ class PowerOffScreen(BaseTopNavScreen): @dataclass class PowerOffNotRequiredScreen(BaseTopNavScreen): def __post_init__(self): - self.title = "Just Unplug It" + self.title = _("Just Unplug It") self.show_back_button = True super().__post_init__() - text = _("It is safe to disconnect power at any time.") self.components.append(TextArea( - text=text, + text=_("It is safe to disconnect power at any time."), screen_y=self.top_nav.height, height=self.canvas_height - self.top_nav.height, )) diff --git a/src/seedsigner/gui/screens/seed_screens.py b/src/seedsigner/gui/screens/seed_screens.py index 896fd7bc..54e07454 100644 --- a/src/seedsigner/gui/screens/seed_screens.py +++ b/src/seedsigner/gui/screens/seed_screens.py @@ -433,7 +433,6 @@ class SeedFinalizeScreen(ButtonListScreen): @dataclass class SeedOptionsScreen(ButtonListScreen): - # Customize defaults fingerprint: str = None has_passphrase: bool = False diff --git a/src/seedsigner/gui/screens/settings_screens.py b/src/seedsigner/gui/screens/settings_screens.py index a7e3fa23..cf014f7a 100644 --- a/src/seedsigner/gui/screens/settings_screens.py +++ b/src/seedsigner/gui/screens/settings_screens.py @@ -309,12 +309,11 @@ class DonateScreen(BaseTopNavScreen): @dataclass class SettingsQRConfirmationScreen(ButtonListScreen): config_name: str = None - title: str = "Settings QR" - status_message: str = "Settings updated..." + title: str = _("Settings QR") + status_message: str = _("Settings updated...") is_bottom_list: bool = True def __post_init__(self): - self.title = _("Settings QR") # Customize defaults self.button_data = [_("Home")] self.show_back_button = False @@ -323,7 +322,7 @@ class SettingsQRConfirmationScreen(ButtonListScreen): start_y = self.top_nav.height + 20 if self.config_name: self.config_name_textarea = TextArea( - text=f'"{self.config_name}"', + text=f'"{self.config_name}"', # User-supplied string (from SettingsQR); don't wrap to translate is_text_centered=True, auto_line_break=True, screen_y=start_y @@ -332,7 +331,7 @@ class SettingsQRConfirmationScreen(ButtonListScreen): start_y = self.config_name_textarea.screen_y + 50 self.components.append(TextArea( - text=self.status_message, + text=_(self.status_message), is_text_centered=True, auto_line_break=True, screen_y=start_y diff --git a/src/seedsigner/gui/screens/tools_screens.py b/src/seedsigner/gui/screens/tools_screens.py index f71c117a..f0bb42e2 100644 --- a/src/seedsigner/gui/screens/tools_screens.py +++ b/src/seedsigner/gui/screens/tools_screens.py @@ -75,7 +75,7 @@ class ToolsImageEntropyLivePreviewScreen(BaseScreen): int(self.renderer.canvas_width/2), self.renderer.canvas_height - GUIConstants.EDGE_PADDING ), - text="< back | click joystick", + text="< " + _("back") + " | " + _("click joystick"), # TODO: Render with UI elements instead of text fill=GUIConstants.BODY_FONT_COLOR, font=instructions_font, stroke_width=4, @@ -100,13 +100,18 @@ class ToolsImageEntropyFinalImageScreen(BaseScreen): instructions_font = Fonts.get_font(GUIConstants.get_body_font_name(), GUIConstants.get_button_font_size()) self.renderer.canvas.paste(self.final_image) + + # TRANSLATOR_NOTE: A prompt to the user to either accept or reshoot the image + reshoot = _("reshoot") + + # TRANSLATOR_NOTE: A prompt to the user to either accept or reshoot the image + accept = _("accept") self.renderer.draw.text( xy=( int(self.renderer.canvas_width/2), self.renderer.canvas_height - GUIConstants.EDGE_PADDING ), - # TRANSLATOR_NOTE: Instructions when reviewing an image; "reshoot" must point left, "accept" must point right - text=_(" < reshoot | accept > "), + text=" < " + reshoot + " | " + accept + " > ", fill=GUIConstants.BODY_FONT_COLOR, font=instructions_font, stroke_width=4, @@ -253,9 +258,10 @@ class ToolsCalcFinalWordScreen(ButtonListScreen): # significant n bits always rendered in same column) discard_selected_bits = "_" * (len(self.checksum_bits)) + # TRANSLATOR_NOTE: The additional entropy the user supplied (e.g. coin flips) + your_input = _('Your input: "{}"').format(selection_text) self.components.append(TextArea( - # TRANSLATOR_NOTE: A labeled presentation of a user-supplied value - text=_('Your input: "{}"').format(selection_text), + text=your_input, screen_y=self.top_nav.height + GUIConstants.COMPONENT_PADDING - 2, # Nudge to last line doesn't get too close to "Next" button height_ignores_below_baseline=True, # Keep the next line (bits display) snugged up, regardless of text rendering below the baseline )) diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index d57bd3e6..a6fa8200 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -57,6 +57,7 @@ class SettingsConstants: LOCALE__PORTUGUESE = "pt" LOCALE__RUSSIAN = "ru" LOCALE__SPANISH = "es" + # Do not wrap for translation; always present each language in its native form ALL_LOCALES = [ # (LOCALE__ARABIC, "Arabic"), (LOCALE__CZECH, "čeština"), @@ -76,10 +77,10 @@ class SettingsConstants: BTC_DENOMINATION__THRESHOLD = "thr" BTC_DENOMINATION__BTCSATSHYBRID = "hyb" ALL_BTC_DENOMINATIONS = [ - (BTC_DENOMINATION__BTC, "BTC"), - (BTC_DENOMINATION__SATS, "sats"), - (BTC_DENOMINATION__THRESHOLD, "Threshold at 0.01"), - (BTC_DENOMINATION__BTCSATSHYBRID, "BTC | sats hybrid"), + (BTC_DENOMINATION__BTC, _("BTC")), + (BTC_DENOMINATION__SATS, _("sats")), + (BTC_DENOMINATION__THRESHOLD, _("Threshold at 0.01")), + (BTC_DENOMINATION__BTCSATSHYBRID, _("BTC | sats hybrid")), ] CAMERA_ROTATION__0 = 0 @@ -97,10 +98,16 @@ class SettingsConstants: DENSITY__LOW = "L" DENSITY__MEDIUM = "M" DENSITY__HIGH = "H" + # TRANSLATOR_NOTE: QR code density option: Low, Medium, High + density_low = _("Low") + # TRANSLATOR_NOTE: QR code density option: Low, Medium, High + density_medium = _("Medium") + # TRANSLATOR_NOTE: QR code density option: Low, Medium, High + density_high = _("High") ALL_DENSITIES = [ - (DENSITY__LOW, _("Low")), - (DENSITY__MEDIUM, _("Medium")), - (DENSITY__HIGH, _("High")), + (DENSITY__LOW, density_low), + (DENSITY__MEDIUM, density_medium), + (DENSITY__HIGH, density_high), ] # Seed-related constants @@ -115,6 +122,7 @@ class SettingsConstants: @classmethod def map_network_to_embit(cls, network) -> str: + # Note these are `embit` constants; do not wrap for translation if network == SettingsConstants.MAINNET: return "main" elif network == SettingsConstants.TESTNET: @@ -138,11 +146,11 @@ class SettingsConstants: TAPROOT = "tr" CUSTOM_DERIVATION = "cus" ALL_SCRIPT_TYPES = [ - (NATIVE_SEGWIT, "Native Segwit"), - (NESTED_SEGWIT, "Nested Segwit"), - (LEGACY_P2PKH, "Legacy"), - (TAPROOT, "Taproot"), - (CUSTOM_DERIVATION, "Custom Derivation"), + (NATIVE_SEGWIT, _("Native Segwit")), + (NESTED_SEGWIT, _("Nested Segwit")), + (LEGACY_P2PKH, _("Legacy")), + (TAPROOT, _("Taproot")), + (CUSTOM_DERIVATION, _("Custom Derivation")), ] WORDLIST_LANGUAGE__ENGLISH = "en" @@ -166,6 +174,7 @@ class SettingsConstants: # Individual SettingsEntry attr_names + # Note: attr_names are internal constants; do not wrap for translation SETTING__LOCALE = "locale" SETTING__WORDLIST_LANGUAGE = "wordlist_language" SETTING__PERSISTENT_SETTINGS = "persistent_settings" @@ -228,8 +237,10 @@ class SettingsConstants: ELECTRUM_PBKDF2_ROUNDS=2048 # Label strings - LABEL__BIP39_PASSPHRASE = "BIP-39 Passphrase" - LABEL__CUSTOM_EXTENSION = "Custom Extension" # Terminology used by Electrum seeds + LABEL__BIP39_PASSPHRASE = _("BIP-39 Passphrase") + # TRANSLATOR_NOTE: Terminology used by Electrum seeds; equivalent to bip39 passphrase + custom_extension = _("Custom Extension") + LABEL__CUSTOM_EXTENSION = custom_extension @@ -368,8 +379,8 @@ class SettingsDefinition: SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM, attr_name=SettingsConstants.SETTING__LOCALE, - display_name=_("Language"), abbreviated_name="lang", + display_name=_("Language"), type=SettingsConstants.TYPE__SELECT_1, selection_options=SettingsConstants.ALL_LOCALES, default_value=SettingsConstants.LOCALE__ENGLISH), @@ -494,7 +505,7 @@ class SettingsDefinition: attr_name=SettingsConstants.SETTING__ELECTRUM_SEEDS, abbreviated_name="electrum", display_name="Electrum seeds", - help_text="Native Segwit only", + help_text=_("Native Segwit only"), visibility=SettingsConstants.VISIBILITY__ADVANCED, default_value=SettingsConstants.OPTION__DISABLED), diff --git a/src/seedsigner/resources/seedsigner-translations b/src/seedsigner/resources/seedsigner-translations index 7b4f05a4..23bb8f65 160000 --- a/src/seedsigner/resources/seedsigner-translations +++ b/src/seedsigner/resources/seedsigner-translations @@ -1 +1 @@ -Subproject commit 7b4f05a4cf458c9e18d8b3903b6780b9e1250c5b +Subproject commit 23bb8f6533574e36c6c068dc10c62633c29a6e18 diff --git a/src/seedsigner/views/psbt_views.py b/src/seedsigner/views/psbt_views.py index 807a1d08..5b64c474 100644 --- a/src/seedsigner/views/psbt_views.py +++ b/src/seedsigner/views/psbt_views.py @@ -15,11 +15,10 @@ from seedsigner.views.view import BackStackView, MainMenuView, NotYetImplemented class PSBTSelectSeedView(View): - # TODO: Class-level attrs vs dynamic gettext calls - SCAN_SEED = ("Scan a seed", SeedSignerIconConstants.QRCODE) - TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) - TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) - TYPE_ELECTRUM = ("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD) + SCAN_SEED = (_("Scan a seed"), SeedSignerIconConstants.QRCODE) + TYPE_12WORD = (_("Enter 12-word seed"), FontAwesomeIconConstants.KEYBOARD) + TYPE_24WORD = (_("Enter 24-word seed"), FontAwesomeIconConstants.KEYBOARD) + TYPE_ELECTRUM = (_("Enter Electrum seed"), FontAwesomeIconConstants.KEYBOARD) def run(self): @@ -29,9 +28,6 @@ class PSBTSelectSeedView(View): # Shouldn't be able to get here raise Exception("No PSBT currently loaded") - self.SCAN_SEED = (_("Scan a seed"), SeedSignerIconConstants.QRCODE) - self.TYPE_12WORD = (_("Enter 12-word seed"), FontAwesomeIconConstants.KEYBOARD) - self.TYPE_24WORD = (_("Enter 24-word seed"), FontAwesomeIconConstants.KEYBOARD) if self.controller.psbt_seed: if PSBTParser.has_matching_input_fingerprint(psbt=self.controller.psbt, seed=self.controller.psbt_seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)): # skip the seed prompt if a seed was previous selected and has matching input fingerprint @@ -43,7 +39,8 @@ class PSBTSelectSeedView(View): button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)) if not PSBTParser.has_matching_input_fingerprint(psbt=self.controller.psbt, seed=seed, network=self.settings.get_value(SettingsConstants.SETTING__NETWORK)): # Doesn't look like this seed can sign the current PSBT - button_str += " (?)" + # TRANSLATOR_NOTE: Inserts fingerprint w/"?" to indicate that this seed can't sign the current PSBT + button_str = _("{} (?)").format(button_str) button_data.append((button_str, SeedSignerIconConstants.FINGERPRINT)) @@ -303,9 +300,9 @@ class PSBTAddressDetailsView(View): class PSBTChangeDetailsView(View): - NEXT = "Next" - SKIP_VERIFICATION = "Skip Verification" - VERIFY_MULTISIG = "Verify Multisig Change" + NEXT = _("Next") + SKIP_VERIFICATION = _("Skip Verification") + VERIFY_MULTISIG = _("Verify Multisig Change") def __init__(self, change_address_num): @@ -347,12 +344,9 @@ class PSBTChangeDetailsView(View): is_change_derivation_path = int(derivation_path.split("/")[-2]) == 1 derivation_path_addr_index = int(derivation_path.split("/")[-1]) - self.NEXT = _("Next") - if is_change_derivation_path: # TRANSLATOR_NOTE: The amount you're receiving back from the transaction title = _("Your Change") - self.VERIFY_MULTISIG = _("Verify Multisig Change") else: title = _("Self-Transfer") self.VERIFY_MULTISIG = _("Verify Multisig Addr") @@ -569,11 +563,10 @@ class PSBTSignedQRDisplayView(View): class PSBTSigningErrorView(View): - SELECT_DIFF_SEED = "Select Diff Seed" + SELECT_DIFF_SEED = _("Select Diff Seed") def run(self): psbt_parser: PSBTParser = self.controller.psbt_parser - self.SELECT_DIFF_SEED = _("Select Diff Seed") if not psbt_parser: # Should not be able to get here return Destination(MainMenuView) diff --git a/src/seedsigner/views/scan_views.py b/src/seedsigner/views/scan_views.py index 945f65a9..067297f6 100644 --- a/src/seedsigner/views/scan_views.py +++ b/src/seedsigner/views/scan_views.py @@ -62,10 +62,11 @@ class ScanView(View): # current flow. # Report QR types in more human-readable text (e.g. QRType # `seed__compactseedqr` as "seed: compactseedqr"). + # TODO: cleanup l10n presentation return Destination(ErrorView, view_args=dict( title="Error", status_headline=_("Wrong QR Type"), - text=self.invalid_qr_type_message + f""", received "{self.decoder.qr_type.replace("__", ": ").replace("_", " ")}\" format""", + text=_(self.invalid_qr_type_message) + f""", received "{self.decoder.qr_type.replace("__", ": ").replace("_", " ")}\" format""", button_text="Back", next_destination=Destination(BackStackView, skip_current_view=True), )) diff --git a/src/seedsigner/views/seed_views.py b/src/seedsigner/views/seed_views.py index 88d4854a..aab06ce1 100644 --- a/src/seedsigner/views/seed_views.py +++ b/src/seedsigner/views/seed_views.py @@ -78,10 +78,10 @@ class SeedSelectSeedView(View): * `flow`: indicates which user flow is in progress during seed selection (e.g. verify single sig addr or sign message). """ - SCAN_SEED = ("Scan a seed", SeedSignerIconConstants.QRCODE) - TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) - TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) - TYPE_ELECTRUM = ("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD) + SCAN_SEED = (_("Scan a seed"), SeedSignerIconConstants.QRCODE) + TYPE_12WORD = (_("Enter 12-word seed"), FontAwesomeIconConstants.KEYBOARD) + TYPE_24WORD = (_("Enter 24-word seed"), FontAwesomeIconConstants.KEYBOARD) + TYPE_ELECTRUM = (_("Enter Electrum seed"), FontAwesomeIconConstants.KEYBOARD) def __init__(self, flow: str = Controller.FLOW__VERIFY_SINGLESIG_ADDR): @@ -90,9 +90,6 @@ class SeedSelectSeedView(View): def run(self): - self.SCAN_SEED = (_("Scan a seed"), SeedSignerIconConstants.QRCODE) - self.TYPE_12WORD = (_("Enter 12-word seed"), FontAwesomeIconConstants.KEYBOARD) - self.TYPE_24WORD = (_("Enter 24-word seed"), FontAwesomeIconConstants.KEYBOARD) seeds = self.controller.storage.seeds if self.flow == Controller.FLOW__VERIFY_SINGLESIG_ADDR: @@ -172,17 +169,13 @@ class SeedSelectSeedView(View): Loading seeds, passphrases, etc ****************************************************************************""" class LoadSeedView(View): - SEED_QR = ("Scan a SeedQR", SeedSignerIconConstants.QRCODE) - TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) - TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) - TYPE_ELECTRUM = ("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD) - CREATE = (" Create a seed", SeedSignerIconConstants.PLUS) + SEED_QR = (_("Scan a SeedQR"), SeedSignerIconConstants.QRCODE) + TYPE_12WORD = (_("Enter 12-word seed"), FontAwesomeIconConstants.KEYBOARD) + TYPE_24WORD = (_("Enter 24-word seed"), FontAwesomeIconConstants.KEYBOARD) + TYPE_ELECTRUM = (_("Enter Electrum seed"), FontAwesomeIconConstants.KEYBOARD) + CREATE = (_(" Create a seed"), SeedSignerIconConstants.PLUS) def run(self): - self.SEED_QR = (_("Scan a SeedQR"), SeedSignerIconConstants.QRCODE) - self.TYPE_12WORD = (_("Enter 12-word seed"), FontAwesomeIconConstants.KEYBOARD) - self.TYPE_24WORD = (_("Enter 24-word seed"), FontAwesomeIconConstants.KEYBOARD) - self.CREATE = (_("Create a seed"), SeedSignerIconConstants.PLUS) button_data = [ self.SEED_QR, self.TYPE_12WORD, @@ -284,8 +277,8 @@ class SeedMnemonicEntryView(View): class SeedMnemonicInvalidView(View): - EDIT = "Review & Edit" - DISCARD = ("Discard", None, None, "red") + EDIT = _("Review & Edit") + DISCARD = (_("Discard"), None, None, "red") def __init__(self): super().__init__() @@ -293,8 +286,6 @@ class SeedMnemonicInvalidView(View): def run(self): - self.EDIT = _("Review & Edit") - self.DISCARD = (_("Discard"), None, None, "red") button_data = [self.EDIT, self.DISCARD] selected_menu_num = self.run_screen( WarningScreen, @@ -315,7 +306,8 @@ class SeedMnemonicInvalidView(View): class SeedFinalizeView(View): - FINALIZE = "Done" + FINALIZE = _("Done") + PASSPHRASE = _("BIP-39 Passphrase") def __init__(self): super().__init__() @@ -337,9 +329,6 @@ class SeedFinalizeView(View): def run(self): - self.FINALIZE = _("Done") - self.PASSPHRASE = _("BIP-39 Passphrase") - button_data = [self.FINALIZE] passphrase_button = self.seed.passphrase_label if self.settings.get_value(SettingsConstants.SETTING__PASSPHRASE) != SettingsConstants.OPTION__DISABLED: @@ -391,8 +380,8 @@ class SeedAddPassphraseView(View): class SeedAddPassphraseExitDialogView(View): - EDIT = "Edit passphrase" - DISCARD = ("Discard passphrase", None, None, "red") + EDIT = _("Edit passphrase") + DISCARD = (_("Discard passphrase"), None, None, "red") def __init__(self): super().__init__() @@ -404,9 +393,9 @@ class SeedAddPassphraseExitDialogView(View): selected_menu_num = self.run_screen( WarningScreen, - title="Discard passphrase?", + title=_("Discard passphrase?"), status_headline=None, - text=f"Your current passphrase entry will be erased", + text=_("Your current passphrase entry will be erased"), show_back_button=False, button_data=button_data, ) @@ -424,8 +413,8 @@ class SeedReviewPassphraseView(View): """ Display the completed passphrase back to the user. """ - EDIT = "Edit passphrase" - DONE = "Done" + EDIT = _("Edit passphrase") + DONE = _("Done") def __init__(self): super().__init__() @@ -433,9 +422,6 @@ class SeedReviewPassphraseView(View): def run(self): - self.EDIT = _("Edit passphrase") - self.DONE = _("Done") - # Get the before/after fingerprints network = self.settings.get_value(SettingsConstants.SETTING__NETWORK) passphrase = self.seed.passphrase @@ -467,8 +453,8 @@ class SeedReviewPassphraseView(View): class SeedDiscardView(View): - KEEP = "Keep Seed" - DISCARD = ("Discard", None, None, "red") + KEEP = _("Keep Seed") + DISCARD = (_("Discard"), None, None, "red") def __init__(self, seed_num: int = None): super().__init__() @@ -480,9 +466,6 @@ class SeedDiscardView(View): def run(self): - self.KEEP = _("Keep Seed") - self.DISCARD = (_("Discard"), None, None, "red") - button_data = [self.KEEP, self.DISCARD] fingerprint = self.seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)) @@ -522,9 +505,9 @@ class SeedElectrumMnemonicStartView(View): def run(self): self.run_screen( WarningScreen, - title="Electrum warning", + title=_("Electrum warning"), status_headline=None, - text=f"Some features are disabled for Electrum seeds.", + text=_("Some features are disabled for Electrum seeds."), show_back_button=False, ) @@ -538,14 +521,14 @@ class SeedElectrumMnemonicStartView(View): Views for actions on individual seeds: ****************************************************************************""" class SeedOptionsView(View): - SCAN_PSBT = ("Scan PSBT", SeedSignerIconConstants.QRCODE) - VERIFY_ADDRESS = "Verify Addr" - EXPORT_XPUB = "Export Xpub" - EXPLORER = "Address Explorer" - SIGN_MESSAGE = "Sign Message" - BACKUP = ("Backup Seed", None, None, None, SeedSignerIconConstants.CHEVRON_RIGHT) - BIP85_CHILD_SEED = "BIP-85 Child Seed" - DISCARD = ("Discard Seed", None, None, "red") + SCAN_PSBT = (_("Scan PSBT"), SeedSignerIconConstants.QRCODE) + VERIFY_ADDRESS = _("Verify Addr") + EXPORT_XPUB = _("Export Xpub") + EXPLORER = _("Address Explorer") + SIGN_MESSAGE = _("Sign Message") + BACKUP = (_("Backup Seed"), None, None, None, SeedSignerIconConstants.CHEVRON_RIGHT) + BIP85_CHILD_SEED = _("BIP-85 Child Seed") + DISCARD = (_("Discard Seed"), None, None, "red") def __init__(self, seed_num: int): @@ -557,13 +540,6 @@ class SeedOptionsView(View): def run(self): from seedsigner.views.psbt_views import PSBTOverviewView - self.SCAN_PSBT = (_("Scan PSBT"), SeedSignerIconConstants.QRCODE) - self.VERIFY_ADDRESS = _("Verify Addr") - self.EXPORT_XPUB = _("Export Xpub") - self.EXPLORER = _("Address Explorer") - self.BACKUP = (_("Backup Seed"), None, None, None, SeedSignerIconConstants.CHEVRON_RIGHT) - self.BIP85_CHILD_SEED = _("BIP-85 Child Seed") - self.DISCARD = (_("Discard Seed"), None, None, "red") if self.controller.unverified_address: if self.controller.resume_main_flow == Controller.FLOW__VERIFY_SINGLESIG_ADDR: # Jump straight back into the single sig addr verification flow @@ -655,8 +631,8 @@ class SeedOptionsView(View): class SeedBackupView(View): - VIEW_WORDS = "View Seed Words" - EXPORT_SEEDQR = "Export as SeedQR" + VIEW_WORDS = _("View Seed Words") + EXPORT_SEEDQR = _("Export as SeedQR") def __init__(self, seed_num): super().__init__() @@ -665,8 +641,6 @@ class SeedBackupView(View): def run(self): - self.VIEW_WORDS = _("View Seed Words") - self.EXPORT_SEEDQR = _("Export as SeedQR") button_data = [self.VIEW_WORDS] if self.seed.seedqr_supported: @@ -694,8 +668,8 @@ class SeedBackupView(View): Export Xpub flow ****************************************************************************""" class SeedExportXpubSigTypeView(View): - SINGLE_SIG = "Single Sig" - MULTISIG = "Multisig" + SINGLE_SIG = _("Single Sig") + MULTISIG = _("Multisig") def __init__(self, seed_num: int): super().__init__() @@ -707,8 +681,6 @@ class SeedExportXpubSigTypeView(View): # Nothing to select; skip this screen return Destination(SeedExportXpubScriptTypeView, view_args={"seed_num": self.seed_num, "sig_type": self.settings.get_value(SettingsConstants.SETTING__SIG_TYPES)[0]}, skip_current_view=True) - self.SINGLE_SIG = _("Single Sig") - self.MULTISIG = _("Multisig") button_data=[self.SINGLE_SIG, self.MULTISIG] selected_menu_num = self.run_screen( @@ -765,7 +737,7 @@ class SeedExportXpubScriptTypeView(View): button_data = [] for script_type, display_name in SettingsConstants.ALL_SCRIPT_TYPES: if script_type in self.settings.get_value(SettingsConstants.SETTING__SCRIPT_TYPES): - button_data.append((script_type, _(display_name))) + button_data.append((script_type, display_name)) selected_menu_num = self.run_screen( ButtonListScreen, @@ -1071,6 +1043,9 @@ class SeedWordsWarningView(View): class SeedWordsView(View): + NEXT = _("Next") + DONE = _("Done") + def __init__(self, seed_num: int, bip85_data: dict = None, page_index: int = 0): super().__init__() self.seed_num = seed_num @@ -1083,9 +1058,6 @@ class SeedWordsView(View): def run(self): - NEXT = _("Next") - DONE = _("Done") - # Slice the mnemonic to our current 4-word section words_per_page = 4 # TODO: eventually make this configurable for bigger screens? @@ -1101,9 +1073,9 @@ class SeedWordsView(View): button_data = [] num_pages = int(len(mnemonic)/words_per_page) if self.page_index < num_pages - 1 or self.seed_num is None: - button_data.append(NEXT) + button_data.append(self.NEXT) else: - button_data.append(DONE) + button_data.append(self.DONE) selected_menu_num = seed_screens.SeedWordsScreen( title=f"{title}: {self.page_index+1}/{num_pages}", @@ -1116,7 +1088,7 @@ class SeedWordsView(View): if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - if button_data[selected_menu_num] == NEXT: + if button_data[selected_menu_num] == self.NEXT: if self.seed_num is None and self.page_index == num_pages - 1: return Destination( SeedWordsBackupTestPromptView, @@ -1128,7 +1100,7 @@ class SeedWordsView(View): view_args=dict(seed_num=self.seed_num, page_index=self.page_index + 1, bip85_data=self.bip85_data) ) - elif button_data[selected_menu_num] == DONE: + elif button_data[selected_menu_num] == self.DONE: # Must clear history to avoid BACK button returning to private info return Destination( SeedWordsBackupTestPromptView, @@ -1148,6 +1120,10 @@ class SeedBIP85ApplicationModeView(View): * WIF (HDSEED) * XPRV (BIP32) """ + # TODO: Future enhancement to display WIF (HD-SEED) and XPRV (Bip32)? + WORDS_12 = _("12 Words") + WORDS_24 = _("24 Words") + def __init__(self, seed_num: int): super().__init__() self.seed_num = seed_num @@ -1156,11 +1132,7 @@ class SeedBIP85ApplicationModeView(View): def run(self): - # TODO: Future enhancement to display WIF (HD-SEED) and XPRV (Bip32)? - WORDS_12 = _("12 Words") - WORDS_24 = _("24 Words") - - button_data = [WORDS_12, WORDS_24] + button_data = [self.WORDS_12, self.WORDS_24] selected_menu_num = ButtonListScreen( title=_("BIP-85 Num Words"), @@ -1170,9 +1142,9 @@ class SeedBIP85ApplicationModeView(View): if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - if button_data[selected_menu_num] == WORDS_12: + if button_data[selected_menu_num] == self.WORDS_12: self.num_words = 12 - elif button_data[selected_menu_num] == WORDS_24: + elif button_data[selected_menu_num] == self.WORDS_24: self.num_words = 24 return Destination( @@ -1247,6 +1219,9 @@ class SeedBIP85InvalidChildIndexView(View): Seed Words Backup Test ****************************************************************************""" class SeedWordsBackupTestPromptView(View): + VERIFY = _("Verify") + SKIP = _("Skip") + def __init__(self, seed_num: int, bip85_data: dict = None): super().__init__() self.seed_num = seed_num @@ -1254,20 +1229,18 @@ class SeedWordsBackupTestPromptView(View): def run(self): - VERIFY = _("Verify") - SKIP = _("Skip") - button_data = [VERIFY, SKIP] + button_data = [self.VERIFY, self.SKIP] selected_menu_num = seed_screens.SeedWordsBackupTestPromptScreen( button_data=button_data, ).display() - if button_data[selected_menu_num] == VERIFY: + if button_data[selected_menu_num] == self.VERIFY: return Destination( SeedWordsBackupTestView, view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data), ) - elif button_data[selected_menu_num] == SKIP: + elif button_data[selected_menu_num] == self.SKIP: if self.seed_num is not None: return Destination(SeedOptionsView, view_args=dict(seed_num=self.seed_num)) else: @@ -1352,6 +1325,9 @@ class SeedWordsBackupTestView(View): class SeedWordsBackupTestMistakeView(View): + REVIEW = _("Review Seed Words") + RETRY = _("Try Again") + def __init__(self, seed_num: int, bip85_data: dict = None, cur_index: int = None, wrong_word: str = None, confirmed_list: List[bool] = None): super().__init__() self.seed_num = seed_num @@ -1362,9 +1338,7 @@ class SeedWordsBackupTestMistakeView(View): def run(self): - REVIEW = _("Review Seed Words") - RETRY = _("Try Again") - button_data = [REVIEW, RETRY] + button_data = [self.REVIEW, self.RETRY] # TRANSLATOR_NOTE: Inserts the word number and the word (e.g. "Word #1 is not "apple"!") text = _("Word #{} is not \"{}\"!").format(self.cur_index + 1, self.wrong_word) @@ -1380,13 +1354,13 @@ class SeedWordsBackupTestMistakeView(View): text=text, ).display() - if button_data[selected_menu_num] == REVIEW: + if button_data[selected_menu_num] == self.REVIEW: return Destination( SeedWordsView, view_args=dict(seed_num=self.seed_num, bip85_data=self.bip85_data), ) - elif button_data[selected_menu_num] == RETRY: + elif button_data[selected_menu_num] == self.RETRY: return Destination( SeedWordsBackupTestView, view_args=dict( @@ -1598,6 +1572,9 @@ class SeedTranscribeSeedQRZoomedInView(View): class SeedTranscribeSeedQRConfirmQRPromptView(View): + SCAN = (_("Confirm SeedQR"), SeedSignerIconConstants.QRCODE) + DONE = _("Done") + def __init__(self, seed_num: int): super().__init__() self.seed_num = seed_num @@ -1605,9 +1582,7 @@ class SeedTranscribeSeedQRConfirmQRPromptView(View): def run(self): - SCAN = (_("Confirm SeedQR"), SeedSignerIconConstants.QRCODE) - DONE = _("Done") - button_data = [SCAN, DONE] + button_data = [self.SCAN, self.DONE] selected_menu_option = seed_screens.SeedTranscribeSeedQRConfirmQRPromptScreen( title=_("Confirm SeedQR?"), @@ -1617,10 +1592,10 @@ class SeedTranscribeSeedQRConfirmQRPromptView(View): if selected_menu_option == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - elif button_data[selected_menu_option] == SCAN: + elif button_data[selected_menu_option] == self.SCAN: return Destination(SeedTranscribeSeedQRConfirmScanView, view_args={"seed_num": self.seed_num}) - elif button_data[selected_menu_option] == DONE: + elif button_data[selected_menu_option] == self.DONE: return Destination(SeedOptionsView, view_args={"seed_num": self.seed_num}, clear_history=True) @@ -1739,13 +1714,8 @@ class AddressVerificationStartView(View): class AddressVerificationSigTypeView(View): - SINGLE_SIG = "Single Sig" - MULTISIG = "Multisig" - - def __post_init__(self): - self.SINGLE_SIG = _("Single Sig") - self.MULTISIG = _("Multisig") - return super().__post_init__() + SINGLE_SIG = _("Single Sig") + MULTISIG = _("Multisig") def run(self): button_data = [self.SINGLE_SIG, self.MULTISIG] @@ -1796,6 +1766,10 @@ class SeedAddressVerificationView(View): Performs single sig verification on `seed_num` if specified, otherwise assumes multisig. """ + # TRANSLATOR_NOTE: Option when scanning for a matching address; skips ten addresses ahead + SKIP_10 = _("Skip 10") + CANCEL = _("Cancel") + def __init__(self, seed_num: int = None): super().__init__() self.seed_num = seed_num @@ -1849,10 +1823,7 @@ class SeedAddressVerificationView(View): # Start brute-force calculations from the zero-th index self.addr_verification_thread.start() - # TRANSLATOR_NOTE: Option when scanning for a matching address; skips ten addresses ahead - SKIP_10 = _("Skip 10") - CANCEL = _("Cancel") - button_data = [SKIP_10, CANCEL] + button_data = [self.SKIP_10, self.CANCEL] script_type_settings_entry = SettingsDefinition.get_settings_entry(SettingsConstants.SETTING__SCRIPT_TYPES) script_type_display = script_type_settings_entry.get_selection_option_display_name_by_value(self.script_type) @@ -1894,10 +1865,10 @@ class SeedAddressVerificationView(View): time.sleep(0.1) continue - if button_data[selected_menu_num] == SKIP_10: + if button_data[selected_menu_num] == self.SKIP_10: self.threadsafe_counter.increment(10) - elif button_data[selected_menu_num] == CANCEL: + elif button_data[selected_menu_num] == self.CANCEL: break if self.verified_index.cur_count is not None: @@ -2013,20 +1984,11 @@ class AddressVerificationSuccessView(View): class LoadMultisigWalletDescriptorView(View): - SCAN = ("Scan Descriptor", SeedSignerIconConstants.QRCODE) - CANCEL = "Cancel" - - def __post_init__(self): - self.SCAN = (_("Scan Descriptor"), SeedSignerIconConstants.QRCODE) - self.CANCEL = _("Cancel") - return super().__post_init__() - + SCAN = (_("Scan Descriptor"), SeedSignerIconConstants.QRCODE) + CANCEL = _("Cancel") def run(self): - button_data = [ - self.SCAN, - self.CANCEL - ] + button_data = [self.SCAN, self.CANCEL] selected_menu_num = self.run_screen( seed_screens.LoadMultisigWalletDescriptorScreen, button_data=button_data, @@ -2046,18 +2008,10 @@ class LoadMultisigWalletDescriptorView(View): class MultisigWalletDescriptorView(View): - RETURN = "Return to PSBT" - VERIFY_ADDR = "Verify Addr" - ADDRESS_EXPLORER = "Address Explorer" - OK = "OK" - - def __post_init__(self): - self.RETURN = _("Return to PSBT") - self.VERIFY = _("Verify Addr") - self.EXPLORER = _("Address Explorer") - self.OK = _("OK") - super().__post_init__() - + RETURN = _("Return to PSBT") + VERIFY_ADDR = _("Verify Addr") + ADDRESS_EXPLORER = _("Address Explorer") + OK = _("OK") def run(self): descriptor = self.controller.multisig_wallet_descriptor diff --git a/src/seedsigner/views/settings_views.py b/src/seedsigner/views/settings_views.py index 81636556..5284fac6 100644 --- a/src/seedsigner/views/settings_views.py +++ b/src/seedsigner/views/settings_views.py @@ -16,8 +16,8 @@ from .view import View, Destination, BackStackView, MainMenuView class SettingsMenuView(View): - IO_TEST = "I/O test" - DONATE = "Donate" + IO_TEST = _("I/O test") + DONATE = _("Donate") def __init__(self, visibility: str = SettingsConstants.VISIBILITY__GENERAL, selected_attr: str = None, initial_scroll: int = 0): super().__init__() @@ -29,13 +29,10 @@ class SettingsMenuView(View): def run(self): - self.IO_TEST = _("I/O test") - self.DONATE = _("Donate") - settings_entries = SettingsDefinition.get_settings_entries( visibility=self.visibility ) - button_data=[_(e.display_name) for e in settings_entries] + button_data=[e.display_name for e in settings_entries] selected_button = 0 if self.selected_attr: @@ -121,7 +118,7 @@ class SettingsEntryUpdateSelectionView(View): value, display_name = value else: display_name = value - button_data.append(_(display_name)) + button_data.append(display_name) if (type(initial_value) == list and value in initial_value) or value == initial_value: checked_buttons.append(i) diff --git a/src/seedsigner/views/tools_views.py b/src/seedsigner/views/tools_views.py index 75071a93..7679dd91 100644 --- a/src/seedsigner/views/tools_views.py +++ b/src/seedsigner/views/tools_views.py @@ -27,20 +27,11 @@ logger = logging.getLogger(__name__) class ToolsMenuView(View): - IMAGE = ("New seed", FontAwesomeIconConstants.CAMERA) - DICE = ("New seed", FontAwesomeIconConstants.DICE) - KEYBOARD = ("Calc 12th/24th word", FontAwesomeIconConstants.KEYBOARD) - ADDRESS_EXPLORER = "Address Explorer" - VERIFY_ADDRESS = "Verify address" - - def __post_init__(self): - self.IMAGE = (_("New seed"), FontAwesomeIconConstants.CAMERA) - self.DICE = (_("New seed"), FontAwesomeIconConstants.DICE) - self.KEYBOARD = (_("Calc 12th/24th word"), FontAwesomeIconConstants.KEYBOARD) - self.ADDRESS_EXPLORER = _("Address Explorer") - self.ADDRESS = _("Verify Address") - super().__post_init__() - + IMAGE = (_("New seed"), FontAwesomeIconConstants.CAMERA) + DICE = (_("New seed"), FontAwesomeIconConstants.DICE) + KEYBOARD = (_("Calc 12th/24th word"), FontAwesomeIconConstants.KEYBOARD) + ADDRESS_EXPLORER = _("Address Explorer") + VERIFY_ADDRESS = _("Verify Address") def run(self): button_data = [self.IMAGE, self.DICE, self.KEYBOARD, self.ADDRESS_EXPLORER, self.VERIFY_ADDRESS] @@ -126,10 +117,11 @@ class ToolsImageEntropyFinalImageView(View): class ToolsImageEntropyMnemonicLengthView(View): + TWELVE_WORDS = _("12 words") + TWENTYFOUR_WORDS = _("24 words") + def run(self): - TWELVE_WORDS = _("12 words") - TWENTYFOUR_WORDS = _("24 words") - button_data = [TWELVE_WORDS, TWENTYFOUR_WORDS] + button_data = [self.TWELVE_WORDS, self.TWENTYFOUR_WORDS] selected_menu_num = ButtonListScreen( title=_("Mnemonic Length?"), @@ -139,7 +131,7 @@ class ToolsImageEntropyMnemonicLengthView(View): if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - if button_data[selected_menu_num] == TWELVE_WORDS: + if button_data[selected_menu_num] == self.TWELVE_WORDS: mnemonic_length = 12 else: mnemonic_length = 24 @@ -198,14 +190,15 @@ class ToolsImageEntropyMnemonicLengthView(View): Dice rolls Views ****************************************************************************""" class ToolsDiceEntropyMnemonicLengthView(View): - def run(self): - # TRANSLATOR_NOTE: Inserts the number of dice rolls needed for a 12-word mnemonic - TWELVE = _("12 words ({} rolls)").format(mnemonic_generation.DICE__NUM_ROLLS__12WORD) + # TRANSLATOR_NOTE: Inserts the number of dice rolls needed for a 12-word mnemonic + TWELVE = _("12 words ({} rolls)").format(mnemonic_generation.DICE__NUM_ROLLS__12WORD) - # TRANSLATOR_NOTE: Inserts the number of dice rolls needed for a 24-word mnemonic - TWENTY_FOUR = _("24 words ({} rolls)").format(mnemonic_generation.DICE__NUM_ROLLS__24WORD) - - button_data = [TWELVE, TWENTY_FOUR] + # TRANSLATOR_NOTE: Inserts the number of dice rolls needed for a 24-word mnemonic + TWENTY_FOUR = _("24 words ({} rolls)").format(mnemonic_generation.DICE__NUM_ROLLS__24WORD) + + + def run(self): + button_data = [self.TWELVE, self.TWENTY_FOUR] selected_menu_num = ButtonListScreen( title=_("Mnemonic Length"), is_bottom_list=True, @@ -216,10 +209,10 @@ class ToolsDiceEntropyMnemonicLengthView(View): if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - elif button_data[selected_menu_num] == TWELVE: + elif button_data[selected_menu_num] == self.TWELVE: return Destination(ToolsDiceEntropyEntryView, view_args=dict(total_rolls=mnemonic_generation.DICE__NUM_ROLLS__12WORD)) - elif button_data[selected_menu_num] == TWENTY_FOUR: + elif button_data[selected_menu_num] == self.TWENTY_FOUR: return Destination(ToolsDiceEntropyEntryView, view_args=dict(total_rolls=mnemonic_generation.DICE__NUM_ROLLS__24WORD)) @@ -253,13 +246,10 @@ class ToolsDiceEntropyEntryView(View): Calc final word Views ****************************************************************************""" class ToolsCalcFinalWordNumWordsView(View): - TWELVE = "12 words" - TWENTY_FOUR = "24 words" + TWELVE = _("12 words") + TWENTY_FOUR = _("24 words") def run(self): - self.TWELVE = _("12 words") - self.TWENTY_FOUR = _("24 words") - button_data = [self.TWELVE, self.TWENTY_FOUR] selected_menu_num = self.run_screen( @@ -288,6 +278,15 @@ class ToolsCalcFinalWordNumWordsView(View): class ToolsCalcFinalWordFinalizePromptView(View): + # TRANSLATOR_NOTE: Label to gather entropy through coin tosses + COIN_FLIPS = _("Coin flip entropy") + + # TRANSLATOR_NOTE: Label to gather entropy through user specified BIP-39 word + SELECT_WORD = _("Word selection entropy") + + # TRANSLATOR_NOTE: Label to allow user to default entropy as all-zeros + ZEROS = _("Finalize with zeros") + def run(self): mnemonic = self.controller.storage.pending_mnemonic mnemonic_length = len(mnemonic) @@ -296,14 +295,7 @@ class ToolsCalcFinalWordFinalizePromptView(View): else: num_entropy_bits = 3 - # TRANSLATOR_NOTE: Label to gather entropy through coin tosses - COIN_FLIPS = _("Coin flip entropy") - # TRANSLATOR_NOTE: Label to gather entropy through user specified BIP-39 word - SELECT_WORD = _("Word selection entropy") - # TRANSLATOR_NOTE: Label to allow user to default entropy as all-zeros - ZEROS = _("Finalize with zeros") - - button_data = [COIN_FLIPS, SELECT_WORD, ZEROS] + button_data = [self.COIN_FLIPS, self.SELECT_WORD, self.ZEROS] selected_menu_num = ToolsCalcFinalWordFinalizePromptScreen( mnemonic_length=mnemonic_length, num_entropy_bits=num_entropy_bits, @@ -313,15 +305,15 @@ class ToolsCalcFinalWordFinalizePromptView(View): if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - elif button_data[selected_menu_num] == COIN_FLIPS: + elif button_data[selected_menu_num] == self.COIN_FLIPS: return Destination(ToolsCalcFinalWordCoinFlipsView) - elif button_data[selected_menu_num] == SELECT_WORD: + elif button_data[selected_menu_num] == self.SELECT_WORD: # Clear the final word slot, just in case we're returning via BACK button self.controller.storage.update_pending_mnemonic(None, mnemonic_length - 1) return Destination(SeedMnemonicEntryView, view_args=dict(is_calc_final_word=True, cur_word_index=mnemonic_length - 1)) - elif button_data[selected_menu_num] == ZEROS: + elif button_data[selected_menu_num] == self.ZEROS: # User skipped the option to select a final word to provide last bits of # entropy. We'll insert all zeros and piggy-back on the coin flip attr wordlist_language_code = self.settings.get_value(SettingsConstants.SETTING__WORDLIST_LANGUAGE) @@ -352,6 +344,8 @@ class ToolsCalcFinalWordCoinFlipsView(View): class ToolsCalcFinalWordShowFinalWordView(View): + NEXT = _("Next") + def __init__(self, coin_flips: str = None): super().__init__() # Construct the actual final word. The user's selected_final_word @@ -404,9 +398,7 @@ class ToolsCalcFinalWordShowFinalWordView(View): def run(self): - # l10n_note already exists. - NEXT = _("Next") - button_data = [NEXT] + button_data = [self.NEXT] selected_menu_num = self.run_screen( ToolsCalcFinalWordScreen, # TRANSLATOR_NOTE: label to calculate the last word of a BIP-39 mnemonic seed phrase @@ -421,20 +413,21 @@ class ToolsCalcFinalWordShowFinalWordView(View): if selected_menu_num == RET_CODE__BACK_BUTTON: return Destination(BackStackView) - elif button_data[selected_menu_num] == NEXT: + elif button_data[selected_menu_num] == self.NEXT: return Destination(ToolsCalcFinalWordDoneView) class ToolsCalcFinalWordDoneView(View): + LOAD = _("Load seed") + DISCARD = (_("Discard"), None, None, "red") + def run(self): mnemonic = self.controller.storage.pending_mnemonic mnemonic_word_length = len(mnemonic) final_word = mnemonic[-1] - LOAD = _("Load seed") - DISCARD = (_("Discard"), None, None, "red") - button_data = [LOAD, DISCARD] + button_data = [self.LOAD, self.DISCARD] selected_menu_num = ToolsCalcFinalWordDoneScreen( final_word=final_word, @@ -448,30 +441,26 @@ class ToolsCalcFinalWordDoneView(View): self.controller.storage.convert_pending_mnemonic_to_pending_seed() - if button_data[selected_menu_num] == LOAD: + if button_data[selected_menu_num] == self.LOAD: return Destination(SeedFinalizeView) - elif button_data[selected_menu_num] == DISCARD: + elif button_data[selected_menu_num] == self.DISCARD: return Destination(SeedDiscardView) + """**************************************************************************** Address Explorer Views ****************************************************************************""" class ToolsAddressExplorerSelectSourceView(View): - SCAN_SEED = ("Scan a seed", SeedSignerIconConstants.QRCODE) - SCAN_DESCRIPTOR = ("Scan wallet descriptor", SeedSignerIconConstants.QRCODE) - TYPE_12WORD = ("Enter 12-word seed", FontAwesomeIconConstants.KEYBOARD) - TYPE_24WORD = ("Enter 24-word seed", FontAwesomeIconConstants.KEYBOARD) - TYPE_ELECTRUM = ("Enter Electrum seed", FontAwesomeIconConstants.KEYBOARD) - + SCAN_SEED = (_("Scan a seed"), SeedSignerIconConstants.QRCODE) + SCAN_DESCRIPTOR = (_("Scan wallet descriptor"), SeedSignerIconConstants.QRCODE) + TYPE_12WORD = (_("Enter 12-word seed"), FontAwesomeIconConstants.KEYBOARD) + TYPE_24WORD = (_("Enter 24-word seed"), FontAwesomeIconConstants.KEYBOARD) + TYPE_ELECTRUM = (_("Enter Electrum seed"), FontAwesomeIconConstants.KEYBOARD) def run(self): seeds = self.controller.storage.seeds - self.SCAN_SEED = (_("Scan a seed"), SeedSignerIconConstants.QRCODE) - self.SCAN_DESCRIPTOR = (_("Scan wallet descriptor"), SeedSignerIconConstants.QRCODE) - self.TYPE_12WORD = (_("Enter 12-word seed"), FontAwesomeIconConstants.KEYBOARD) - self.TYPE_24WORD = (_("Enter 24-word seed"), FontAwesomeIconConstants.KEYBOARD) button_data = [] for seed in seeds: button_str = seed.get_fingerprint(self.settings.get_value(SettingsConstants.SETTING__NETWORK)) @@ -529,8 +518,11 @@ class ToolsAddressExplorerSelectSourceView(View): class ToolsAddressExplorerAddressTypeView(View): - RECEIVE = "Receive Addresses" - CHANGE = "Change Addresses" + # TRANSLATOR_NOTE: label for addresses where others send us incoming payments + RECEIVE = _("Receive Addresses") + + # TRANSLATOR_NOTE: label for addresses that collect the change from our own outgoing payments + CHANGE = _("Change Addresses") def __init__(self, seed_num: int = None, script_type: str = None, custom_derivation: str = None): @@ -584,11 +576,6 @@ class ToolsAddressExplorerAddressTypeView(View): def run(self): data = self.controller.address_explorer_data - # TRANSLATOR_NOTE: label for addresses where others send us incoming payments - self.RECEIVE = _("Receive Addresses") - # TRANSLATOR_NOTE: label for addresses that collect the change from our own outgoing payments - self.CHANGE = _("Change Addresses") - wallet_descriptor_display_name = None if "wallet_descriptor" in data: wallet_descriptor_display_name = data["wallet_descriptor"].brief_policy.replace(" (sorted)", "") diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index 6dbf9eca..0f157db9 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -182,17 +182,14 @@ class Destination: # ######################################################################################### class MainMenuView(View): - SCAN = ("Scan", SeedSignerIconConstants.SCAN) - SEEDS = ("Seeds", SeedSignerIconConstants.SEEDS) - TOOLS = ("Tools", SeedSignerIconConstants.TOOLS) - SETTINGS = ("Settings", SeedSignerIconConstants.SETTINGS) + SCAN = (_("Scan"), SeedSignerIconConstants.SCAN) + SEEDS = (_("Seeds"), SeedSignerIconConstants.SEEDS) + TOOLS = (_("Tools"), SeedSignerIconConstants.TOOLS) + SETTINGS = (_("Settings"), SeedSignerIconConstants.SETTINGS) def run(self): - self.SCAN = (_("Scan"), SeedSignerIconConstants.SCAN) - self.SEEDS = (_("Seeds"), SeedSignerIconConstants.SEEDS) - self.TOOLS = (_("Tools"), SeedSignerIconConstants.TOOLS) - self.SETTINGS = (_("Settings"), SeedSignerIconConstants.SETTINGS) from seedsigner.gui.screens.screen import MainMenuScreen + button_data = [self.SCAN, self.SEEDS, self.TOOLS, self.SETTINGS] selected_menu_num = self.run_screen( MainMenuScreen, @@ -222,12 +219,10 @@ class MainMenuView(View): class PowerOptionsView(View): - RESET = ("Restart", SeedSignerIconConstants.RESTART) - POWER_OFF = ("Power Off", SeedSignerIconConstants.POWER) + RESET = (_("Restart"), SeedSignerIconConstants.RESTART) + POWER_OFF = (_("Power Off"), SeedSignerIconConstants.POWER) def run(self): - self.RESET = (_("Restart"), SeedSignerIconConstants.RESTART) - self.POWER_OFF = (_("Power Off"), SeedSignerIconConstants.POWER) button_data = [self.RESET, self.POWER_OFF] selected_menu_num = self.run_screen( LargeButtonScreen, @@ -321,7 +316,7 @@ class NotYetImplementedView(View): @dataclass class ErrorView(View): - title: str = "Error" + title: str = _("Error") show_back_button: bool = True status_headline: str = None text: str = None @@ -330,7 +325,6 @@ class ErrorView(View): def run(self): - self.title = _("Error") self.run_screen( WarningScreen, title=self.title, @@ -346,16 +340,14 @@ class ErrorView(View): @dataclass class NetworkMismatchErrorView(ErrorView): - title: str = "Network Mismatch" + title: str = _("Network Mismatch") show_back_button: bool = False - button_text: str = "Change Setting" + button_text: str = _("Change Setting") next_destination: Destination = None def __post_init__(self): super().__post_init__() - self.title = _("Network Mismatch") - self.button_text = _("Change Setting") if not self.text: # TRANSLATOR_NOTE: Inserts mainnet/testnet/regtest self.text = _("Current network setting ({}) doesn't match current action.").format( @@ -377,6 +369,8 @@ class UnhandledExceptionView(View): self.run_screen( DireWarningScreen, title=_("System Error"), + status_headline=self.error[0], + text=self.error[1] + "\n" + self.error[2], allow_text_overflow=True, # Fit what we can, let the rest go off the edges ) diff --git a/tests/screenshot_generator/generator.py b/tests/screenshot_generator/generator.py index 2cb04bed..9a8431a8 100644 --- a/tests/screenshot_generator/generator.py +++ b/tests/screenshot_generator/generator.py @@ -342,8 +342,8 @@ def test_generate_screenshots(target_locale): time.sleep(0.1) #jdlcdl - # Parse the main `babel/messages.pot` for overall stats - messages_source_path = os.path.join(pathlib.Path(__file__).parent.resolve().parent.resolve().parent.resolve(), "babel", "messages.pot") + # Parse the main `l10n/messages.pot` for overall stats + messages_source_path = os.path.join(pathlib.Path(__file__).parent.resolve().parent.resolve().parent.resolve(), "l10n", "messages.pot") with open(messages_source_path, 'r') as messages_source_file: num_source_messages = messages_source_file.read().count("msgid \"") - 1