diff --git a/README.md b/README.md
index 480a62c6..b66fd36d 100644
--- a/README.md
+++ b/README.md
@@ -118,16 +118,19 @@ flatpak install flathub io.github.slgobinath.SafeEyes
Ensure to meet the following dependencies:
- gir1.2-notify-0.7
+- gir1.2-gtk-4.0
- python3-babel
- python3-croniter
+- python3-gi
- python3-psutil
- python3-packaging
- python3-xlib
-- xprintidle (optional)
-- wlrctl (wayland optional)
+- python3-pywayland (optional for KDE/other wayland)
+- xprintidle (optional for X11)
+- wlrctl (optional for wayland/wlroots)
- Python 3.10+
-**To install Safe Eyes:**
+**To install Safe Eyes from PyPI:**
```bash
sudo pip3 install safeeyes
diff --git a/ruff.toml b/ruff.toml
index 4724771d..3a8fcc03 100644
--- a/ruff.toml
+++ b/ruff.toml
@@ -1,8 +1,5 @@
target-version = "py310"
-# gettext
-builtins = ["_"]
-
[lint]
select = ["E", "W", "F", "D2", "D3", "D4"]
ignore = [
diff --git a/safeeyes/__main__.py b/safeeyes/__main__.py
index e4e0d9f1..e8de3724 100755
--- a/safeeyes/__main__.py
+++ b/safeeyes/__main__.py
@@ -21,21 +21,18 @@
"""
import argparse
-import gettext
-import locale
import logging
import signal
import sys
import psutil
-from safeeyes import utility
+from safeeyes import utility, translations
+from safeeyes.translations import translate as _
from safeeyes.model import Config
from safeeyes.safeeyes import SafeEyes
from safeeyes.safeeyes import SAFE_EYES_VERSION
from safeeyes.rpc import RPCClient
-gettext.install("safeeyes", utility.LOCALE_PATH)
-
def __running():
"""Check if SafeEyes is already running."""
@@ -68,21 +65,7 @@ def __running():
def main():
"""Start the Safe Eyes."""
- system_locale = gettext.translation(
- "safeeyes",
- localedir=utility.LOCALE_PATH,
- languages=[utility.system_locale(), "en_US"],
- fallback=True,
- )
- system_locale.install()
- try:
- # locale.bindtextdomain is required for Glade files
- locale.bindtextdomain("safeeyes", utility.LOCALE_PATH)
- except AttributeError:
- logging.warning(
- "installed python's gettext module does not support locale.bindtextdomain."
- " locale.bindtextdomain is required for Glade files"
- )
+ system_locale = translations.setup()
parser = argparse.ArgumentParser(prog="safeeyes")
group = parser.add_mutually_exclusive_group()
diff --git a/safeeyes/model.py b/safeeyes/model.py
index d397ffb1..b19080f1 100644
--- a/safeeyes/model.py
+++ b/safeeyes/model.py
@@ -34,6 +34,7 @@
from gi.repository import Gtk
from safeeyes import utility
+from safeeyes.translations import translate as _
class Break:
diff --git a/safeeyes/plugins/donotdisturb/dependency_checker.py b/safeeyes/plugins/donotdisturb/dependency_checker.py
index 600cb2bf..96a00de4 100644
--- a/safeeyes/plugins/donotdisturb/dependency_checker.py
+++ b/safeeyes/plugins/donotdisturb/dependency_checker.py
@@ -17,6 +17,7 @@
# along with this program. If not, see .
from safeeyes import utility
+from safeeyes.translations import translate as _
def validate(plugin_config, plugin_settings):
diff --git a/safeeyes/plugins/healthstats/dependency_checker.py b/safeeyes/plugins/healthstats/dependency_checker.py
index daa3c942..475b535a 100644
--- a/safeeyes/plugins/healthstats/dependency_checker.py
+++ b/safeeyes/plugins/healthstats/dependency_checker.py
@@ -17,6 +17,7 @@
# along with this program. If not, see .
from safeeyes import utility
+from safeeyes.translations import translate as _
def validate(plugin_config, plugin_settings):
diff --git a/safeeyes/plugins/healthstats/plugin.py b/safeeyes/plugins/healthstats/plugin.py
index a1dc1f74..d5c0251c 100644
--- a/safeeyes/plugins/healthstats/plugin.py
+++ b/safeeyes/plugins/healthstats/plugin.py
@@ -21,6 +21,7 @@
import croniter
import datetime
import logging
+from safeeyes.translations import translate as _
context = None
session = None
diff --git a/safeeyes/plugins/limitconsecutiveskipping/plugin.py b/safeeyes/plugins/limitconsecutiveskipping/plugin.py
index 864b609e..16101bf9 100644
--- a/safeeyes/plugins/limitconsecutiveskipping/plugin.py
+++ b/safeeyes/plugins/limitconsecutiveskipping/plugin.py
@@ -19,6 +19,7 @@
"""Limit how many breaks can be skipped or postponed in a row."""
import logging
+from safeeyes.translations import translate as _
context = None
no_of_skipped_breaks = 0
diff --git a/safeeyes/plugins/notification/plugin.py b/safeeyes/plugins/notification/plugin.py
index 5dd4d51d..5c3a3d0b 100644
--- a/safeeyes/plugins/notification/plugin.py
+++ b/safeeyes/plugins/notification/plugin.py
@@ -20,6 +20,7 @@
import gi
from safeeyes.model import BreakType
+from safeeyes.translations import translate as _
gi.require_version("Notify", "0.7")
from gi.repository import Notify
diff --git a/safeeyes/plugins/smartpause/dependency_checker.py b/safeeyes/plugins/smartpause/dependency_checker.py
index 6d9b7b29..c3df55d4 100644
--- a/safeeyes/plugins/smartpause/dependency_checker.py
+++ b/safeeyes/plugins/smartpause/dependency_checker.py
@@ -17,6 +17,7 @@
# along with this program. If not, see .
from safeeyes import utility
+from safeeyes.translations import translate as _
def validate(plugin_config, plugin_settings):
diff --git a/safeeyes/plugins/trayicon/dependency_checker.py b/safeeyes/plugins/trayicon/dependency_checker.py
index 2ea5d217..5febe594 100644
--- a/safeeyes/plugins/trayicon/dependency_checker.py
+++ b/safeeyes/plugins/trayicon/dependency_checker.py
@@ -18,6 +18,7 @@
from safeeyes import utility
from safeeyes.model import PluginDependency
+from safeeyes.translations import translate as _
import gi
diff --git a/safeeyes/plugins/trayicon/plugin.py b/safeeyes/plugins/trayicon/plugin.py
index 21ced707..fbbeb1bc 100644
--- a/safeeyes/plugins/trayicon/plugin.py
+++ b/safeeyes/plugins/trayicon/plugin.py
@@ -24,6 +24,7 @@
from gi.repository import Gio, GLib
import logging
from safeeyes import utility
+from safeeyes.translations import translate as _
import threading
import time
import typing
diff --git a/safeeyes/safeeyes.py b/safeeyes/safeeyes.py
index d7f743ef..4ea5a93e 100644
--- a/safeeyes/safeeyes.py
+++ b/safeeyes/safeeyes.py
@@ -32,6 +32,7 @@
from safeeyes.ui.required_plugin_dialog import RequiredPluginDialog
from safeeyes.model import State, RequiredPluginException
from safeeyes.rpc import RPCServer
+from safeeyes.translations import translate as _
from safeeyes.plugin_manager import PluginManager
from safeeyes.core import SafeEyesCore
from safeeyes.ui.settings_dialog import SettingsDialog
diff --git a/safeeyes/translations.py b/safeeyes/translations.py
new file mode 100644
index 00000000..f9ee344d
--- /dev/null
+++ b/safeeyes/translations.py
@@ -0,0 +1,51 @@
+#!/usr/bin/env python
+# Safe Eyes is a utility to remind you to take break frequently
+# to protect your eyes from eye strain.
+
+# Copyright (C) 2024 Mel Dafert
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+"""Translation setup and helpers."""
+
+import locale
+import logging
+import gettext
+from safeeyes import utility
+
+_translations = gettext.NullTranslations()
+
+
+def setup():
+ global _translations
+ _translations = gettext.translation(
+ "safeeyes",
+ localedir=utility.LOCALE_PATH,
+ languages=[utility.system_locale(), "en_US"],
+ fallback=True,
+ )
+ try:
+ # locale.bindtextdomain is required for Glade files
+ locale.bindtextdomain("safeeyes", utility.LOCALE_PATH)
+ except AttributeError:
+ logging.warning(
+ "installed python's gettext module does not support locale.bindtextdomain."
+ " locale.bindtextdomain is required for Glade files"
+ )
+
+ return _translations
+
+
+def translate(message: str) -> str:
+ """Translate the message using the current translator."""
+ return _translations.gettext(message)
diff --git a/safeeyes/ui/about_dialog.py b/safeeyes/ui/about_dialog.py
index 62f3ed07..5914d44a 100644
--- a/safeeyes/ui/about_dialog.py
+++ b/safeeyes/ui/about_dialog.py
@@ -21,6 +21,7 @@
import os
from safeeyes import utility
+from safeeyes.translations import translate as _
ABOUT_DIALOG_GLADE = os.path.join(utility.BIN_DIRECTORY, "glade/about_dialog.glade")
diff --git a/safeeyes/ui/break_screen.py b/safeeyes/ui/break_screen.py
index 3ff43469..e74594a7 100644
--- a/safeeyes/ui/break_screen.py
+++ b/safeeyes/ui/break_screen.py
@@ -23,6 +23,7 @@
import gi
from safeeyes import utility
+from safeeyes.translations import translate as _
import Xlib
from Xlib.display import Display
from Xlib import X
diff --git a/safeeyes/ui/required_plugin_dialog.py b/safeeyes/ui/required_plugin_dialog.py
index 0c29decd..ebfbd5d7 100644
--- a/safeeyes/ui/required_plugin_dialog.py
+++ b/safeeyes/ui/required_plugin_dialog.py
@@ -24,6 +24,7 @@
from safeeyes import utility
from safeeyes.model import PluginDependency
+from safeeyes.translations import translate as _
REQUIRED_PLUGIN_DIALOG_GLADE = os.path.join(
utility.BIN_DIRECTORY, "glade/required_plugin_dialog.glade"
diff --git a/safeeyes/ui/settings_dialog.py b/safeeyes/ui/settings_dialog.py
index 03adf447..1189d109 100644
--- a/safeeyes/ui/settings_dialog.py
+++ b/safeeyes/ui/settings_dialog.py
@@ -22,6 +22,7 @@
import gi
from safeeyes import utility
from safeeyes.model import Config, PluginDependency
+from safeeyes.translations import translate as _
gi.require_version("Gtk", "4.0")
from gi.repository import Gtk, Gio
diff --git a/safeeyes/utility.py b/safeeyes/utility.py
index 7ff5ec93..1ec4b601 100644
--- a/safeeyes/utility.py
+++ b/safeeyes/utility.py
@@ -46,6 +46,7 @@
from gi.repository import GLib
from gi.repository import GdkPixbuf
from packaging.version import parse
+from safeeyes.translations import translate as _
BIN_DIRECTORY = os.path.dirname(os.path.realpath(__file__))
HOME_DIRECTORY = os.environ.get("HOME") or os.path.expanduser("~")
@@ -533,7 +534,7 @@ def initialize_platform():
logging.error("Failed to create desktop entry at %s" % desktop_entry)
# Add links for all icons
- for path, _, filenames in os.walk(SYSTEM_ICONS):
+ for path, _dirnames, filenames in os.walk(SYSTEM_ICONS):
for filename in filenames:
system_icon = os.path.join(path, filename)
local_icon = os.path.join(