From d7e029705dd46adf57f6bad8d8247d0813f09d3b Mon Sep 17 00:00:00 2001 From: buhtz Date: Tue, 29 Aug 2023 14:19:14 +0200 Subject: [PATCH] feat: Message to approach translators after 10 manual starts of BIT GUI Add a dialog with a message about current state of translation and how to contribute to the translation. The dialog is shown to users after the first 10 manual starts of Back In Time GUI. It is accessible every time via the Help menu. Modifications in detail: - New dialog class "ApproachTranslatorDialog" - config: New variable "manual_starts_countdown" - languages.py: Add a dictionary containing the language specific completeness in percent for each language. Additional minor fixes and modifications: - Fixed a problem with determining available languages. - Snapshot settings dialog title is now consistent with its corresponding menu entry. - GitHub issue template: Add information about the mailing list. - README: Extended description about mailing list to make clear it is open not only to developers. --- .github/ISSUE_TEMPLATE.md | 3 +- CHANGES | 1 + README.md | 5 +- common/config.py | 23 ++++++++- common/languages.py | 1 + common/tools.py | 33 +++++++++--- qt/app.py | 45 ++++++++++++++--- qt/languagedialog.py | 104 ++++++++++++++++++++++++++++++++++---- qt/settingsdialog.py | 2 +- update_language_files.py | 3 ++ 10 files changed, 193 insertions(+), 27 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 8a371ffa8..488053c90 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,4 +1,5 @@ To help us diagnose the problem quickly, please provide the output of the console command `backintime --diagnostics`. -Additionally, please specify as precisely as you can the package or installation source where you got BackInTime. Sometimes there are multiple alternatives, like [for Arch-based distros](https://aur.archlinux.org/packages?K=backintime). +Additionally, please specify as precisely as you can the package or installation source where you got Back In Time from. Sometimes there are multiple alternatives, like in [for Arch-based distros](https://aur.archlinux.org/packages?K=backintime). +As an alternative fell free to use our mailing list for every topic about Back In Time. Visit the subscribtion page at https://mail.python.org/mailman3/lists/bit-dev.python.org or send an email with subject "Subscribe" to bit-dev-join@python.org. diff --git a/CHANGES b/CHANGES index 6fbb5f746..7d08af1bc 100644 --- a/CHANGES +++ b/CHANGES @@ -33,6 +33,7 @@ Version 1.3.4-dev (development of upcoming release) * Translation: Plural forms support (#1488). * Removed: Translation in Canadian English, British English and Javanese (#1455). * Added: Translation in Persian and Vietnamese (#1460). +* Added: Message to users (after 10 starts of BIT Gui) to motivate them contributing translations (#1473). Version 1.3.3 (2023-01-04) * Feature: New command line argument "--diagnostics" to show helpful info for better issue support (#1100) diff --git a/README.md b/README.md index 2cb026ed9..28db0cb35 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,10 @@ and [help wanted](https://github.com/bit-team/backintime/issues?q=is%3Aissue+is% * [FAQ - Frequently Asked Questions](FAQ.md) * [Source code documentation for developers](https://backintime-dev.readthedocs.org) * Use [Issues](https://github.com/bit-team/backintime/issues) to ask questions and report bugs. - * [Mailing list _bit-dev_](https://mail.python.org/mailman3/lists/bit-dev.python.org/) + * [Mailing list + _bit-dev_](https://mail.python.org/mailman3/lists/bit-dev.python.org/) for + **every topic**, question and idea about _Back In Time_. Despite its name + it is not restricted to development topics only. ## Installation diff --git a/common/config.py b/common/config.py index c844ff46a..32b38f680 100644 --- a/common/config.py +++ b/common/config.py @@ -47,7 +47,7 @@ # The bigger problem with config.py is that it do use translatebale strings. # Strings like this do not belong into a config file or its context. try: - _('Foo') + _('Cancel') except NameError: _ = lambda val: val @@ -568,6 +568,27 @@ def language(self) -> str: def setLanguage(self, language: str): self.setStrValue('global.language', language if language else '') + def manual_starts_countdown(self) -> int: + """Countdown value about how often the users startet the Back In Time + GUI. + + It is an internal variable not meant to be used or manipulated be the + users. At the end of the countown the + :py:class:`ApproachTranslatorDialog` is presented to the user. + + """ + return self.intValue('internal.manual_starts_countdown', 10) + + def decrement_manual_starts_countdown(self): + """Counts down to -1. + + See :py:func:`manual_starts_countdown()` for details. + """ + val = self.manual_starts_countdown() + + if val > -1: + self.setIntValue('internal.manual_starts_countdown', val - 1) + # SSH def sshSnapshotsPath(self, profile_id = None): #?Snapshot path on remote host. If the path is relative (no leading '/') diff --git a/common/languages.py b/common/languages.py index b34828d25..aed9e1c12 100644 --- a/common/languages.py +++ b/common/languages.py @@ -2084,6 +2084,7 @@ 'da': 25, 'de': 99, 'el': 12, + 'en': 100, 'eo': 34, 'es': 55, 'et': 23, diff --git a/common/tools.py b/common/tools.py index 0305fc101..479096947 100644 --- a/common/tools.py +++ b/common/tools.py @@ -196,24 +196,24 @@ def get_available_language_codes(): # full path of one mo-file # e.g. /usr/share/locale/de/LC_MESSAGES/backintime.mo - po = gettext.find(domain=_GETTEXT_DOMAIN, localedir=_GETTEXT_LOCALE_DIR) + mo = gettext.find(domain=_GETTEXT_DOMAIN, localedir=_GETTEXT_LOCALE_DIR) - if po: - po = pathlib.Path(po) + if mo: + mo = pathlib.Path(mo) else: # Workaround. This happens if LC_ALL=C and BIT don't use an explicite # language. Should be re-design. - po = _GETTEXT_LOCALE_DIR / 'xy' / 'LC_MESSAGES' / 'backintime.po' + mo = _GETTEXT_LOCALE_DIR / 'xy' / 'LC_MESSAGES' / 'backintime.mo' # e.g. de/LC_MESSAGES/backintime.mo - po = po.relative_to(_GETTEXT_LOCALE_DIR) + mo = mo.relative_to(_GETTEXT_LOCALE_DIR) # e.g. */LC_MESSAGES/backintime.mo - po = pathlib.Path('*') / pathlib.Path(*po.parts[1:]) + mo = pathlib.Path('*') / pathlib.Path(*mo.parts[1:]) - pofiles = _GETTEXT_LOCALE_DIR.rglob(str(po)) + mofiles = _GETTEXT_LOCALE_DIR.rglob(str(mo)) - return [p.relative_to(_GETTEXT_LOCALE_DIR).parts[0] for p in pofiles] + return [p.relative_to(_GETTEXT_LOCALE_DIR).parts[0] for p in mofiles] def get_language_names(language_code): @@ -263,6 +263,23 @@ def get_language_names(language_code): return result +def get_native_language_and_completeness(language_code): + """Return the language name in its native flavour and the completeness of + its translation in percent. + + Args: + language_code(str): The language code. + + Returns: + A two-entry tuple with language name as string and a percent as + integer. + """ + name = languages.names[language_code][language_code] + completeness = languages.completeness[language_code] + + return (name, completeness) + + # |------------------------------------| # | Miscellaneous, not categorized yet | # |------------------------------------| diff --git a/qt/app.py b/qt/app.py index e010d86c6..eb0e1ce0a 100644 --- a/qt/app.py +++ b/qt/app.py @@ -94,7 +94,7 @@ import snapshotsdialog import logviewdialog from restoredialog import RestoreDialog -from languagedialog import LanguageDialog +import languagedialog import messagebox @@ -400,6 +400,21 @@ def __init__(self, config, appInstance, qapp): SetupCron(self).start() + # Finished countdown of manual GUI starts + if 0 == self.config.manual_starts_countdown(): + + # Do nothing if English is the current used language + if self.config.language_used != 'en': + + # Show the message only if teh current used language is + # translated equal or less then 97% + self._open_approach_translator_dialog(cutoff=97) + + # BIT counts down how often the GUI was started. Until the end of that + # countdown a dialog with a text about contributing to translating + # BIT is prestented to the users. + self.config.decrement_manual_starts_countdown() + @property def showHiddenFiles(self): return self.config.boolValue('qt.show_hidden_files', False) @@ -473,7 +488,7 @@ def _create_actions(self): self.btnLastLogViewClicked, None, None), 'act_settings': ( - icon.SETTINGS, _('Manage profiles…'), + icon.SETTINGS, '{}…'.format(_('Manage profiles')), self.btnSettingsClicked, ['Ctrl+Shift+P'], None), 'act_shutdown': ( @@ -481,7 +496,7 @@ def _create_actions(self): None, None, _('Shut down system after snapshot has finished.')), 'act_setup_language': ( - None, _('Setup language…'), + None, '{}…'.format(_('Setup language')), self.slot_setup_language, None, None), 'act_quit': ( @@ -510,6 +525,9 @@ def _create_actions(self): 'act_help_bugreport': ( icon.BUG, _('Report a bug'), self.btnReportBugClicked, None, None), + 'act_help_translation': ( + None, _('Translation'), + self.slot_help_translation, None, None), 'act_help_about': ( icon.ABOUT, _('About'), self.btnAboutClicked, None, None), @@ -540,7 +558,7 @@ def _create_actions(self): icon.SHOW_HIDDEN, _('Show hidden files'), None, ['Ctrl+H'], None), 'act_snapshots_dialog': ( - icon.SNAPSHOTS, _('Compare snapshots…'), + icon.SNAPSHOTS, '{}…'.format(_('Compare snapshots')), self.btnSnapshotsClicked, None, None), } @@ -626,6 +644,7 @@ def _create_menubar(self): self.act_help_faq, self.act_help_question, self.act_help_bugreport, + self.act_help_translation, self.act_help_about, ) } @@ -1750,6 +1769,16 @@ def suspendMouseButtonNavigation(self): yield self.setMouseButtonNavigation() + def _open_approach_translator_dialog(self, cutoff=101): + code = self.config.language_used + name, perc = tools.get_native_language_and_completeness(code) + + if perc > cutoff: + return + + dlg = languagedialog.ApproachTranslatorDialog(self, name, perc) + dlg.exec() + # |-------| # | Slots | # |-------| @@ -1757,13 +1786,14 @@ def slot_setup_language(self): """Show a modal language settings dialog and modify the UI language settings.""" - dlg = LanguageDialog( + dlg = languagedialog.LanguageDialog( used_language_code=self.config.language_used, configured_language_code=self.config.language()) dlg.exec() - if dlg.result() == 1 and self.config.language != dlg.language_code: + # Apply/OK pressed & the language value modified + if dlg.result() == 1 and self.config.language() != dlg.language_code: self.config.setLanguage(dlg.language_code) @@ -1771,6 +1801,9 @@ def slot_setup_language(self): 'restarting Back In Time.'), widget_to_center_on=dlg) + def slot_help_translation(self): + self._open_approach_translator_dialog() + class About(QDialog): def __init__(self, parent = None): diff --git a/qt/languagedialog.py b/qt/languagedialog.py index 128d6dcd4..c25b19c02 100644 --- a/qt/languagedialog.py +++ b/qt/languagedialog.py @@ -1,17 +1,20 @@ -import locale +import textwrap from PyQt5.QtCore import Qt +from PyQt5.QtGui import QCursor from PyQt5.QtWidgets import (QApplication, QDialog, QWidget, + QTabWidget, QScrollArea, QGridLayout, QVBoxLayout, QDialogButtonBox, QRadioButton, + QLabel, + QToolTip, ) import tools import qttools -import logger import languages @@ -22,7 +25,7 @@ def __init__(self, used_language_code: str, configured_language_code: str): self.used_language_code = used_language_code self.configured_language_code = configured_language_code - self.setWindowTitle(_('Language selection')) + self.setWindowTitle(_('Setup language')) self.setWindowFlag(Qt.WindowMaximizeButtonHint, True) scroll = QScrollArea(self) @@ -37,12 +40,15 @@ def __init__(self, used_language_code: str, configured_language_code: str): new_width = self._calculate_scroll_area_width() self._scroll.setMinimumWidth(new_width) - button = QDialogButtonBox(QDialogButtonBox.Apply, self) - button.clicked.connect(self.accept) + buttonbox = QDialogButtonBox( + QDialogButtonBox.Cancel | QDialogButtonBox.Ok, self) + + buttonbox.accepted.connect(self.accept) + buttonbox.rejected.connect(self.reject) layout = QVBoxLayout(self) layout.addWidget(scroll) - layout.addWidget(button) + layout.addWidget(buttonbox) def _calculate_scroll_area_width(self): """Credits: @@ -61,11 +67,13 @@ def _create_radio_button(self, lang_code, label, tooltip) -> QRadioButton: r.lang_code = lang_code # Is it the current used AND configured language? - if r.lang_code == self.used_language_code and r.lang_code == self.configured_language_code: + if (r.lang_code == self.used_language_code + and r.lang_code == self.configured_language_code): + r.setChecked(True) # "System default" - elif self.configured_language_code == '' and r.lang_code == None: + elif self.configured_language_code == '' and r.lang_code is None: r.setChecked(True) return r @@ -134,7 +142,7 @@ def _language_widget(self): tooltip = '{}\n{}'.format( tooltip, _('Translated: {percent}').format( - percent=f'{languages.completeness[code]}%') + percent=f'{complete}%') ) # Create button @@ -156,3 +164,81 @@ def slot_radio(self, val): if btn.isChecked(): self.language_code = btn.lang_code + + +class ApproachTranslatorDialog(QDialog): + """Prestens a message to the users to motivate them contributing to the + translation of Back In Time. + """ + + # ToDo (2023-08): Move to packages meta-data (pyproject.toml). + _URL_PLATFORM = 'https://translate.codeberg.org/engage/backintime' + _URL_PROJECT = 'https://github.com/bit-team/backintime' + + @staticmethod + def _complete_text(language, percent): + + # Note: The length of the variable names in that string are on + # purpose. It is relevant when wrapping the text. + txt = _( + 'Hello' + '\n' + 'You have used Back In Time in the {language} ' + 'language a few times by now.' + '\n' + 'The translation of your installed version of Back In Time ' + 'into {language} is {perc} complete. Regardless of your ' + 'level of technical expertise, you can contribute to the ' + 'translation and thus Back In Time itself.' + '\n' + 'Please visit the {translation_platform_url} if you wish ' + 'to contribute. For further assistance and questions, ' + 'please visit the {back_in_time_project_website}.' + '\n' + 'We apologize for the interruption, and this message ' + 'will not be shown again. This dialog is available at ' + 'any time via the help menu.' + '\n' + 'Your Back In Time Team.' + ) + + # Wrap the lines, insert
tag as linebreak and wrap paragraphs in + #

tags. + result = '' + for t in txt.split('\n'): + result += '

' + '
'.join(textwrap.wrap(t, width=60)) + '

' + + # Insert data in placeholder variables. + result = result.format( + language=f'{language}', + perc=f'{percent} %', + translation_platform_url='{}'.format( + __class__._URL_PLATFORM, + _('translation platform')), + back_in_time_project_website='Back In Time {}'.format( + __class__._URL_PROJECT, + _('Website')) + ) + + return result + + def __init__(self, parent, language_name, completeness): + super().__init__(parent) + + self.setWindowTitle(_('Your translation')) + self.setWindowFlag(Qt.WindowMaximizeButtonHint, True) + + txt = __class__._complete_text(language_name, completeness) + widget = QLabel(txt, self) + widget.setOpenExternalLinks(True) + widget.linkHovered.connect(self.slot_link_hovered) + + button = QDialogButtonBox(QDialogButtonBox.Ok, self) + button.clicked.connect(self.accept) + + layout = QVBoxLayout(self) + layout.addWidget(widget) + layout.addWidget(button) + + def slot_link_hovered(self, url): + QToolTip.showText(QCursor.pos(), url.strip('https://')) diff --git a/qt/settingsdialog.py b/qt/settingsdialog.py index 5ea667e49..6798138cf 100644 --- a/qt/settingsdialog.py +++ b/qt/settingsdialog.py @@ -84,7 +84,7 @@ def __init__(self, parent): self.config.setErrorHandler(self.errorHandler) self.setWindowIcon(icon.SETTINGS_DIALOG) - self.setWindowTitle(_('Settings')) + self.setWindowTitle(_('Manage profiles')) self.mainLayout = QVBoxLayout(self) diff --git a/update_language_files.py b/update_language_files.py index 39dcc95b0..daeae8fe0 100755 --- a/update_language_files.py +++ b/update_language_files.py @@ -185,6 +185,9 @@ def create_completeness_dict(): pof.save() + # "en" is the source language + result['en'] = 100 + # info print(json.dumps(result, indent=4))