diff --git a/.gitignore b/.gitignore index 6ae5b379..a40407f0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ src/seedsigner.egg-info/ .nova .vscode src/seedsigner/models/settings_definition.json +.mo diff --git a/babel/README.md b/babel/README.md new file mode 100644 index 00000000..8622dc23 --- /dev/null +++ b/babel/README.md @@ -0,0 +1,71 @@ +# 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) + ``` + + +### Rescanning for text that needs translations +Re-generate the `messages.pot` file: +``` +pybabel extract -N -F babel/babel.cfg -o babel/messages.pot . +``` +This will rescan all wrapped text, picking up new strings as well as updating existings strings that have been edited. + +Then run `update`: +``` +pybabel update -N --no-wrap -i babel/messages.pot -d src/seedsigner/resources/babel + +# Or target a specific language code: +pybabel update -N --no-wrap -i babel/messages.pot -d src/seedsigner/resources/babel -l es +``` +_note: the `-N` flag prevents babel from trying to use fuzzy matching to re-use existing translations for new strings. The fuzzy matching does not seem to do what we would want so we keep it disabled._ + +Any newly wrapped text strings will be added to each `messages.po` file. Altered strings will be flagged as needing review to see if the existing translations can still be used. + +Once the next round of translations is complete, recompile the results: +``` +pybabel compile -d src/seedsigner/resources/babel + +# Or target a specific language code: +pybabel compile -d src/seedsigner/resources/babel -l es +``` + + +### Adding support for another language +Assuming you have `extract`ed an updated `messages.pot`, all you have to do is generate the initial version of each new language file: +``` +pybabel init -i babel/messages.pot -d src/seedsigner/resources/babel -l es +pybabel init -i babel/messages.pot -d src/seedsigner/resources/babel -l fr +pybabel init -i babel/messages.pot -d src/seedsigner/resources/babel -l de +``` + +Then `update` and `compile` as above. + +The only other step is to add the new language to the LOCALES list in `SettingsDefinition`. + + +## For Packagers: +The *.mo files are not checked into the repository as (large) binary (large) objects ("BLOBs") should not be checked into git to reduce repo bloat. So they need to be generated after fetching the code: + +``` +pybabel compile -d src/seedsigner/resources/babel +``` + +In order to make that as transparent as possible, that procedure has been integrated into the setup-process. So it will be executed by: + +``` +python3 setup.py install +``` + +Unfortunately, it won't be executed by `pip3 install -e .` even though that has been propagated very long to be the developement-env installation procedure. \ No newline at end of file diff --git a/babel/babel.cfg b/babel/babel.cfg new file mode 100644 index 00000000..50b31f30 --- /dev/null +++ b/babel/babel.cfg @@ -0,0 +1 @@ +[python: src/**/**.py] \ No newline at end of file diff --git a/babel/messages.pot b/babel/messages.pot new file mode 100644 index 00000000..6bdcaaa3 --- /dev/null +++ b/babel/messages.pot @@ -0,0 +1,38 @@ +# Translations template for PROJECT. +# Copyright (C) 2022 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2022. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2022-04-29 08:48-0500\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.10.1\n" + +#: src/seedsigner/views/view.py:132 +msgid "Scan" +msgstr "" + +#: src/seedsigner/views/view.py:133 +msgid "Seeds" +msgstr "" + +#: src/seedsigner/views/view.py:134 +msgid "Tools" +msgstr "" + +#: src/seedsigner/views/view.py:135 +msgid "Settings" +msgstr "" + +#: src/seedsigner/views/view.py:139 +msgid "Home" +msgstr "" diff --git a/requirements.txt b/requirements.txt index 1fb2f7b7..1b8b8531 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,9 @@ +Babel==2.10.1 embit==0.4.10 numpy==1.21.1 picamera==1.13 Pillow==8.2.0 +pytz==2022.1 -e git+https://github.com/seedsigner/pyzbar.git@c3c237821c6a20b17953efe59b90df0b514a1c03#egg=pyzbar qrcode==7.3.1 RPi.GPIO==0.7.0 diff --git a/src/seedsigner/controller.py b/src/seedsigner/controller.py index 2cf7bef8..5a8d47ad 100644 --- a/src/seedsigner/controller.py +++ b/src/seedsigner/controller.py @@ -1,5 +1,6 @@ import logging import traceback +import gettext from embit.descriptor import Descriptor from embit.psbt import PSBT @@ -82,6 +83,9 @@ class Controller(Singleton): back_stack: BackStack = None screensaver: ScreensaverScreen = None + # Babel initialization + gettext.install('messages', localedir='seedsigner/resources/babel') + @classmethod def get_instance(cls): diff --git a/src/seedsigner/models/settings.py b/src/seedsigner/models/settings.py index 2b57e45e..b64afa8b 100644 --- a/src/seedsigner/models/settings.py +++ b/src/seedsigner/models/settings.py @@ -1,3 +1,4 @@ +import gettext import json import os @@ -103,6 +104,16 @@ class Settings(Singleton): os.remove(self.SETTINGS_FILENAME) print(f"Removed {self.SETTINGS_FILENAME}") + # Special handling for babel + if attr_name == SettingsConstants.SETTING__LOCALE: + # `value` will be the language code (e.g. "en", "es", "de", etc) + os.environ['LANGUAGE'] = value + + # Re-initialize babel with the new locale + gettext.install('messages', localedir='seedsigner/resources/babel') + + print(f"Set LANGUAGE locale to {os.environ['LANGUAGE']}") + self._data[attr_name] = value self.save() diff --git a/src/seedsigner/models/settings_definition.py b/src/seedsigner/models/settings_definition.py index f1f12af6..aa085e15 100644 --- a/src/seedsigner/models/settings_definition.py +++ b/src/seedsigner/models/settings_definition.py @@ -40,9 +40,17 @@ class SettingsConstants: (COORDINATOR__SPECTER_DESKTOP, "Specter Desktop"), ] - LANGUAGE__ENGLISH = "en" - ALL_LANGUAGES = [ - (LANGUAGE__ENGLISH, "English"), + LOCALE__CZECH = "cs" + LOCALE__ENGLISH = "en" + LOCALE__FRENCH = "fr" + LOCALE__GERMAN = "de" + LOCALE__SPANISH = "es" + ALL_LOCALES = [ + (LOCALE__CZECH, "Czech"), + (LOCALE__ENGLISH, "English"), + (LOCALE__FRENCH, "French"), + (LOCALE__GERMAN, "German"), + (LOCALE__SPANISH, "Spanish"), ] BTC_DENOMINATION__BTC = "btc" @@ -137,7 +145,7 @@ class SettingsConstants: # Individual SettingsEntry attr_names - SETTING__LANGUAGE = "language" + SETTING__LOCALE = "locale" SETTING__WORDLIST_LANGUAGE = "wordlist_language" SETTING__PERSISTENT_SETTINGS = "persistent_settings" SETTING__COORDINATORS = "coordinators" @@ -330,14 +338,12 @@ class SettingsDefinition: settings_entries: List[SettingsEntry] = [ # General options - # TODO: Full babel multilanguage support! Until then, type == HIDDEN SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM, - attr_name=SettingsConstants.SETTING__LANGUAGE, + attr_name=SettingsConstants.SETTING__LOCALE, display_name="Language", type=SettingsConstants.TYPE__SELECT_1, - visibility=SettingsConstants.VISIBILITY__HIDDEN, - selection_options=SettingsConstants.ALL_LANGUAGES, - default_value=SettingsConstants.LANGUAGE__ENGLISH), + selection_options=SettingsConstants.ALL_LOCALES, + default_value=SettingsConstants.LOCALE__ENGLISH), # TODO: Support other bip-39 wordlist languages! Until then, type == HIDDEN SettingsEntry(category=SettingsConstants.CATEGORY__SYSTEM, diff --git a/src/seedsigner/resources/babel/cs/LC_MESSAGES/messages.po b/src/seedsigner/resources/babel/cs/LC_MESSAGES/messages.po new file mode 100644 index 00000000..9e80130f --- /dev/null +++ b/src/seedsigner/resources/babel/cs/LC_MESSAGES/messages.po @@ -0,0 +1,39 @@ +# Czech translations for PROJECT. +# Copyright (C) 2022 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2022-04-29 08:48-0500\n" +"PO-Revision-Date: 2022-04-29 13:53-0500\n" +"Last-Translator: FULL NAME \n" +"Language: cs\n" +"Language-Team: cs \n" +"Plural-Forms: nplurals=3; plural=((n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.10.1\n" + +#: src/seedsigner/views/view.py:132 +msgid "Scan" +msgstr "Skenuj" + +#: src/seedsigner/views/view.py:133 +msgid "Seeds" +msgstr "Seedy" + +#: src/seedsigner/views/view.py:134 +msgid "Tools" +msgstr "Nástroje" + +#: src/seedsigner/views/view.py:135 +msgid "Settings" +msgstr "Nastavení" + +#: src/seedsigner/views/view.py:139 +msgid "Home" +msgstr "Nabídka" \ No newline at end of file diff --git a/src/seedsigner/resources/babel/de/LC_MESSAGES/messages.po b/src/seedsigner/resources/babel/de/LC_MESSAGES/messages.po new file mode 100644 index 00000000..8b1507f6 --- /dev/null +++ b/src/seedsigner/resources/babel/de/LC_MESSAGES/messages.po @@ -0,0 +1,39 @@ +# German translations for PROJECT. +# Copyright (C) 2022 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2022-04-29 08:48-0500\n" +"PO-Revision-Date: 2022-04-29 13:49-0500\n" +"Last-Translator: FULL NAME \n" +"Language: de\n" +"Language-Team: de \n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.10.1\n" + +#: src/seedsigner/views/view.py:132 +msgid "Scan" +msgstr "Scan" + +#: src/seedsigner/views/view.py:133 +msgid "Seeds" +msgstr "Seeds" + +#: src/seedsigner/views/view.py:134 +msgid "Tools" +msgstr "Tools" + +#: src/seedsigner/views/view.py:135 +msgid "Settings" +msgstr "Einst." + +#: src/seedsigner/views/view.py:139 +msgid "Home" +msgstr "Start" \ No newline at end of file diff --git a/src/seedsigner/resources/babel/es/LC_MESSAGES/messages.po b/src/seedsigner/resources/babel/es/LC_MESSAGES/messages.po new file mode 100644 index 00000000..367153de --- /dev/null +++ b/src/seedsigner/resources/babel/es/LC_MESSAGES/messages.po @@ -0,0 +1,42 @@ +# Translations template for PROJECT. +# Copyright (C) 2022 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2022. +# +# Translators: +# Keith Mukai, 2022 +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2022-04-29 08:48-0500\n" +"PO-Revision-Date: 2022-04-29 13:50+0000\n" +"Last-Translator: Keith Mukai, 2022\n" +"Language-Team: Spanish (https://www.transifex.com/the-seedsigner-project/teams/135832/es/)\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.10.1\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: src/seedsigner/views/view.py:132 +msgid "Scan" +msgstr "Escanear" + +#: src/seedsigner/views/view.py:133 +msgid "Seeds" +msgstr "Llaves" + +#: src/seedsigner/views/view.py:134 +msgid "Tools" +msgstr "Útiles" + +#: src/seedsigner/views/view.py:135 +msgid "Settings" +msgstr "Ajustes" + +#: src/seedsigner/views/view.py:139 +msgid "Home" +msgstr "Principal" diff --git a/src/seedsigner/resources/babel/fr/LC_MESSAGES/messages.po b/src/seedsigner/resources/babel/fr/LC_MESSAGES/messages.po new file mode 100644 index 00000000..31556fee --- /dev/null +++ b/src/seedsigner/resources/babel/fr/LC_MESSAGES/messages.po @@ -0,0 +1,39 @@ +# French translations for PROJECT. +# Copyright (C) 2022 ORGANIZATION +# This file is distributed under the same license as the PROJECT project. +# FIRST AUTHOR , 2022. +# +msgid "" +msgstr "" +"Project-Id-Version: PROJECT VERSION\n" +"Report-Msgid-Bugs-To: EMAIL@ADDRESS\n" +"POT-Creation-Date: 2022-04-29 08:48-0500\n" +"PO-Revision-Date: 2022-04-29 13:49-0500\n" +"Last-Translator: FULL NAME \n" +"Language: fr\n" +"Language-Team: fr \n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=utf-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Generated-By: Babel 2.10.1\n" + +#: src/seedsigner/views/view.py:132 +msgid "Scan" +msgstr "Scan" + +#: src/seedsigner/views/view.py:133 +msgid "Seeds" +msgstr "Graines" + +#: src/seedsigner/views/view.py:134 +msgid "Tools" +msgstr "Outils" + +#: src/seedsigner/views/view.py:135 +msgid "Settings" +msgstr "Paramètres" + +#: src/seedsigner/views/view.py:139 +msgid "Home" +msgstr "Accueil" \ No newline at end of file diff --git a/src/seedsigner/views/view.py b/src/seedsigner/views/view.py index 815e5932..da47f330 100644 --- a/src/seedsigner/views/view.py +++ b/src/seedsigner/views/view.py @@ -128,14 +128,14 @@ class MainMenuView(View): from .tools_views import ToolsMenuView from seedsigner.gui.screens import LargeButtonScreen menu_items = [ - (("Scan", FontAwesomeIconConstants.QRCODE), ScanView), - (("Seeds", FontAwesomeIconConstants.KEY), SeedsMenuView), - (("Tools", FontAwesomeIconConstants.SCREWDRIVER_WRENCH), ToolsMenuView), - (("Settings", FontAwesomeIconConstants.GEAR), SettingsMenuView), + ((_("Scan"), FontAwesomeIconConstants.QRCODE), ScanView), + ((_("Seeds"), FontAwesomeIconConstants.KEY), SeedsMenuView), + ((_("Tools"), FontAwesomeIconConstants.SCREWDRIVER_WRENCH), ToolsMenuView), + ((_("Settings"), FontAwesomeIconConstants.GEAR), SettingsMenuView), ] screen = LargeButtonScreen( - title="Home", + title=_("Home"), title_font_size=26, button_data=[entry[0] for entry in menu_items], show_back_button=False,