From b59cb347c4bbc1eaf8a0fe90142dcacc26d09f3b Mon Sep 17 00:00:00 2001 From: Vinicius Moreira Date: Mon, 25 Nov 2019 13:09:50 -0300 Subject: [PATCH] [improvement] Not breaking the application when a i18n (translation) key was not found --- CHANGELOG.md | 4 ++ CONTRIBUTING.md | 8 ++++ README.md | 10 +++-- bauh/__init__.py | 2 +- bauh/api/abstract/context.py | 6 +-- bauh/app.py | 11 ++++-- bauh/gems/arch/confirmation.py | 6 +-- bauh/gems/arch/message.py | 7 ++-- bauh/view/core/downloader.py | 3 +- bauh/view/core/gems.py | 9 +++-- bauh/view/qt/about.py | 17 +++++---- bauh/view/qt/apps_table.py | 11 +++--- bauh/view/qt/confirmation.py | 9 +++-- bauh/view/qt/dialog.py | 7 ++-- bauh/view/qt/gem_selector.py | 3 +- bauh/view/qt/history.py | 3 +- bauh/view/qt/info.py | 3 +- bauh/view/qt/root.py | 3 +- bauh/view/qt/screenshots.py | 3 +- bauh/view/qt/styles.py | 3 +- bauh/view/qt/systray.py | 3 +- bauh/view/qt/thread.py | 35 +++++++++--------- bauh/view/qt/window.py | 13 ++++--- bauh/view/util/translation.py | 67 ++++++++++++++++++++++++++++++++++ bauh/view/util/util.py | 37 ------------------- 25 files changed, 175 insertions(+), 108 deletions(-) create mode 100644 bauh/view/util/translation.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a5d77ab..a90a5c77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [0.7.3] +### Improvements +- Not breaking the application when a i18n (translation) key was not found + ## [0.7.2] 2019-11-01 ### Improvements - Snap diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 178c7035..83c87a6a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,3 +20,11 @@ The way to go here is to ask yourself if the improvement would be useful for mor * Explain why you think these changes could be useful * If it fixes a bug, be sure to link to the issue itself. * Follow the [PEP 8](https://www.python.org/dev/peps/pep-0008/) code style to keep the code consistent. + +# Adding a new translation +* To add a new translation, you will have to create a file in each directory listed below named as the first two letters in the ISO format (e.g: for 'english' would be 'en'): +- **bauh/view/resources/locale** +- **bauh/gems/appimage/resources/locale** +- **bauh/gems/arch/resources/locale** +- **bauh/gems/flatpak/resources/locale** +- **bauh/gems/snap/resources/locale** diff --git a/README.md b/README.md index b85589e5..84a97bd1 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ It has a **tray mode** (see **Settings** below) that attaches the application ic This project has an official Twitter account ( **@bauh4linux** ) so people can stay on top of its news. +To contribute with this project, have a look at [CONTRIBUTING.md](https://github.com/vinifmor/bauh/blob/master/CONTRIBUTING.md) + ![management panel](https://raw.githubusercontent.com/vinifmor/bauh/master/pictures/panel.png) @@ -65,13 +67,13 @@ If you do not want to clone / download this repository, go to your **Home** fold In order to autostart the application, use your Desktop Environment settings to register it as a startup application / script (**bauh --tray=1**). ### Gems ( package technology support ) -#### Flatpak ( flatpak gem ) +#### Flatpak ( flatpak ) - The user is able to search, install, uninstall, downgrade, launch and retrieve the applications history -#### Snap ( snap gem ) +#### Snap ( snap ) - The user is able to search, install, uninstall, refresh, launch and downgrade applications -#### AUR ( arch gem ) +#### AUR ( arch ) - It is **not enabled by default** - The user is able to search, install, uninstall, downgrade, launch and retrieve the packages history - It handles conflicts, and missing / optional packages installations ( including from your distro mirrors ) @@ -90,7 +92,7 @@ will be pre-downloaded faster ( it does **NOT** modify your **pacman** settings - Arch package memory-indexer running every 20 minutes. This memory index is used when AUR Api cannot handle the amount of results found for a given search. It can be disabled via the environment variable **BAUH_ARCH_AUR_INDEX_UPDATER=0**. - If some of your installed packages are not categorized, send an e-mail to **bauh4linux@gmail.com** informing their names and categories in the following format: ```name=category1[,category2,category3,...]``` -#### AppImage ( appimage gem ) +#### AppImage ( appimage ) - The user is able to search, install, uninstall, downgrade, launch and retrieve the applications history - Supported sources: [AppImageHub](https://appimage.github.io) ( **applications with no releases published to GitHub are currently not available** ) - Faster downloads if **aria2c** is installed. Same behavior described in the **AUR support** section. diff --git a/bauh/__init__.py b/bauh/__init__.py index 85695271..932ff985 100644 --- a/bauh/__init__.py +++ b/bauh/__init__.py @@ -1,4 +1,4 @@ -__version__ = '0.7.2' +__version__ = '0.7.3' __app_name__ = 'bauh' import os diff --git a/bauh/api/abstract/context.py b/bauh/api/abstract/context.py index 056998f5..0cd3d6dc 100644 --- a/bauh/api/abstract/context.py +++ b/bauh/api/abstract/context.py @@ -1,16 +1,16 @@ import logging -import platform import sys from bauh.api.abstract.cache import MemoryCacheFactory from bauh.api.abstract.disk import DiskCacheLoaderFactory from bauh.api.abstract.download import FileDownloader from bauh.api.http import HttpClient +from bauh.view.util.translation import I18n class ApplicationContext: - def __init__(self, disk_cache: bool, download_icons: bool, http_client: HttpClient, app_root_dir: str, i18n: dict, + def __init__(self, disk_cache: bool, download_icons: bool, http_client: HttpClient, app_root_dir: str, i18n: I18n, cache_factory: MemoryCacheFactory, disk_loader_factory: DiskCacheLoaderFactory, logger: logging.Logger, file_downloader: FileDownloader, distro: str): """ @@ -18,7 +18,7 @@ class ApplicationContext: :param download_icons: if packages icons should be downloaded :param http_client: a shared instance of http client :param app_root_dir: GUI root dir - :param i18n: the i18n dictionary keys + :param i18n: the translation keys :param cache_factory: :param disk_loader_factory: :param logger: a logger instance diff --git a/bauh/app.py b/bauh/app.py index 4a332791..89140feb 100755 --- a/bauh/app.py +++ b/bauh/app.py @@ -12,9 +12,12 @@ from bauh.view.core.controller import GenericSoftwareManager from bauh.view.core.downloader import AdaptableFileDownloader from bauh.view.qt.systray import TrayIcon from bauh.view.qt.window import ManageWindow -from bauh.view.util import util, logs, resource +from bauh.view.util import util, logs, resource, translation from bauh.view.util.cache import DefaultMemoryCacheFactory, CacheCleaner from bauh.view.util.disk import DefaultDiskCacheLoaderFactory +from bauh.view.util.translation import I18n + +DEFAULT_I18N_KEY = 'en' def main(): @@ -25,7 +28,9 @@ def main(): logger = logs.new_logger(__app_name__, bool(args.logs)) app_args.validate(args, logger) - i18n_key, i18n = util.get_locale_keys(args.locale) + i18n_key, current_i18n = translation.get_locale_keys(args.locale) + default_i18n = translation.get_locale_keys(DEFAULT_I18N_KEY)[1] if i18n_key != DEFAULT_I18N_KEY else {} + i18n = I18n(current_i18n, default_i18n) cache_cleaner = CacheCleaner() cache_factory = DefaultMemoryCacheFactory(expiration_time=args.cache_exp, cleaner=cache_cleaner) @@ -57,7 +62,7 @@ def main(): if app.style().objectName().lower() not in {'fusion', 'breeze'}: app.setStyle('Fusion') - managers = gems.load_managers(context=context, locale=i18n_key, config=user_config) + managers = gems.load_managers(context=context, locale=i18n_key, config=user_config, default_locale=DEFAULT_I18N_KEY) manager = GenericSoftwareManager(managers, context=context, app_args=args) manager.prepare() diff --git a/bauh/gems/arch/confirmation.py b/bauh/gems/arch/confirmation.py index d47765a3..dd114388 100644 --- a/bauh/gems/arch/confirmation.py +++ b/bauh/gems/arch/confirmation.py @@ -2,11 +2,11 @@ from typing import Set from bauh.api.abstract.handler import ProcessWatcher from bauh.api.abstract.view import MultipleSelectComponent, InputOption - from bauh.commons.html import bold +from bauh.view.util.translation import I18n -def request_optional_deps(pkgname: str, pkg_mirrors: dict, watcher: ProcessWatcher, i18n: dict) -> Set[str]: +def request_optional_deps(pkgname: str, pkg_mirrors: dict, watcher: ProcessWatcher, i18n: I18n) -> Set[str]: opts = [InputOption('{}{} ( {} )'.format(p, ': ' + d['desc'] if d['desc'] else '', d['mirror'].upper()), p) for p, d in pkg_mirrors.items()] view_opts = MultipleSelectComponent(label='', options=opts, @@ -21,7 +21,7 @@ def request_optional_deps(pkgname: str, pkg_mirrors: dict, watcher: ProcessWatch return {o.value for o in view_opts.values} -def request_install_missing_deps(pkgname: str, pkg_mirrors: dict, watcher: ProcessWatcher, i18n: dict) -> bool: +def request_install_missing_deps(pkgname: str, pkg_mirrors: dict, watcher: ProcessWatcher, i18n: I18n) -> bool: deps_str = ''.join(['
- {} ( {} )'.format(d, m.upper()) for d, m in pkg_mirrors.items()]) msg = '

{}

'.format(i18n['arch.missing_deps.body'].format(bold(pkgname)) + ':
' + deps_str) msg += i18n['ask.continue'] diff --git a/bauh/gems/arch/message.py b/bauh/gems/arch/message.py index df02103b..320cf23b 100644 --- a/bauh/gems/arch/message.py +++ b/bauh/gems/arch/message.py @@ -1,21 +1,22 @@ from bauh.api.abstract.handler import ProcessWatcher from bauh.api.abstract.view import MessageType from bauh.commons.html import bold +from bauh.view.util.translation import I18n -def show_dep_not_installed(watcher: ProcessWatcher, pkgname: str, depname: str, i18n: dict): +def show_dep_not_installed(watcher: ProcessWatcher, pkgname: str, depname: str, i18n: I18n): watcher.show_message(title=i18n['error'], body=i18n['arch.install.dependency.install.error'].format(bold(depname), bold(pkgname)), type_=MessageType.ERROR) -def show_dep_not_found(depname: str, i18n: dict, watcher: ProcessWatcher): +def show_dep_not_found(depname: str, i18n: I18n, watcher: ProcessWatcher): watcher.show_message(title=i18n['arch.install.dep_not_found.title'], body=i18n['arch.install.dep_not_found.body'].format(bold(depname)), type_=MessageType.ERROR) -def show_optdep_not_installed(depname: str, watcher: ProcessWatcher, i18n: dict): +def show_optdep_not_installed(depname: str, watcher: ProcessWatcher, i18n: I18n): watcher.show_message(title=i18n['error'], body=i18n['arch.install.optdep.error'].format(bold(depname)), type_=MessageType.ERROR) diff --git a/bauh/view/core/downloader.py b/bauh/view/core/downloader.py index 960fc3ab..d29231fb 100644 --- a/bauh/view/core/downloader.py +++ b/bauh/view/core/downloader.py @@ -8,11 +8,12 @@ from bauh.api.abstract.handler import ProcessWatcher from bauh.api.http import HttpClient from bauh.commons.html import bold from bauh.commons.system import run_cmd, new_subprocess, ProcessHandler, SystemProcess, SimpleProcess +from bauh.view.util.translation import I18n class AdaptableFileDownloader(FileDownloader): - def __init__(self, logger: logging.Logger, multithread_enabled: bool, i18n: dict, http_client: HttpClient): + def __init__(self, logger: logging.Logger, multithread_enabled: bool, i18n: I18n, http_client: HttpClient): self.logger = logger self.multithread_enabled = multithread_enabled self.i18n = i18n diff --git a/bauh/view/core/gems.py b/bauh/view/core/gems.py index bfd140d4..c9756759 100644 --- a/bauh/view/core/gems.py +++ b/bauh/view/core/gems.py @@ -6,7 +6,7 @@ from typing import List from bauh import ROOT_DIR from bauh.api.abstract.controller import SoftwareManager, ApplicationContext from bauh.view.core.config import Configuration -from bauh.view.util import util +from bauh.view.util import util, translation def find_manager(member): @@ -20,7 +20,7 @@ def find_manager(member): return manager_found -def load_managers(locale: str, context: ApplicationContext, config: Configuration) -> List[SoftwareManager]: +def load_managers(locale: str, context: ApplicationContext, config: Configuration, default_locale: str) -> List[SoftwareManager]: managers = [] for f in os.scandir(ROOT_DIR + '/gems'): @@ -37,7 +37,10 @@ def load_managers(locale: str, context: ApplicationContext, config: Configuratio locale_path = '{}/resources/locale'.format(f.path) if os.path.exists(locale_path): - context.i18n.update(util.get_locale_keys(locale, locale_path)[1]) + context.i18n.current.update(translation.get_locale_keys(locale, locale_path)[1]) + + if default_locale and context.i18n.default: + context.i18n.default.update(translation.get_locale_keys(default_locale, locale_path)[1]) man = manager_class(context=context) diff --git a/bauh/view/qt/about.py b/bauh/view/qt/about.py index 7f3971a3..39065f0f 100644 --- a/bauh/view/qt/about.py +++ b/bauh/view/qt/about.py @@ -1,11 +1,12 @@ from glob import glob from PyQt5.QtCore import Qt -from PyQt5.QtGui import QPixmap, QIcon +from PyQt5.QtGui import QPixmap from PyQt5.QtWidgets import QVBoxLayout, QDialog, QLabel, QWidget, QHBoxLayout from bauh import __version__, __app_name__, ROOT_DIR from bauh.view.util import resource +from bauh.view.util.translation import I18n PROJECT_URL = 'https://github.com/vinifmor/' + __app_name__ LICENSE_URL = 'https://raw.githubusercontent.com/vinifmor/{}/master/LICENSE'.format(__app_name__) @@ -13,9 +14,9 @@ LICENSE_URL = 'https://raw.githubusercontent.com/vinifmor/{}/master/LICENSE'.for class AboutDialog(QDialog): - def __init__(self, locale_keys: dict): + def __init__(self, i18n: I18n): super(AboutDialog, self).__init__() - self.setWindowTitle(locale_keys['tray.action.about']) + self.setWindowTitle(i18n['tray.action.about']) layout = QVBoxLayout() self.setLayout(layout) @@ -33,7 +34,7 @@ class AboutDialog(QDialog): line_desc = QLabel(self) line_desc.setStyleSheet('font-size: 12px; font-weight: bold;') - line_desc.setText(locale_keys['about.info.desc']) + line_desc.setText(i18n['about.info.desc']) line_desc.setAlignment(Qt.AlignCenter) line_desc.setMinimumWidth(400) layout.addWidget(line_desc) @@ -55,21 +56,21 @@ class AboutDialog(QDialog): layout.addWidget(gems_widget) layout.addWidget(QLabel('')) - label_version = QLabel(locale_keys['version'].lower() + ' ' + __version__) + label_version = QLabel(i18n['version'].lower() + ' ' + __version__) label_version.setStyleSheet('QLabel { font-size: 11px; font-weight: bold }') label_version.setAlignment(Qt.AlignCenter) layout.addWidget(label_version) label_more_info = QLabel() label_more_info.setStyleSheet('font-size: 11px;') - label_more_info.setText(locale_keys['about.info.link'] + ": {url}".format(url=PROJECT_URL)) + label_more_info.setText(i18n['about.info.link'] + ": {url}".format(url=PROJECT_URL)) label_more_info.setOpenExternalLinks(True) label_more_info.setAlignment(Qt.AlignCenter) layout.addWidget(label_more_info) label_license = QLabel() label_license.setStyleSheet('font-size: 11px;') - label_license.setText("{}".format(LICENSE_URL, locale_keys['about.info.license'])) + label_license.setText("{}".format(LICENSE_URL, i18n['about.info.license'])) label_license.setOpenExternalLinks(True) label_license.setAlignment(Qt.AlignCenter) layout.addWidget(label_license) @@ -78,7 +79,7 @@ class AboutDialog(QDialog): label_rate = QLabel() label_rate.setStyleSheet('font-size: 11px; font-weight: bold;') - label_rate.setText(locale_keys['about.info.rate'] + ' :)') + label_rate.setText(i18n['about.info.rate'] + ' :)') label_rate.setOpenExternalLinks(True) label_rate.setAlignment(Qt.AlignCenter) layout.addWidget(label_rate) diff --git a/bauh/view/qt/apps_table.py b/bauh/view/qt/apps_table.py index c9ea76c4..040fab21 100644 --- a/bauh/view/qt/apps_table.py +++ b/bauh/view/qt/apps_table.py @@ -15,6 +15,7 @@ from bauh.view.qt import dialog from bauh.view.qt.components import IconButton from bauh.view.qt.view_model import PackageView, PackageViewStatus from bauh.view.util import resource +from bauh.view.util.translation import I18n INSTALL_BT_STYLE = 'background: {back}; color: white; font-size: 10px; font-weight: bold' @@ -25,7 +26,7 @@ PUBLISHER_MAX_SIZE = 25 class UpdateToggleButton(QWidget): - def __init__(self, app_view: PackageView, root: QWidget, i18n: dict, checked: bool = True, clickable: bool = True): + def __init__(self, app_view: PackageView, root: QWidget, i18n: I18n, checked: bool = True, clickable: bool = True): super(UpdateToggleButton, self).__init__() self.app_view = app_view @@ -113,7 +114,7 @@ class AppsTable(QTableWidget): if dialog.ask_confirmation( title=self.i18n['manage_window.apps_table.row.actions.downgrade'], body=self._parag(self.i18n['manage_window.apps_table.row.actions.downgrade.popup.body'].format(self._bold(str(pkg)))), - locale_keys=self.i18n): + i18n=self.i18n): self.window.downgrade(pkg) action_downgrade.triggered.connect(downgrade) @@ -131,7 +132,7 @@ class AppsTable(QTableWidget): if dialog.ask_confirmation( title=self.i18n[action.i18_label_key], body=self._parag('{} {} ?'.format(self.i18n[action.i18_label_key], self._bold(str(pkg)))), - locale_keys=self.i18n): + i18n=self.i18n): self.window.execute_custom_action(pkg, action) item.triggered.connect(custom_action) @@ -162,7 +163,7 @@ class AppsTable(QTableWidget): def _uninstall_app(self, app_v: PackageView): if dialog.ask_confirmation(title=self.i18n['manage_window.apps_table.row.actions.uninstall.popup.title'], body=self._parag(self.i18n['manage_window.apps_table.row.actions.uninstall.popup.body'].format(self._bold(str(app_v)))), - locale_keys=self.i18n): + i18n=self.i18n): self.window.uninstall_app(app_v) def _bold(self, text: str) -> str: @@ -176,7 +177,7 @@ class AppsTable(QTableWidget): if dialog.ask_confirmation( title=self.i18n['manage_window.apps_table.row.actions.install.popup.title'], body=self._parag(self.i18n['manage_window.apps_table.row.actions.install.popup.body'].format(self._bold(str(pkgv)))), - locale_keys=self.i18n): + i18n=self.i18n): self.window.install(pkgv) diff --git a/bauh/view/qt/confirmation.py b/bauh/view/qt/confirmation.py index 53980ccb..ceeee6ab 100644 --- a/bauh/view/qt/confirmation.py +++ b/bauh/view/qt/confirmation.py @@ -1,22 +1,23 @@ from typing import List from PyQt5.QtWidgets import QMessageBox, QVBoxLayout, QLabel, QWidget + from bauh.api.abstract.view import ViewComponent, SingleSelectComponent, MultipleSelectComponent from bauh.view.qt import css - from bauh.view.qt.components import MultipleSelectQt, new_single_select +from bauh.view.util.translation import I18n class ConfirmationDialog(QMessageBox): - def __init__(self, title: str, body: str, locale_keys: dict, components: List[ViewComponent] = None, confirmation_label: str = None, deny_label: str = None): + def __init__(self, title: str, body: str, i18n: I18n, components: List[ViewComponent] = None, confirmation_label: str = None, deny_label: str = None): super(ConfirmationDialog, self).__init__() self.setWindowTitle(title) self.setStyleSheet('QLabel { margin-right: 25px; }') - self.bt_yes = self.addButton(locale_keys['popup.button.yes'] if not confirmation_label else confirmation_label.capitalize(), QMessageBox.YesRole) + self.bt_yes = self.addButton(i18n['popup.button.yes'] if not confirmation_label else confirmation_label.capitalize(), QMessageBox.YesRole) self.bt_yes.setStyleSheet(css.OK_BUTTON) - self.addButton(locale_keys['popup.button.no'] if not deny_label else deny_label.capitalize(), QMessageBox.NoRole) + self.addButton(i18n['popup.button.no'] if not deny_label else deny_label.capitalize(), QMessageBox.NoRole) if body: if not components: diff --git a/bauh/view/qt/dialog.py b/bauh/view/qt/dialog.py index ddf40304..2dbc1a08 100644 --- a/bauh/view/qt/dialog.py +++ b/bauh/view/qt/dialog.py @@ -6,6 +6,7 @@ from bauh.api.abstract.view import MessageType from bauh.view.util import resource from bauh.view.qt import css +from bauh.view.util.translation import I18n MSG_TYPE_MAP = { MessageType.ERROR: QMessageBox.Critical, @@ -26,7 +27,7 @@ def show_message(title: str, body: str, type_: MessageType, icon: QIcon = QIcon( popup.exec_() -def ask_confirmation(title: str, body: str, locale_keys: dict, icon: QIcon = QIcon(resource.get_path('img/logo.svg')), widgets: List[QWidget] = None): +def ask_confirmation(title: str, body: str, i18n: I18n, icon: QIcon = QIcon(resource.get_path('img/logo.svg')), widgets: List[QWidget] = None): diag = QMessageBox() diag.setIcon(QMessageBox.Question) diag.setWindowTitle(title) @@ -42,10 +43,10 @@ def ask_confirmation(title: str, body: str, locale_keys: dict, icon: QIcon = QIc diag.layout().addWidget(wbody, 0, 1) - bt_yes = diag.addButton(locale_keys['popup.button.yes'], QMessageBox.YesRole) + bt_yes = diag.addButton(i18n['popup.button.yes'], QMessageBox.YesRole) bt_yes.setStyleSheet(css.OK_BUTTON) - diag.addButton(locale_keys['popup.button.no'], QMessageBox.NoRole) + diag.addButton(i18n['popup.button.no'], QMessageBox.NoRole) if icon: diag.setWindowIcon(icon) diff --git a/bauh/view/qt/gem_selector.py b/bauh/view/qt/gem_selector.py index be810f7f..2a2a4644 100644 --- a/bauh/view/qt/gem_selector.py +++ b/bauh/view/qt/gem_selector.py @@ -9,11 +9,12 @@ from bauh.view.core.controller import GenericSoftwareManager from bauh.view.util import resource from bauh.view.qt import qt_utils, css from bauh.view.qt.components import MultipleSelectQt, CheckboxQt, new_spacer +from bauh.view.util.translation import I18n class GemSelectorPanel(QWidget): - def __init__(self, window: QWidget, manager: GenericSoftwareManager, i18n: dict, config: Configuration, show_panel_after_restart: bool = False): + def __init__(self, window: QWidget, manager: GenericSoftwareManager, i18n: I18n, config: Configuration, show_panel_after_restart: bool = False): super(GemSelectorPanel, self).__init__() self.window = window self.manager = manager diff --git a/bauh/view/qt/history.py b/bauh/view/qt/history.py index 78fe283c..2498f2cc 100644 --- a/bauh/view/qt/history.py +++ b/bauh/view/qt/history.py @@ -7,11 +7,12 @@ from PyQt5.QtWidgets import QDialog, QVBoxLayout, QTableWidget, QTableWidgetItem from bauh.api.abstract.cache import MemoryCache from bauh.api.abstract.model import PackageHistory +from bauh.view.util.translation import I18n class HistoryDialog(QDialog): - def __init__(self, history: PackageHistory, icon_cache: MemoryCache, i18n: dict): + def __init__(self, history: PackageHistory, icon_cache: MemoryCache, i18n: I18n): super(HistoryDialog, self).__init__() self.setWindowFlags(self.windowFlags() | Qt.WindowSystemMenuHint | Qt.WindowMinMaxButtonsHint) diff --git a/bauh/view/qt/info.py b/bauh/view/qt/info.py index b9bbb11c..1a19011a 100644 --- a/bauh/view/qt/info.py +++ b/bauh/view/qt/info.py @@ -4,13 +4,14 @@ from PyQt5.QtWidgets import QDialog, QVBoxLayout, QGroupBox, \ QLineEdit, QLabel, QGridLayout, QPushButton, QPlainTextEdit, QToolBar from bauh.api.abstract.cache import MemoryCache +from bauh.view.util.translation import I18n IGNORED_ATTRS = {'name', '__app__'} class InfoDialog(QDialog): - def __init__(self, app: dict, icon_cache: MemoryCache, i18n: dict, screen_size: QSize()): + def __init__(self, app: dict, icon_cache: MemoryCache, i18n: I18n, screen_size: QSize()): super(InfoDialog, self).__init__() self.setWindowTitle(str(app['__app__'])) self.screen_size = screen_size diff --git a/bauh/view/qt/root.py b/bauh/view/qt/root.py index d4f16cb0..3638c816 100644 --- a/bauh/view/qt/root.py +++ b/bauh/view/qt/root.py @@ -7,13 +7,14 @@ from bauh.api.abstract.view import MessageType from bauh.commons.system import new_subprocess from bauh.view.qt.dialog import show_message from bauh.view.util import resource +from bauh.view.util.translation import I18n def is_root(): return os.getuid() == 0 -def ask_root_password(i18n: dict): +def ask_root_password(i18n: I18n): diag = QInputDialog() diag.setStyleSheet("""QLineEdit { border-radius: 5px; font-size: 16px }""") diag.setInputMode(QInputDialog.TextInput) diff --git a/bauh/view/qt/screenshots.py b/bauh/view/qt/screenshots.py index 56bd13fc..f61712d3 100644 --- a/bauh/view/qt/screenshots.py +++ b/bauh/view/qt/screenshots.py @@ -12,6 +12,7 @@ from bauh.view.qt import qt_utils from bauh.view.qt.components import new_spacer from bauh.view.qt.thread import AnimateProgress from bauh.view.qt.view_model import PackageView +from bauh.view.util.translation import I18n class ScreenshotsDialog(QDialog): @@ -19,7 +20,7 @@ class ScreenshotsDialog(QDialog): MAX_HEIGHT = 600 MAX_WIDTH = 800 - def __init__(self, pkg: PackageView, http_client: HttpClient, icon_cache: MemoryCache, i18n: dict, screenshots: List[QPixmap], logger: logging.Logger): + def __init__(self, pkg: PackageView, http_client: HttpClient, icon_cache: MemoryCache, i18n: I18n, screenshots: List[QPixmap], logger: logging.Logger): super(ScreenshotsDialog, self).__init__() self.setWindowTitle(str(pkg)) self.screenshots = screenshots diff --git a/bauh/view/qt/styles.py b/bauh/view/qt/styles.py index 9636b163..20e00313 100644 --- a/bauh/view/qt/styles.py +++ b/bauh/view/qt/styles.py @@ -5,11 +5,12 @@ from bauh.commons.html import bold from bauh.view.core import config from bauh.view.util import util from bauh.view.qt import dialog +from bauh.view.util.translation import I18n class StylesComboBox(QComboBox): - def __init__(self, parent: QWidget, i18n: dict, show_panel_after_restart: bool): + def __init__(self, parent: QWidget, i18n: I18n, show_panel_after_restart: bool): super(StylesComboBox, self).__init__(parent=parent) self.app = QApplication.instance() self.styles = [] diff --git a/bauh/view/qt/systray.py b/bauh/view/qt/systray.py index cb168f8f..201936c8 100755 --- a/bauh/view/qt/systray.py +++ b/bauh/view/qt/systray.py @@ -14,6 +14,7 @@ from bauh.api.abstract.model import PackageUpdate from bauh.view.util import util, resource from bauh.view.qt.about import AboutDialog from bauh.view.qt.window import ManageWindow +from bauh.view.util.translation import I18n class UpdateCheck(QThread): @@ -35,7 +36,7 @@ class UpdateCheck(QThread): class TrayIcon(QSystemTrayIcon): - def __init__(self, i18n: dict, manager: SoftwareManager, manage_window: ManageWindow, check_interval: int = 60, update_notification: bool = True): + def __init__(self, i18n: I18n, manager: SoftwareManager, manage_window: ManageWindow, check_interval: int = 60, update_notification: bool = True): super(TrayIcon, self).__init__() self.i18n = i18n self.manager = manager diff --git a/bauh/view/qt/thread.py b/bauh/view/qt/thread.py index f3be47e0..eacc72dc 100644 --- a/bauh/view/qt/thread.py +++ b/bauh/view/qt/thread.py @@ -12,9 +12,9 @@ from bauh.api.abstract.handler import ProcessWatcher from bauh.api.abstract.model import PackageStatus, SoftwarePackage, PackageAction from bauh.api.abstract.view import InputViewComponent, MessageType from bauh.api.exception import NoInternetException -from bauh.api.http import HttpClient from bauh.view.qt import commons from bauh.view.qt.view_model import PackageView +from bauh.view.util.translation import I18n RE_VERSION_IN_NAME = re.compile(r'\s+version\s+[\w\.]+\s*$') @@ -76,12 +76,12 @@ class AsyncAction(QThread, ProcessWatcher): class UpdateSelectedApps(AsyncAction): - def __init__(self, manager: SoftwareManager, locale_keys: dict, apps_to_update: List[PackageView] = None): + def __init__(self, manager: SoftwareManager, i18n: I18n, apps_to_update: List[PackageView] = None): super(UpdateSelectedApps, self).__init__() self.apps_to_update = apps_to_update self.manager = manager self.root_password = None - self.locale_keys = locale_keys + self.i18n = i18n def run(self): @@ -93,7 +93,7 @@ class UpdateSelectedApps(AsyncAction): name = app.model.name if not RE_VERSION_IN_NAME.findall(app.model.name) else app.model.name.split('version')[0].strip() - self.change_status('{} {} {}...'.format(self.locale_keys['manage_window.status.upgrading'], name, app.model.version)) + self.change_status('{} {} {}...'.format(self.i18n['manage_window.status.upgrading'], name, app.model.version)) success = bool(self.manager.update(app.model, self.root_password, self)) self.change_substatus('') @@ -168,11 +168,11 @@ class UninstallApp(AsyncAction): class DowngradeApp(AsyncAction): - def __init__(self, manager: SoftwareManager, locale_keys: dict, app: PackageView = None): + def __init__(self, manager: SoftwareManager, i18n: I18n, app: PackageView = None): super(DowngradeApp, self).__init__() self.manager = manager self.app = app - self.locale_keys = locale_keys + self.i18n = i18n self.root_password = None def run(self): @@ -182,7 +182,7 @@ class DowngradeApp(AsyncAction): success = self.manager.downgrade(self.app.model, self.root_password, self) except (requests.exceptions.ConnectionError, NoInternetException) as e: success = False - self.print(self.locale_keys['internet.required']) + self.print(self.i18n['internet.required']) finally: self.notify_finished({'app': self.app, 'success': success}) self.app = None @@ -206,18 +206,18 @@ class GetAppInfo(AsyncAction): class GetAppHistory(AsyncAction): - def __init__(self, manager: SoftwareManager, locale_keys: dict, app: PackageView = None): + def __init__(self, manager: SoftwareManager, i18n: I18n, app: PackageView = None): super(GetAppHistory, self).__init__() self.app = app self.manager = manager - self.locale_keys = locale_keys + self.i18n = i18n def run(self): if self.app: try: self.notify_finished({'history': self.manager.get_history(self.app.model)}) except (requests.exceptions.ConnectionError, NoInternetException) as e: - self.notify_finished({'error': self.locale_keys['internet.required']}) + self.notify_finished({'error': self.i18n['internet.required']}) finally: self.app = None @@ -246,13 +246,13 @@ class SearchPackages(AsyncAction): class InstallPackage(AsyncAction): - def __init__(self, manager: SoftwareManager, disk_cache: bool, icon_cache: MemoryCache, locale_keys: dict, pkg: PackageView = None): + def __init__(self, manager: SoftwareManager, disk_cache: bool, icon_cache: MemoryCache, i18n: I18n, pkg: PackageView = None): super(InstallPackage, self).__init__() self.pkg = pkg self.manager = manager self.icon_cache = icon_cache self.disk_cache = disk_cache - self.locale_keys = locale_keys + self.i18n = i18n self.root_password = None def run(self): @@ -272,7 +272,7 @@ class InstallPackage(AsyncAction): only_icon=False) except (requests.exceptions.ConnectionError, NoInternetException): success = False - self.print(self.locale_keys['internet.required']) + self.print(self.i18n['internet.required']) finally: self.signal_finished.emit({'success': success, 'pkg': self.pkg}) self.pkg = None @@ -403,9 +403,9 @@ class ListWarnings(QThread): signal_warnings = pyqtSignal(list) - def __init__(self, man: SoftwareManager, locale_keys: dict): + def __init__(self, man: SoftwareManager, i18n: I18n): super(QThread, self).__init__() - self.locale_keys = locale_keys + self.i18n = i18n self.man = man def run(self): @@ -464,12 +464,13 @@ class ApplyFilters(AsyncAction): class CustomAction(AsyncAction): - def __init__(self, manager: SoftwareManager, custom_action: PackageAction = None, pkg: PackageView = None, root_password: str = None): + def __init__(self, manager: SoftwareManager, i18n: I18n, custom_action: PackageAction = None, pkg: PackageView = None, root_password: str = None): super(CustomAction, self).__init__() self.manager = manager self.pkg = pkg self.custom_action = custom_action self.root_password = root_password + self.i18n = i18n def run(self): success = True @@ -481,7 +482,7 @@ class CustomAction(AsyncAction): watcher=self) except (requests.exceptions.ConnectionError, NoInternetException): success = False - self.signal_output.emit(self.locale_keys['internet.required']) + self.signal_output.emit(self.i18n['internet.required']) self.notify_finished({'success': success, 'pkg': self.pkg}) self.pkg = None diff --git a/bauh/view/qt/window.py b/bauh/view/qt/window.py index 16da819d..334816a2 100755 --- a/bauh/view/qt/window.py +++ b/bauh/view/qt/window.py @@ -36,6 +36,7 @@ from bauh.view.qt.thread import UpdateSelectedApps, RefreshApps, UninstallApp, D from bauh.view.qt.view_model import PackageView from bauh.view.qt.view_utils import load_icon from bauh.view.util import util, resource +from bauh.view.util.translation import I18n DARK_ORANGE = '#FF4500' @@ -50,7 +51,7 @@ class ManageWindow(QWidget): signal_user_res = pyqtSignal(bool) signal_table_update = pyqtSignal() - def __init__(self, i18n: dict, icon_cache: MemoryCache, manager: SoftwareManager, disk_cache: bool, + def __init__(self, i18n: I18n, icon_cache: MemoryCache, manager: SoftwareManager, disk_cache: bool, download_icons: bool, screen_size, suggestions: bool, display_limit: int, config: Configuration, context: ApplicationContext, notifications: bool, http_client: HttpClient, logger: logging.Logger, tray_icon=None): @@ -231,7 +232,7 @@ class ManageWindow(QWidget): self.thread_downgrade = self._bind_async_action(DowngradeApp(self.manager, self.i18n), finished_call=self._finish_downgrade) self.thread_suggestions = self._bind_async_action(FindSuggestions(man=self.manager), finished_call=self._finish_search, only_finished=True) self.thread_run_app = self._bind_async_action(LaunchApp(self.manager), finished_call=self._finish_run_app, only_finished=False) - self.thread_custom_action = self._bind_async_action(CustomAction(manager=self.manager), finished_call=self._finish_custom_action) + self.thread_custom_action = self._bind_async_action(CustomAction(manager=self.manager, i18n=self.i18n), finished_call=self._finish_custom_action) self.thread_screenshots = self._bind_async_action(GetScreenshots(self.manager), finished_call=self._finish_get_screenshots) self.thread_apply_filters = ApplyFilters() @@ -239,7 +240,7 @@ class ManageWindow(QWidget): self.thread_apply_filters.signal_table.connect(self._update_table_and_upgrades) self.signal_table_update.connect(self.thread_apply_filters.stop_waiting) - self.thread_install = InstallPackage(manager=self.manager, disk_cache=self.disk_cache, icon_cache=self.icon_cache, locale_keys=self.i18n) + self.thread_install = InstallPackage(manager=self.manager, disk_cache=self.disk_cache, icon_cache=self.icon_cache, i18n=self.i18n) self._bind_async_action(self.thread_install, finished_call=self._finish_install) self.thread_animate_progress = AnimateProgress() @@ -289,7 +290,7 @@ class ManageWindow(QWidget): self.dialog_about = None self.first_refresh = suggestions - self.thread_warnings = ListWarnings(man=manager, locale_keys=i18n) + self.thread_warnings = ListWarnings(man=manager, i18n=i18n) self.thread_warnings.signal_warnings.connect(self._show_warnings) def set_tray_icon(self, tray_icon): @@ -349,7 +350,7 @@ class ManageWindow(QWidget): self.thread_animate_progress.pause() diag = ConfirmationDialog(title=msg['title'], body=msg['body'], - locale_keys=self.i18n, + i18n=self.i18n, components=msg['components'], confirmation_label=msg['confirmation_label'], deny_label=msg['deny_label']) @@ -792,7 +793,7 @@ class ManageWindow(QWidget): if to_update and dialog.ask_confirmation(title=self.i18n['manage_window.upgrade_all.popup.title'], body=self.i18n['manage_window.upgrade_all.popup.body'], - locale_keys=self.i18n, + i18n=self.i18n, widgets=[UpdateToggleButton(None, self, self.i18n, clickable=False)]): pwd = None diff --git a/bauh/view/util/translation.py b/bauh/view/util/translation.py new file mode 100644 index 00000000..ae3a349d --- /dev/null +++ b/bauh/view/util/translation.py @@ -0,0 +1,67 @@ +import glob +import locale +from typing import Tuple + +from bauh.view.util import resource + + +class I18n(dict): + + def __init__(self, current_locale: dict, default_locale: dict): + super(I18n, self).__init__() + self.current = current_locale + self.default = default_locale + + def __getitem__(self, item): + try: + return self.current.__getitem__(item) + except KeyError: + if self.default: + return self.default.__getitem__(item) + else: + raise + + def get(self, *args, **kwargs): + res = self.current.get(args[0]) + + if res is None: + if self.default: + return self.default.get(*args, **kwargs) + else: + return self.current.get(*args, **kwargs) + + return res + + +def get_locale_keys(key: str = None, locale_dir: str = resource.get_path('locale')) -> Tuple[str, dict]: + + locale_path = None + + if key is None: + current_locale = locale.getdefaultlocale() + else: + current_locale = [key.strip().lower()] + + if current_locale: + current_locale = current_locale[0] + + for locale_file in glob.glob(locale_dir + '/*'): + name = locale_file.split('/')[-1] + + if current_locale == name or current_locale.startswith(name + '_'): + locale_path = locale_file + break + + if not locale_path: + return key, {} + + with open(locale_path, 'r') as f: + locale_keys = f.readlines() + + locale_obj = {} + for line in locale_keys: + if line: + keyval = line.strip().split('=') + locale_obj[keyval[0].strip()] = keyval[1].strip() + + return locale_path.split('/')[-1], locale_obj diff --git a/bauh/view/util/util.py b/bauh/view/util/util.py index f4f3efc7..c4ffb795 100644 --- a/bauh/view/util/util.py +++ b/bauh/view/util/util.py @@ -1,9 +1,6 @@ -import glob -import locale import os import subprocess import sys -from typing import Tuple from PyQt5.QtCore import QCoreApplication @@ -12,40 +9,6 @@ from bauh.commons.system import run_cmd from bauh.view.util import resource -def get_locale_keys(key: str = None, locale_dir: str = resource.get_path('locale')) -> Tuple[str, dict]: - - locale_path = None - - if key is None: - current_locale = locale.getdefaultlocale() - else: - current_locale = [key.strip().lower()] - - if current_locale: - current_locale = current_locale[0] - - for locale_file in glob.glob(locale_dir + '/*'): - name = locale_file.split('/')[-1] - - if current_locale == name or current_locale.startswith(name + '_'): - locale_path = locale_file - break - - if not locale_path: - locale_path = resource.get_path('locale/en') - - with open(locale_path, 'r') as f: - locale_keys = f.readlines() - - locale_obj = {} - for line in locale_keys: - if line: - keyval = line.strip().split('=') - locale_obj[keyval[0].strip()] = keyval[1].strip() - - return locale_path.split('/')[-1], locale_obj - - def notify_user(msg: str, icon_path: str = resource.get_path('img/logo.svg')): os.system("notify-send -a {} {} '{}'".format(__app_name__, "-i {}".format(icon_path) if icon_path else '', msg))