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))