Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Internationalization - Proof of Concept #264

Open
wants to merge 42 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
62557b1
Prototype go at extracted string for i18n
jscheidtmann Jan 12, 2025
b146085
First i18n prototype that works
jscheidtmann Jan 12, 2025
a10cb48
Translate when displaying the menu
jscheidtmann Jan 14, 2025
3f3c90d
Checkin latest po.
jscheidtmann Jan 22, 2025
8d347f6
Update messages.po
laurentbourasseau Jan 22, 2025
ef1f378
Update messages.po
laurentbourasseau Jan 22, 2025
169d871
Update messages.po
laurentbourasseau Jan 22, 2025
8d9881a
Update messages.po
laurentbourasseau Jan 22, 2025
43eaac4
Update messages.po
laurentbourasseau Jan 22, 2025
8d63ab2
Update messages.po
laurentbourasseau Jan 22, 2025
09aedba
Update messages.po
laurentbourasseau Jan 22, 2025
5a3ae59
Menu entry "Language" added.
jscheidtmann Jan 22, 2025
04c2343
Update messages.po
laurentbourasseau Jan 22, 2025
903051b
Switch langauge on the fly.
jscheidtmann Jan 22, 2025
bba95da
Merge branch 'i18n' of github.com:jscheidtmann/PiFinder into i18n
jscheidtmann Jan 22, 2025
3047161
Update messages.po
laurentbourasseau Jan 22, 2025
37c2824
Merge branch 'i18n' of github.com:jscheidtmann/PiFinder into i18n
jscheidtmann Jan 22, 2025
2491d2c
Updated messages and cleaned up import in sys_utils
jscheidtmann Jan 22, 2025
87e2cb1
nox runs through with-out errors.
jscheidtmann Jan 23, 2025
596ad2a
Merge branch 'main' of github.com:brickbots/PiFinder into i18n
jscheidtmann Jan 23, 2025
514fc63
Update po and mo after merging.
jscheidtmann Jan 23, 2025
465c98c
Remove print from switch_language
jscheidtmann Jan 23, 2025
7af0b30
Update messages.po
laurentbourasseau Feb 1, 2025
5c16aca
Updated .mo-files
jscheidtmann Feb 7, 2025
ecafe45
Merge remote-tracking branch 'origin/main' into i18n
jscheidtmann Feb 7, 2025
942e5f4
make nox babel optional. Include info on I18N in developer docs.
jscheidtmann Feb 7, 2025
78edf9e
Fix formatting and typos.
jscheidtmann Feb 7, 2025
a2c8209
Catch ValueError, too, if starting cedar_detect fails
jscheidtmann Feb 8, 2025
fc4ca79
First pass over ui classes for i18n
jscheidtmann Feb 8, 2025
565cf88
Add TRANSLATORS comments to .po file.
jscheidtmann Feb 8, 2025
79cf6c1
Make mypy ignore _()
jscheidtmann Feb 8, 2025
b585b99
Remove wrong placed "nox"
jscheidtmann Feb 8, 2025
f7affcb
When logging observation, do not use empty set for German translation
jscheidtmann Feb 8, 2025
92a9ed3
Update messages.po
laurentbourasseau Feb 8, 2025
17feba6
Update messages.po
laurentbourasseau Feb 8, 2025
60e653c
Update messages.po
laurentbourasseau Feb 9, 2025
dcb8972
Update messages.po
laurentbourasseau Feb 9, 2025
7072def
Update french translation
jscheidtmann Feb 9, 2025
95bf203
Merge remote-tracking branch 'origin/main' into i18n
jscheidtmann Feb 9, 2025
c9c321a
Update messages.po
laurentbourasseau Feb 9, 2025
f8119bb
Update messages.po
laurentbourasseau Feb 9, 2025
e3096e0
Updates to french translation
jscheidtmann Feb 9, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions default_config.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
"gps_lock": false,
"timezone": "America/Los_Angeles"
},
"language": "en",
"camera_exp": 400000,
"camera_gain": 20,
"menu_anim_speed": 0.1,
Expand Down
23 changes: 23 additions & 0 deletions docs/source/dev_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,22 @@ You can then use the supplied ``Makefile`` to build a html tree using ``make htm
cd build/html; python -m http.server


Internationalization
-----------------------

PiFinder uses ``gettext`` and ``pybabel`` for internationalization.
You can find the information in folder ``python/locale`` in the repository.
This means that strings that need translation must be
enclosed in a call to ``_()`` such als ``_("string that needs translation")``.

As we would like to allow users to switch the language of the user interface from the menu, and with-out restarting PiFinder,
care must be taken, that translations are performed dynamically, i.e. not at load time of python files.
If you have a variable at package level that needs to be translated, you still need to mark the strings with ``_()``, but make sure
it is not translated by overriding the ``_()``-function with a local one, that returns the string and then ``del`` that from the context, when you're done.
You can find an example of this in ``menu_structure.py`` at the top and bottom of the file.

Please also check your unit tests, that these take care of installing ``_()`` into the local context. (We have not had that case yet.)

Setup the development environment
---------------------------------

Expand Down Expand Up @@ -190,6 +206,13 @@ The defined sessions are:
session is not run by default, but is executed on code check in to the PiFinder
repository.

- babel -> Runs the complete toolchain for internationalization (based on `pybabel`).
That means extracts strings to translate and updates the `.po`-files in `python/locale/**`
Then these are compiled into `.mo`-files. Unfortuntely, this changes the `.mo`-files in any case,
even if the there have been no changes to strings or their translation. As this will show up
as changes to checked-in, this is not run by default.


CI/CD
.......

Expand Down
32 changes: 31 additions & 1 deletion python/PiFinder/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

"""

import gettext

import os

# skyfield performance fix, see: https://rhodesmill.org/skyfield/accuracy-efficiency.html
Expand Down Expand Up @@ -51,6 +53,12 @@
from PiFinder.calc_utils import sf_utils
from PiFinder.displays import DisplayBase, get_display

# Install the _("text") into global context for Internationalization
# On RasPi/Ubuntu the default locale is C.utf8, see `locale -a`, which locales are available
# You need to install `apt install language-pack_xx`, where xx is the ISO country code.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this implies changes to pifinder_setup and pifinder_update so everyone gets the languages. Also, each time a new language is added, these scripts need to be updated - unless we have some queryable single source of languages like a file used by the setup scripts and the app

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When updating, we can check the python/locale folder for new subdirs and then install the respective packages/languages. Alternative could be to install „all“ languages once. I have to check which of these options is preferable.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #264 (comment) : At least on bullyeye, nothing like this is necessary.

# Passing nothing as third parameter means the language is determined from environment variables (e.g. LANG)
gettext.install("PiFinder", "locale")

logger = logging.getLogger("main")

hardware_platform = "Pi"
Expand Down Expand Up @@ -287,6 +295,13 @@ def main(
screen_brightness = cfg.get_option("display_brightness")
set_brightness(screen_brightness, cfg)

# Set user interface language
lang = cfg.get_option("language")
langXX = gettext.translation(
"messages", "locale", languages=[lang], fallback=(lang == "en")
)
langXX.install()

import PiFinder.manager_patch as patch

patch.apply()
Expand Down Expand Up @@ -735,6 +750,10 @@ def rotate_logs() -> Path:
return utils.data_dir / "pifinder.log"


#############################################################################################
#############################################################################################
#############################################################################################

if __name__ == "__main__":
print("Bootstrap logging configuration ...")
logging.basicConfig(format="%(asctime)s BASIC %(name)s: %(levelname)s %(message)s")
Expand Down Expand Up @@ -816,6 +835,11 @@ def rotate_logs() -> Path:
"-x", "--verbose", help="Set logging to debug mode", action="store_true"
)
parser.add_argument("-l", "--log", help="Log to file", action="store_true")
parser.add_argument(
"--lang",
help="Force user interface language (iso2 code). Changes configuration",
type=str,
)
args = parser.parse_args()
# add the handlers to the logger
if args.verbose:
Expand Down Expand Up @@ -863,7 +887,13 @@ def rotate_logs() -> Path:
elif args.keyboard.lower() == "none":
from PiFinder import keyboard_none as keyboard # type: ignore[no-redef]

rlogger.warn("using no keyboard")
rlogger.warning("using no keyboard")

if args.lang:
if args.lang.lower() not in ["en", "de", "fr", "es"]:
raise Exception(f"Unknown language '{args.lang}' passed via command line.")
else:
config.Config().set_option("language", args.lang)

# if args.log:
# datenow = datetime.datetime.now()
Expand Down
8 changes: 7 additions & 1 deletion python/PiFinder/solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,17 @@ def solver(
+ shared_state.arch()
)
except FileNotFoundError as e:
logger.warn(
logger.warning(
"Not using cedar_detect, as corresponding file '%s' could not be found",
e.filename,
)
cedar_detect = None
except ValueError as e:
logger.warning(
"Not using cedar_detect, as the binary path could not be determined: %s",
e,
)
cedar_detect = None

try:
while True:
Expand Down
14 changes: 7 additions & 7 deletions python/PiFinder/ui/align.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def align_on_radec(ra, dec, command_queues, config_object, shared_state) -> bool
dec,
]
)
command_queues["console"].put("Align Timeout")
command_queues["console"].put(_("Align Timeout"))
return False

try:
Expand All @@ -71,7 +71,7 @@ def align_on_radec(ra, dec, command_queues, config_object, shared_state) -> bool
return False

# success, set all the things...
command_queues["console"].put("Alignment Set")
command_queues["console"].put(_("Alignment Set"))
shared_state.set_solve_pixel(target_pixel)
config_object.set_option("solve_pixel", target_pixel)
return True
Expand Down Expand Up @@ -211,13 +211,13 @@ def update(self, force=False):
)
self.draw.text(
(self.display_class.titlebar_height + 2, 20),
"Can't plot",
_("Can't plot"),
font=self.fonts.large.font,
fill=self.colors.get(255),
)
self.draw.text(
(self.display_class.titlebar_height + 2 + self.fonts.large.height, 50),
"No Solve Yet",
_("No Solve Yet"),
font=self.fonts.base.font,
fill=self.colors.get(255),
)
Expand Down Expand Up @@ -330,17 +330,17 @@ def key_square(self):
self.align_mode = False

if self.alignment_star is not None:
self.message("Aligning...", 0.1)
self.message(_("Aligning..."), 0.1)
if align_on_radec(
self.alignment_star["ra_degrees"],
self.alignment_star["dec_degrees"],
self.command_queues,
self.config_object,
self.shared_state,
):
self.message("Aligned!", 1)
self.message(_("Aligned!"), 1)
else:
self.message("Failed", 2)
self.message(_("Alignment failed"), 2)
else:
self.align_mode = True
self.update(force=True)
Expand Down
10 changes: 9 additions & 1 deletion python/PiFinder/ui/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@
from PiFinder.config import Config
from PiFinder.ui.marking_menus import MarkingMenu
from PiFinder.catalogs import Catalogs
from typing import Any, TYPE_CHECKING

if TYPE_CHECKING:

def _(a) -> Any:
return a


class UIModule:
Expand Down Expand Up @@ -191,7 +197,9 @@ def screen_update(self, title_bar=True, button_hints=True) -> None:
(6, 1), str(self.fps), font=self.fonts.bold.font, fill=fg
)
else:
self.draw.text((6, 1), self.title, font=self.fonts.bold.font, fill=fg)
self.draw.text(
(6, 1), _(self.title), font=self.fonts.bold.font, fill=fg
)
imu = self.shared_state.imu()
moving = True if imu and imu["pos"] and imu["moving"] else False

Expand Down
41 changes: 30 additions & 11 deletions python/PiFinder/ui/callbacks.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,19 @@

import datetime
import logging
import gettext

from typing import Any, TYPE_CHECKING
from PiFinder import utils
from PiFinder.ui.base import UIModule
from PiFinder.catalogs import CatalogFilter

if TYPE_CHECKING:

def _(a) -> Any:
return a


sys_utils = utils.get_sys_utils()


Expand All @@ -40,7 +48,7 @@ def reset_filters(ui_module: UIModule) -> None:

ui_module.catalogs.set_catalog_filter(new_filter)
ui_module.catalogs.filter_catalogs()
ui_module.message("Filters Reset")
ui_module.message(_("Filters Reset"))
ui_module.remove_from_stack()
return

Expand All @@ -51,10 +59,10 @@ def activate_debug(ui_module: UIModule) -> None:
add fake gps info
"""
ui_module.command_queues["camera"].put("debug")
ui_module.command_queues["console"].put("Debug: Activated")
ui_module.command_queues["console"].put(_("Debug: Activated"))
dt = datetime.datetime(2024, 6, 1, 2, 0, 0)
ui_module.shared_state.set_datetime(dt)
ui_module.message("Test Mode")
ui_module.message(_("Test Mode"))


def set_exposure(ui_module: UIModule) -> None:
Expand All @@ -70,7 +78,7 @@ def shutdown(ui_module: UIModule) -> None:
"""
shuts down the Pi
"""
ui_module.message("Shutting Down", 10)
ui_module.message(_("Shutting Down"), 10)
sys_utils.shutdown()


Expand All @@ -79,32 +87,32 @@ def restart_pifinder(ui_module: UIModule) -> None:
Uses systemctl to restart the PiFinder
service
"""
ui_module.message("Restarting...", 2)
ui_module.message(_("Restarting..."), 2)
sys_utils.restart_pifinder()


def restart_system(ui_module: UIModule) -> None:
"""
Restarts the system
"""
ui_module.message("Restarting...", 2)
ui_module.message(_("Restarting..."), 2)
sys_utils.restart_system()


def switch_cam_imx477(ui_module: UIModule) -> None:
ui_module.message("Switching cam", 2)
ui_module.message(_("Switching cam"), 2)
sys_utils.switch_cam_imx477()
restart_system(ui_module)


def switch_cam_imx296(ui_module: UIModule) -> None:
ui_module.message("Switching cam", 2)
ui_module.message(_("Switching cam"), 2)
sys_utils.switch_cam_imx296()
restart_system(ui_module)


def switch_cam_imx462(ui_module: UIModule) -> None:
ui_module.message("Switching cam", 2)
ui_module.message(_("Switching cam"), 2)
sys_utils.switch_cam_imx462()
restart_system(ui_module)

Expand All @@ -127,14 +135,25 @@ def get_camera_type(ui_module: UIModule) -> list[str]:
return [cam_id]


def switch_language(ui_module: UIModule) -> None:
iso2_code = ui_module.config_object.get_option("language")
msg = str(f"Language: {iso2_code}")
ui_module.message(_(msg))
lang = gettext.translation(
"messages", "locale", languages=[iso2_code], fallback=(iso2_code == "en")
)
lang.install()
logger.info("Switch Language: %s", iso2_code)


def go_wifi_ap(ui_module: UIModule) -> None:
ui_module.message("WiFi to AP", 2)
ui_module.message(_("WiFi to AP"), 2)
sys_utils.go_wifi_ap()
restart_system(ui_module)


def go_wifi_cli(ui_module: UIModule) -> None:
ui_module.message("WiFi to Client", 2)
ui_module.message(_("WiFi to Client"), 2)
sys_utils.go_wifi_cli()
restart_system(ui_module)

Expand Down
6 changes: 3 additions & 3 deletions python/PiFinder/ui/chart.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# -*- coding:utf-8 -*-
# mypy: ignore-errors
"""
This module contains all the UI Module classes
This module contains the chart (starfield + constellation lines) UI Module class

"""

Expand Down Expand Up @@ -202,13 +202,13 @@ def update(self, force=False):
)
self.draw.text(
(self.display_class.titlebar_height + 2, 20),
"Can't plot",
_("Can't plot"),
font=self.fonts.large.font,
fill=self.colors.get(255),
)
self.draw.text(
(self.display_class.titlebar_height + 2 + self.fonts.large.height, 50),
"No Solve Yet",
_("No Solve Yet"),
font=self.fonts.base.font,
fill=self.colors.get(255),
)
Expand Down
Loading