diff --git a/.gitignore b/.gitignore index c98a4de2..c75b8eed 100644 --- a/.gitignore +++ b/.gitignore @@ -15,3 +15,4 @@ vorta.egg-info .vagrant *.log htmlcov +*.qm diff --git a/.tx/config b/.tx/config new file mode 100644 index 00000000..831e73a8 --- /dev/null +++ b/.tx/config @@ -0,0 +1,10 @@ +[main] +host = https://www.transifex.com + +[vorta.vorta] +file_filter = src/vorta/i18n/ts/vorta..ts +minimum_perc = 0 +source_file = src/vorta/i18n/ts/vorta.en_US.ts +source_lang = en_US +type = QT + diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 714ce6b6..63564d46 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -53,3 +53,76 @@ To test for style errors: ``` $ flake8 ``` + +## Working with translations + +NOTE: we are currently still working on the original strings. + DO NO TRANSLATION WORK EXCEPT IF YOU ARE WILLING TO DO DOUBLE WORK. + +Translations are updated there: https://www.transifex.com/borgbase/vorta/ + +### Policy for translations + +- no google translate or other automated translation. +- only native or as-good-as-native speakers should translate. +- as there is a need for continued maintenance, a translator should be also a + user of vorta, having some own interest in the translation (one-time + translations are not that helpful if there is noone updating them regularly) +- a translation must have >90% translated strings. if a translation falls + and stays below that for a longer time, it will not be used by vorta and + ultimately, it will get removed from the repository also. + +### Adding a new language + +- Only add a new language if you are willing to also update the translation + in future, when new strings are added and existing strings change. +- Request a new language via transifex. +- TODO: add notes here what the maintainer has to do + +### Updating a language + +- Please only work on a translation if you are a native speaker or you have + similar language skills. +- Edit the language on transifex. + +### Data Flow to/from transifex + +- extract: make translations-from-source +- push: make translations-push +- pull: make translations-pull +- compile: make translations-to-qm + + +### Notes for developers + +- original strings in .ui and .py must be American English (en_US) +- in English, not translated: + + - log messages (log file as well as log output on console or elsewhere) + - other console output, print(). + - docs + - py source code, comments, docstrings +- translated: + + - GUI texts / messages +- in Qt (sub)classes, use self.tr("English string"), scope will + be the instance class name. +- elsewhere use vorta.i18n.translate("scopename", "English string") +- to only mark for string extraction, but not immediately translate, + use vorta.i18n.trans_late function. + Later, to translate, use vorta.i18n.translate (giving same scope). + +### Required Software + +To successfully run the translation-related Makefile targets, the translations +maintainer needs: + +- make tool +- pylupdate5 +- lrelease +- transifex-client pypi package + (should be already there via requirements.d/dev.txt) + +Debian 9 "Stretch": + +apt install qttools5-dev-tools pyqt5-dev-tools diff --git a/Makefile b/Makefile index 89ebdb02..861aa6ac 100644 --- a/Makefile +++ b/Makefile @@ -1,3 +1,8 @@ +export VORTA_SRC := src/vorta +export QT_SELECT=5 + +.PHONY : help +.DEFAULT_GOAL := help Vorta.app: #pyrcc5 -o src/vorta/views/collection_rc.py src/vorta/assets/icons/collection.qrc @@ -21,12 +26,12 @@ pypi-release: python setup.py sdist twine upload dist/vorta-0.6.5.tar.gz -bump-version: +bump-version: ## Add new version tag and push to upstream repo. bumpversion patch #bumpversion minor git push upstream -travis-debug: +travis-debug: ## Prepare connecting to Travis instance via SSH. curl -s -X POST \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ @@ -34,3 +39,22 @@ travis-debug: -H "Authorization: token ${TRAVIS_TOKEN}" \ -d '{ "quiet": true }' \ https://api.travis-ci.org/job/${TRAVIS_JOB_ID}/debug + +translations-from-source: ## Extract strings from source code / UI files, merge into .ts. + pylupdate5 -verbose -translate-function trans_late \ + $$VORTA_SRC/*.py $$VORTA_SRC/views/*.py $$VORTA_SRC/borg/*.py \ + $$VORTA_SRC/assets/UI/*.ui \ + -ts $$VORTA_SRC/i18n/ts/vorta.en_US.ts + +translations-push: translations-from-source ## Upload .ts to Transifex. + tx push -s + +translations-pull: ## Download .ts from Transifex. + tx pull -a + +translations-to-qm: ## Compile .ts text files to binary .qm files. + for f in $$(ls $$VORTA_SRC/i18n/ts/vorta.*.ts); do lrelease $$f -qm $$VORTA_SRC/i18n/qm/$$(basename $$f .ts).qm; done + + +help: + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' diff --git a/requirements.d/Brewfile b/requirements.d/Brewfile new file mode 100644 index 00000000..45ed3d11 --- /dev/null +++ b/requirements.d/Brewfile @@ -0,0 +1,6 @@ +# Install required non-Python dev packages using Homebrew on macOS: +# Run `brew bundle` while in this folder + +brew 'qt' +brew 'hub' +cask 'qt-creator' diff --git a/requirements.d/dev.txt b/requirements.d/dev.txt index 3429be2a..928d2c08 100644 --- a/requirements.d/dev.txt +++ b/requirements.d/dev.txt @@ -1,3 +1,4 @@ +transifex-client pytest pytest-qt pytest-mock diff --git a/src/vorta/application.py b/src/vorta/application.py index 8964d30c..0ddc3944 100644 --- a/src/vorta/application.py +++ b/src/vorta/application.py @@ -5,6 +5,7 @@ import fcntl from PyQt5 import QtCore from PyQt5.QtWidgets import QApplication +from .i18n import init_translations, translate from .tray_menu import TrayMenu from .scheduler import VortaScheduler from .models import BackupProfileModel @@ -43,6 +44,8 @@ class VortaApp(QApplication): sys.exit(1) super().__init__(args_raw) + init_translations(self) + self.setQuitOnLastWindowClosed(False) self.scheduler = VortaScheduler(self) @@ -68,7 +71,7 @@ class VortaApp(QApplication): thread = BorgCreateThread(msg['cmd'], msg, parent=self) thread.start() else: - self.backup_log_event.emit(msg['message']) + self.backup_log_event.emit(translate('messages', msg['message'])) def open_main_window_action(self): self.main_window.show() diff --git a/src/vorta/assets/UI/profileadd.ui b/src/vorta/assets/UI/profileadd.ui index bbe47732..ff064d3d 100644 --- a/src/vorta/assets/UI/profileadd.ui +++ b/src/vorta/assets/UI/profileadd.ui @@ -7,7 +7,7 @@ 0 0 532 - 289 + 293 @@ -56,7 +56,7 @@ - <html><head/><body><p>Backup profiles allow for granular backups from different sources to different destinations. You could e.g. back up essential documents to a remote repository via Wifi, while doing a full backup onto a thumb drive.</p><p>Repositories and SSH keys are shared between profiles. Source folders, active destination repo, allowed networks, pruning, validation and scheduling are per-profile.</p></body></html> + <html><head/><body><p>Backup profiles allow for granular backups from different sources to different destinations. You could e.g. back up essential documents to a remote repository via Wifi, while doing a full backup onto a local storage device.</p><p>Repositories and SSH keys are shared between profiles. Source folders, active destination repo, allowed networks, pruning, validation and scheduling are per-profile.</p></body></html> true diff --git a/src/vorta/assets/UI/scheduletab.ui b/src/vorta/assets/UI/scheduletab.ui index 05a231b1..b515fbe6 100644 --- a/src/vorta/assets/UI/scheduletab.ui +++ b/src/vorta/assets/UI/scheduletab.ui @@ -244,7 +244,7 @@ font-weight: bold; - Prune old Snapshots after each backup. + Prune old Archives after each backup. false diff --git a/src/vorta/borg/borg_thread.py b/src/vorta/borg/borg_thread.py index 57287766..4f474107 100644 --- a/src/vorta/borg/borg_thread.py +++ b/src/vorta/borg/borg_thread.py @@ -10,6 +10,7 @@ from PyQt5 import QtCore from PyQt5.QtWidgets import QApplication from subprocess import Popen, PIPE +from vorta.i18n import trans_late from vorta.models import EventLogModel, BackupProfileMixin from vorta.utils import keyring @@ -88,22 +89,22 @@ class BorgThread(QtCore.QThread, BackupProfileMixin): # Do checks to see if running Borg is possible. if cls.is_running(): - ret['message'] = 'Backup is already in progress.' + ret['message'] = trans_late('messages', 'Backup is already in progress.') return ret if cls.prepare_bin() is None: - ret['message'] = 'Borg binary was not found.' + ret['message'] = trans_late('messages', 'Borg binary was not found.') return ret if profile.repo is None: - ret['message'] = 'Add a backup repository first.' + ret['message'] = trans_late('messages', 'Add a backup repository first.') return ret # Try to get password from chosen keyring backend. try: ret['password'] = keyring.get_password("vorta-repo", profile.repo.url) except Exception: - ret['message'] = 'Please make sure you grant Vorta permission to use the Keychain.' + ret['message'] = trans_late('messages', 'Please make sure you grant Vorta permission to use the Keychain.') return ret ret['ssh_key'] = profile.ssh_key @@ -216,7 +217,7 @@ class BorgThread(QtCore.QThread, BackupProfileMixin): self.updated.emit(msg) def started_event(self): - self.updated.emit('Task started') + self.updated.emit(self.tr('Task started')) def finished_event(self, result): self.result.emit(result) diff --git a/src/vorta/borg/check.py b/src/vorta/borg/check.py index 71a79a88..2dd1819e 100644 --- a/src/vorta/borg/check.py +++ b/src/vorta/borg/check.py @@ -8,7 +8,7 @@ class BorgCheckThread(BorgThread): def started_event(self): self.app.backup_started_event.emit() - self.app.backup_log_event.emit('Starting consistency check..') + self.app.backup_log_event.emit(self.tr('Starting consistency check..')) def finished_event(self, result): self.app.backup_finished_event.emit(result) diff --git a/src/vorta/borg/create.py b/src/vorta/borg/create.py index b7f31fc2..6d9b3155 100644 --- a/src/vorta/borg/create.py +++ b/src/vorta/borg/create.py @@ -3,6 +3,7 @@ import tempfile from dateutil import parser import subprocess +from ..i18n import trans_late from ..utils import get_current_wifi, format_archive_name from ..models import SourceFileModel, ArchiveModel, WifiSettingModel, RepoModel from .borg_thread import BorgThread @@ -11,7 +12,7 @@ from .borg_thread import BorgThread class BorgCreateThread(BorgThread): def process_result(self, result): if result['returncode'] in [0, 1] and 'archive' in result['data']: - new_snapshot, created = ArchiveModel.get_or_create( + new_archive, created = ArchiveModel.get_or_create( snapshot_id=result['data']['archive']['id'], defaults={ 'name': result['data']['archive']['name'], @@ -21,7 +22,7 @@ class BorgCreateThread(BorgThread): 'size': result['data']['archive']['stats']['deduplicated_size'] } ) - new_snapshot.save() + new_archive.save() if 'cache' in result['data'] and created: stats = result['data']['cache']['stats'] repo = RepoModel.get(id=result['params']['repo_id']) @@ -31,14 +32,14 @@ class BorgCreateThread(BorgThread): repo.total_unique_chunks = stats['total_unique_chunks'] repo.save() - self.app.backup_log_event.emit('Backup finished.') + self.app.backup_log_event.emit(self.tr('Backup finished.')) def log_event(self, msg): self.app.backup_log_event.emit(msg) def started_event(self): self.app.backup_started_event.emit() - self.app.backup_log_event.emit('Backup started.') + self.app.backup_log_event.emit(self.tr('Backup started.')) def finished_event(self, result): self.app.backup_finished_event.emit(result) @@ -74,7 +75,7 @@ class BorgCreateThread(BorgThread): n_backup_folders = SourceFileModel.select().count() if n_backup_folders == 0: - ret['message'] = 'Add some folders to back up first.' + ret['message'] = trans_late('messages', 'Add some folders to back up first.') return ret current_wifi = get_current_wifi() @@ -89,11 +90,11 @@ class BorgCreateThread(BorgThread): ) ) if wifi_is_disallowed.count() > 0 and profile.repo.is_remote_repo(): - ret['message'] = 'Current Wifi is not allowed.' + ret['message'] = trans_late('messages', 'Current Wifi is not allowed.') return ret if not profile.repo.is_remote_repo() and not os.path.exists(profile.repo.url): - ret['message'] = 'Repo folder not mounted or moved.' + ret['message'] = trans_late('messages', 'Repo folder not mounted or moved.') return ret cmd = ['borg', 'create', '--list', '--info', '--log-json', '--json', '--filter=AM', '-C', profile.compression] @@ -129,10 +130,10 @@ class BorgCreateThread(BorgThread): ret['profile'] = profile ret['repo'] = profile.repo if cls.pre_post_backup_cmd(ret) != 0: - ret['message'] = 'Pre-backup command returned non-zero exit code.' + ret['message'] = trans_late('messages', 'Pre-backup command returned non-zero exit code.') return ret - ret['message'] = 'Starting backup..' + ret['message'] = trans_late('messages', 'Starting backup..') ret['ok'] = True ret['cmd'] = cmd diff --git a/src/vorta/borg/extract.py b/src/vorta/borg/extract.py index f97d774f..5db2c5d0 100644 --- a/src/vorta/borg/extract.py +++ b/src/vorta/borg/extract.py @@ -8,12 +8,12 @@ class BorgExtractThread(BorgThread): def started_event(self): self.app.backup_started_event.emit() - self.app.backup_log_event.emit('Downloading files from archive..') + self.app.backup_log_event.emit(self.tr('Downloading files from archive..')) def finished_event(self, result): self.app.backup_finished_event.emit(result) self.result.emit(result) - self.app.backup_log_event.emit('Restored files from archive.') + self.app.backup_log_event.emit(self.tr('Restored files from archive.')) @classmethod def prepare(cls, profile, archive_name, selected_files, destination_folder): diff --git a/src/vorta/borg/info.py b/src/vorta/borg/info.py index b8705682..dbe676df 100644 --- a/src/vorta/borg/info.py +++ b/src/vorta/borg/info.py @@ -10,7 +10,7 @@ FakeProfile = namedtuple('FakeProfile', ['repo', 'name', 'ssh_key']) class BorgInfoThread(BorgThread): def started_event(self): - self.updated.emit('Validating existing repo...') + self.updated.emit(self.tr('Validating existing repo...')) @classmethod def prepare(cls, params): diff --git a/src/vorta/borg/init.py b/src/vorta/borg/init.py index 08957a4a..a68c57b4 100644 --- a/src/vorta/borg/init.py +++ b/src/vorta/borg/init.py @@ -7,7 +7,7 @@ from vorta.utils import keyring class BorgInitThread(BorgThread): def started_event(self): - self.updated.emit('Setting up new repo...') + self.updated.emit(self.tr('Setting up new repo...')) @classmethod def prepare(cls, params): diff --git a/src/vorta/borg/list_archive.py b/src/vorta/borg/list_archive.py index 1e3ff8c5..294fb2f9 100644 --- a/src/vorta/borg/list_archive.py +++ b/src/vorta/borg/list_archive.py @@ -8,11 +8,11 @@ class BorgListArchiveThread(BorgThread): def started_event(self): self.app.backup_started_event.emit() - self.app.backup_log_event.emit('Getting archive content..') + self.app.backup_log_event.emit(self.tr('Getting archive content..')) def finished_event(self, result): self.app.backup_finished_event.emit(result) - self.app.backup_log_event.emit('Done getting archive content.') + self.app.backup_log_event.emit(self.tr('Done getting archive content.')) self.result.emit(result) @classmethod diff --git a/src/vorta/borg/list_repo.py b/src/vorta/borg/list_repo.py index 52f801ff..e146c5b1 100644 --- a/src/vorta/borg/list_repo.py +++ b/src/vorta/borg/list_repo.py @@ -10,12 +10,12 @@ class BorgListRepoThread(BorgThread): def started_event(self): self.app.backup_started_event.emit() - self.app.backup_log_event.emit('Refreshing snapshots..') + self.app.backup_log_event.emit(self.tr('Refreshing archives...')) def finished_event(self, result): self.app.backup_finished_event.emit(result) self.result.emit(result) - self.app.backup_log_event.emit('Refreshing snapshots done.') + self.app.backup_log_event.emit(self.tr('Refreshing archives done.')) @classmethod def prepare(cls, profile): @@ -36,21 +36,23 @@ class BorgListRepoThread(BorgThread): def process_result(self, result): if result['returncode'] == 0: repo, created = RepoModel.get_or_create(url=result['cmd'][-1]) - remote_snapshots = result['data'].get('archives', []) + if not result['data']: + result['data'] = {} # TODO: Workaround for tests. Can't read mock results 2x. + remote_archives = result['data'].get('archives', []) - # Delete snapshots that don't exist on the remote side - for snapshot in ArchiveModel.select().where(ArchiveModel.repo == repo.id): - if not list(filter(lambda s: s['id'] == snapshot.snapshot_id, remote_snapshots)): - snapshot.delete_instance() + # Delete archives that don't exist on the remote side + for archive in ArchiveModel.select().where(ArchiveModel.repo == repo.id): + if not list(filter(lambda s: s['id'] == archive.snapshot_id, remote_archives)): + archive.delete_instance() - # Add remote snapshots we don't have locally. - for snapshot in result['data'].get('archives', []): - new_snapshot, _ = ArchiveModel.get_or_create( - snapshot_id=snapshot['id'], + # Add remote archives we don't have locally. + for archive in result['data'].get('archives', []): + new_archive, _ = ArchiveModel.get_or_create( + snapshot_id=archive['id'], defaults={ 'repo': repo.id, - 'name': snapshot['name'], - 'time': parser.parse(snapshot['time']) + 'name': archive['name'], + 'time': parser.parse(archive['time']) } ) - new_snapshot.save() + new_archive.save() diff --git a/src/vorta/borg/mount.py b/src/vorta/borg/mount.py index 3ad8c189..791089b4 100644 --- a/src/vorta/borg/mount.py +++ b/src/vorta/borg/mount.py @@ -4,7 +4,7 @@ from .borg_thread import BorgThread class BorgMountThread(BorgThread): def started_event(self): - self.updated.emit('Mounting archive into folder...') + self.updated.emit(self.tr('Mounting archive into folder...')) @classmethod def prepare(cls, profile): diff --git a/src/vorta/borg/prune.py b/src/vorta/borg/prune.py index 4a056dc4..0d2a7ab4 100644 --- a/src/vorta/borg/prune.py +++ b/src/vorta/borg/prune.py @@ -9,12 +9,12 @@ class BorgPruneThread(BorgThread): def started_event(self): self.app.backup_started_event.emit() - self.app.backup_log_event.emit('Pruning old snapshots..') + self.app.backup_log_event.emit(self.tr('Pruning old archives...')) def finished_event(self, result): self.app.backup_finished_event.emit(result) self.result.emit(result) - self.app.backup_log_event.emit('Pruning done.') + self.app.backup_log_event.emit(self.tr('Pruning done.')) @classmethod def prepare(cls, profile): diff --git a/src/vorta/borg/umount.py b/src/vorta/borg/umount.py index e040f1be..cd385daa 100644 --- a/src/vorta/borg/umount.py +++ b/src/vorta/borg/umount.py @@ -1,11 +1,12 @@ import psutil from .borg_thread import BorgThread +from ..i18n import trans_late class BorgUmountThread(BorgThread): def started_event(self): - self.updated.emit('Unmounting archive...') + self.updated.emit(self.tr('Unmounting archive...')) @classmethod def prepare(cls, profile): @@ -22,7 +23,7 @@ class BorgUmountThread(BorgThread): ret['active_mount_points'].append(p.mountpoint) if len(ret['active_mount_points']) == 0: - ret['message'] = 'No active Borg mounts found.' + ret['message'] = trans_late('messages', 'No active Borg mounts found.') return ret cmd = ['borg', 'umount', '--log-json'] diff --git a/src/vorta/i18n/__init__.py b/src/vorta/i18n/__init__.py new file mode 100644 index 00000000..bc5fb422 --- /dev/null +++ b/src/vorta/i18n/__init__.py @@ -0,0 +1,40 @@ +""" +internationalisation (i18n) support code +""" +import logging +import os + +from PyQt5.QtCore import QTranslator, QLocale + +logger = logging.getLogger(__name__) + + +def init_translations(app): + global application, translator, locale # if we don't keep a reference on these, it stops working. pyqt bug? + application = app + translator = QTranslator() + locale = QLocale() + qm_path = os.path.abspath(os.path.join(os.path.dirname(__file__), 'qm')) + ui_langs = locale.uiLanguages() + succeeded = translator.load(locale, 'vorta', prefix='.', directory=qm_path) # e.g. vorta/i18n/qm/vorta.de_DE.qm + if succeeded: + app.installTranslator(translator) + logger.debug('Loading translation %s for %r.' % ('succeeded' if succeeded else 'failed', ui_langs)) + + +def translate(*args, **kwargs): + """ + small wrapper around QCoreApplication.translate() + """ + global application # see init_translation + return application.translate(*args, **kwargs) + + +def trans_late(scope, text): + """ + dummy translate function - only purpose is to mark scope/text for message extraction. + + later, at another place, there will be a translate(scope, text) call, with same + scope and text, potentially not literal scope, not literal text, but from a variable. + """ + return text diff --git a/src/vorta/i18n/qm/.gitkeep b/src/vorta/i18n/qm/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/vorta/i18n/ts/.gitkeep b/src/vorta/i18n/ts/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/src/vorta/models.py b/src/vorta/models.py index 2bc3b3e5..d79c306c 100644 --- a/src/vorta/models.py +++ b/src/vorta/models.py @@ -10,6 +10,7 @@ from datetime import datetime, timedelta import peewee as pw from playhouse.migrate import SqliteMigrator, migrate +from vorta.i18n import trans_late from vorta.utils import slugify SCHEMA_VERSION = 10 @@ -108,7 +109,7 @@ class SourceFileModel(pw.Model): class ArchiveModel(pw.Model): - """A snapshot to a specific remote repository.""" + """An archive in a remote repository.""" snapshot_id = pw.CharField(unique=True) name = pw.CharField() repo = pw.ForeignKeyField(RepoModel, backref='archives') @@ -191,25 +192,37 @@ def get_misc_settings(): 'key': 'use_light_icon', 'value': False, 'type': 'checkbox', - 'label': 'Use light system tray icon (applies after restart, useful for dark themes).' + 'label': trans_late('settings', + 'Use light system tray icon (applies after restart, useful for dark themes).') }, { 'key': 'enable_notifications', 'value': True, 'type': 'checkbox', - 'label': 'Display notifications when background tasks fail.' + 'label': trans_late('settings', + 'Display notifications when background tasks fail.') }, { 'key': 'enable_notifications_success', 'value': False, 'type': 'checkbox', - 'label': 'Also notify about successful background tasks.' + 'label': trans_late('settings', + 'Also notify about successful background tasks.') } ] if sys.platform == 'darwin': settings += [ - {'key': 'autostart', 'value': False, 'type': 'checkbox', - 'label': 'Add Vorta to Login Items in Preferences > Users and Groups > Login Items.'}, - {'key': 'check_for_updates', 'value': True, 'type': 'checkbox', - 'label': 'Check for updates on startup.'}, - {'key': 'updates_include_beta', 'value': False, 'type': 'checkbox', - 'label': 'Include pre-release versions when checking for updates.'}, + { + 'key': 'autostart', 'value': False, 'type': 'checkbox', + 'label': trans_late('settings', + 'Add Vorta to Login Items in Preferences > Users and Groups > Login Items.') + }, + { + 'key': 'check_for_updates', 'value': True, 'type': 'checkbox', + 'label': trans_late('settings', + 'Check for updates on startup.') + }, + { + 'key': 'updates_include_beta', 'value': False, 'type': 'checkbox', + 'label': trans_late('settings', + 'Include pre-release versions when checking for updates.') + }, ] return settings diff --git a/src/vorta/scheduler.py b/src/vorta/scheduler.py index e683f7d6..3900e22d 100644 --- a/src/vorta/scheduler.py +++ b/src/vorta/scheduler.py @@ -7,6 +7,7 @@ from vorta.borg.check import BorgCheckThread from vorta.borg.create import BorgCreateThread from vorta.borg.list_repo import BorgListRepoThread from vorta.borg.prune import BorgPruneThread +from vorta.i18n import translate from .models import BackupProfileModel, EventLogModel from .notifications import VortaNotifications @@ -21,6 +22,10 @@ class VortaScheduler(QtScheduler): self.start() self.reload() + def tr(self, *args, **kwargs): + scope = self.__class__.__name__ + return translate(scope, *args, **kwargs) + def reload(self): for profile in BackupProfileModel.select(): trigger = None @@ -47,7 +52,7 @@ class VortaScheduler(QtScheduler): if self.get_job(job_id) is not None and trigger is not None: self.reschedule_job(job_id, trigger=trigger) notifier = VortaNotifications.pick() - notifier.deliver('Vorta Scheduler', 'Background scheduler was changed.') + notifier.deliver(self.tr('Vorta Scheduler'), self.tr('Background scheduler was changed.')) logger.debug('Job for profile %s was rescheduled.', profile.name) elif trigger is not None: self.add_job( @@ -75,13 +80,13 @@ class VortaScheduler(QtScheduler): profile = BackupProfileModel.get(id=int(jobs[0][1])) return f"{jobs[0][0].strftime('%H:%M')} ({profile.name})" else: - return 'None scheduled' + return self.tr('None scheduled') def next_job_for_profile(self, profile_id): self.wakeup() job = self.get_job(str(profile_id)) if job is None: - return 'None scheduled' + return self.tr('None scheduled') else: return job.next_run_time.strftime('%Y-%m-%d %H:%M') @@ -90,7 +95,9 @@ class VortaScheduler(QtScheduler): profile = BackupProfileModel.get(id=profile_id) logger.info('Starting background backup for %s', profile.name) - notifier.deliver('Vorta Backup', 'Starting background backup for %s.' % profile.name, level='info') + notifier.deliver(self.tr('Vorta Backup'), + self.tr('Starting background backup for %s.') % profile.name, + level='info') msg = BorgCreateThread.prepare(profile) if msg['ok']: @@ -99,16 +106,18 @@ class VortaScheduler(QtScheduler): thread.start() thread.wait() if thread.process.returncode in [0, 1]: - notifier.deliver('Vorta Backup', 'Backup successful for %s.' % profile.name, level='info') + notifier.deliver(self.tr('Vorta Backup'), + self.tr('Backup successful for %s.') % profile.name, + level='info') logger.info('Backup creation successful.') self.post_backup_tasks(profile_id) else: - notifier.deliver('Vorta Backup', 'Error during backup creation.', level='error') + notifier.deliver(self.tr('Vorta Backup'), self.tr('Error during backup creation.'), level='error') logger.error('Error during backup creation.') else: logger.error('Conditions for backup not met. Aborting.') logger.error(msg['message']) - notifier.deliver('Vorta Backup', msg['message'], level='error') + notifier.deliver(self.tr('Vorta Backup'), translate('messages', msg['message']), level='error') def post_backup_tasks(self, profile_id): """ @@ -123,7 +132,7 @@ class VortaScheduler(QtScheduler): prune_thread.start() prune_thread.wait() - # Refresh snapshots + # Refresh archives msg = BorgListRepoThread.prepare(profile) if msg['ok']: list_thread = BorgListRepoThread(msg['cmd'], msg) diff --git a/src/vorta/tray_menu.py b/src/vorta/tray_menu.py index 889ba825..8f91a2fb 100644 --- a/src/vorta/tray_menu.py +++ b/src/vorta/tray_menu.py @@ -29,26 +29,26 @@ class TrayMenu(QSystemTrayIcon): status.setEnabled(False) if BorgThread.is_running(): - status.setText('Backup in Progress') - cancel_action = menu.addAction("Cancel Backup") + status.setText(self.tr('Backup in Progress')) + cancel_action = menu.addAction(self.tr('Cancel Backup')) cancel_action.triggered.connect(self.app.backup_cancelled_event.emit) else: - status.setText(f'Next Task: {self.app.scheduler.next_job}') + status.setText(self.tr('Next Task: %s') % self.app.scheduler.next_job) profiles = BackupProfileModel.select() if profiles.count() > 1: - profile_menu = menu.addMenu('Backup Now') + profile_menu = menu.addMenu(self.tr('Backup Now')) for profile in profiles: new_item = profile_menu.addAction(profile.name) new_item.triggered.connect(lambda state, i=profile.id: self.app.create_backup_action(i)) else: profile = profiles.first() - profile_menu = menu.addAction('Backup Now') + profile_menu = menu.addAction(self.tr('Backup Now')) profile_menu.triggered.connect(lambda state, i=profile.id: self.app.create_backup_action(i)) - settings_action = menu.addAction("Settings") + settings_action = menu.addAction(self.tr('Settings')) settings_action.triggered.connect(self.app.open_main_window_action) menu.addSeparator() - exit_action = menu.addAction("Exit") + exit_action = menu.addAction(self.tr('Exit')) exit_action.triggered.connect(self.app.quit) diff --git a/src/vorta/views/archive_tab.py b/src/vorta/views/archive_tab.py index a4db3154..5cb00ce9 100644 --- a/src/vorta/views/archive_tab.py +++ b/src/vorta/views/archive_tab.py @@ -11,6 +11,7 @@ from vorta.borg.mount import BorgMountThread from vorta.borg.extract import BorgExtractThread from vorta.borg.umount import BorgUmountThread from vorta.views.extract_dialog import ExtractDialog +from vorta.i18n import translate from vorta.utils import get_asset, pretty_bytes, choose_file_dialog, format_archive_name from vorta.models import BackupProfileMixin, ArchiveModel @@ -77,7 +78,7 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin): profile = self.profile() if profile.repo is not None: - self.toolBox.setItemText(0, f'Archives for {profile.repo.url}') + self.toolBox.setItemText(0, self.tr('Archives for %s') % profile.repo.url) archives = [s for s in profile.repo.archives.select().order_by(ArchiveModel.time.desc())] for row, archive in enumerate(archives): @@ -98,7 +99,7 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin): self._toggle_all_buttons(enabled=True) else: self.archiveTable.setRowCount(0) - self.toolBox.setItemText(0, 'Archives') + self.toolBox.setItemText(0, self.tr('Archives')) self._toggle_all_buttons(enabled=False) self.archiveNameTemplate.setText(profile.new_archive_name) @@ -107,11 +108,11 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin): def save_archive_template(self, tpl, key): profile = self.profile() try: - preview = 'Preview: ' + format_archive_name(profile, tpl) + preview = self.tr('Preview: %s') % format_archive_name(profile, tpl) setattr(profile, key, tpl) profile.save() except Exception: - preview = 'Error in archive name template.' + preview = self.tr('Error in archive name template.') if key == 'new_archive_name': self.archiveNamePreview.setText(preview) @@ -121,16 +122,16 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin): def check_action(self): params = BorgCheckThread.prepare(self.profile()) if not params['ok']: - self._set_status(params['message']) + self._set_status(translate(params['message'])) return # Conditions are met (borg binary available, etc) row_selected = self.archiveTable.selectionModel().selectedRows() if row_selected: - snapshot_cell = self.archiveTable.item(row_selected[0].row(), 3) - if snapshot_cell: - snapshot_name = snapshot_cell.text() - params['cmd'][-1] += f'::{snapshot_name}' + archive_cell = self.archiveTable.item(row_selected[0].row(), 3) + if archive_cell: + archive_name = archive_cell.text() + params['cmd'][-1] += f'::{archive_name}' thread = BorgCheckThread(params['cmd'], params, parent=self) thread.updated.connect(self._set_status) @@ -153,7 +154,7 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin): def prune_result(self, result): if result['returncode'] == 0: - self._set_status('Pruning finished.') + self._set_status(self.tr('Pruning finished.')) self.list_action() else: self._toggle_all_buttons(True) @@ -170,23 +171,23 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin): def list_result(self, result): self._toggle_all_buttons(True) if result['returncode'] == 0: - self._set_status('Refreshed snapshots.') + self._set_status(self.tr('Refreshed archives.')) self.populate_from_profile() def mount_action(self): profile = self.profile() params = BorgMountThread.prepare(profile) if not params['ok']: - self._set_status(params['message']) + self._set_status(translate(params['message'])) return # Conditions are met (borg binary available, etc) row_selected = self.archiveTable.selectionModel().selectedRows() if row_selected: - snapshot_cell = self.archiveTable.item(row_selected[0].row(), 3) - if snapshot_cell: - snapshot_name = snapshot_cell.text() - params['cmd'][-1] += f'::{snapshot_name}' + archive_cell = self.archiveTable.item(row_selected[0].row(), 3) + if archive_cell: + archive_name = archive_cell.text() + params['cmd'][-1] += f'::{archive_name}' def receive(): mount_point = dialog.selectedFiles() @@ -200,14 +201,14 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin): thread.result.connect(self.mount_result) thread.start() - dialog = choose_file_dialog(self, "Choose Mount Point") + dialog = choose_file_dialog(self, self.tr("Choose Mount Point")) dialog.open(receive) def mount_result(self, result): self._toggle_all_buttons(True) if result['returncode'] == 0: - self._set_status('Mounted successfully.') - self.mountButton.setText('Unmount') + self._set_status(self.tr('Mounted successfully.')) + self.mountButton.setText(self.tr('Unmount')) self.mountButton.clicked.disconnect() self.mountButton.clicked.connect(self.umount_action) else: @@ -218,7 +219,7 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin): profile = self.profile() params = BorgUmountThread.prepare(profile) if not params['ok']: - self._set_status(params['message']) + self._set_status(translate(params['message'])) return if self.mount_point in params['active_mount_points']: @@ -228,14 +229,14 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin): thread.result.connect(self.umount_result) thread.start() else: - self._set_status('Mount point not active. Try restarting Vorta.') + self._set_status(self.tr('Mount point not active. Try restarting Vorta.')) return def umount_result(self, result): self._toggle_all_buttons(True) if result['returncode'] == 0: - self._set_status('Un-mounted successfully.') - self.mountButton.setText('Mount') + self._set_status(self.tr('Un-mounted successfully.')) + self.mountButton.setText(self.tr('Mount')) self.mountButton.clicked.disconnect() self.mountButton.clicked.connect(self.mount_action) self.mount_point = None @@ -258,7 +259,7 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin): params = BorgListArchiveThread.prepare(profile) if not params['ok']: - self._set_status(params['message']) + self._set_status(translate(params['message'])) return params['cmd'][-1] += f'::{archive_name}' params['archive_name'] = archive_name @@ -270,7 +271,7 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin): thread.result.connect(self.list_archive_result) thread.start() else: - self._set_status('Select an archive to restore first.') + self._set_status(self.tr('Select an archive to restore first.')) def list_archive_result(self, result): self._set_status('') @@ -295,9 +296,9 @@ class ArchiveTab(ArchiveTabBase, ArchiveTabUI, BackupProfileMixin): thread.result.connect(self.extract_archive_result) thread.start() else: - self._set_status(params['message']) + self._set_status(translate(params['message'])) - dialog = choose_file_dialog(self, "Choose Extraction Point") + dialog = choose_file_dialog(self, self.tr("Choose Extraction Point")) dialog.open(receive) def extract_archive_result(self, result): diff --git a/src/vorta/views/main_window.py b/src/vorta/views/main_window.py index ae2b6e74..84fb1925 100644 --- a/src/vorta/views/main_window.py +++ b/src/vorta/views/main_window.py @@ -51,7 +51,7 @@ class MainWindow(MainWindowBase, MainWindowUI): self.app.backup_cancelled_event.connect(self.backup_cancelled_event) # Init profile list - self.profileSelector.addItem('+ Add New Profile', None) + self.profileSelector.addItem(self.tr('+ Add New Profile'), None) self.profileSelector.insertSeparator(2) for profile in BackupProfileModel.select(): self.profileSelector.addItem(profile.name, profile.id) @@ -72,7 +72,7 @@ class MainWindow(MainWindowBase, MainWindowUI): if BorgThread.is_running(): self.createStartBtn.setEnabled(False) self.cancelButton.setEnabled(True) - self.set_status('Backup in progress.', progress_max=0) + self.set_status(self.tr('Backup in progress.'), progress_max=0) def on_close_window(self): self.close() @@ -140,4 +140,4 @@ class MainWindow(MainWindowBase, MainWindowUI): def backup_cancelled_event(self): self._toggle_buttons(create_enabled=True) self.set_status(progress_max=100) - self.set_status('Task cancelled') + self.set_status(self.tr('Task cancelled')) diff --git a/src/vorta/views/misc_tab.py b/src/vorta/views/misc_tab.py index e6a992b5..2f43c247 100644 --- a/src/vorta/views/misc_tab.py +++ b/src/vorta/views/misc_tab.py @@ -1,5 +1,7 @@ from PyQt5 import uic from PyQt5.QtWidgets import QCheckBox + +from vorta.i18n import translate from vorta.utils import get_asset, open_app_at_startup from vorta.models import SettingsModel, BackupProfileMixin, get_misc_settings from vorta._version import __version__ @@ -19,7 +21,7 @@ class MiscTab(MiscTabBase, MiscTabUI, BackupProfileMixin): x = filter(lambda s: s['key'] == setting.key, get_misc_settings()) if not list(x): # Skip settings that aren't specified in vorta.models. continue - b = QCheckBox(setting.label) + b = QCheckBox(translate('settings', setting.label)) b.setCheckState(setting.value) b.setTristate(False) b.stateChanged.connect(lambda v, key=setting.key: self.save_setting(key, v)) diff --git a/src/vorta/views/profile_add_edit_dialog.py b/src/vorta/views/profile_add_edit_dialog.py index 838a6d63..d44b77d2 100644 --- a/src/vorta/views/profile_add_edit_dialog.py +++ b/src/vorta/views/profile_add_edit_dialog.py @@ -19,7 +19,7 @@ class AddProfileWindow(AddProfileBase, AddProfileUI): existing_profile = BackupProfileModel.get(id=rename_existing_id) self.profileNameField.setText(existing_profile.name) self.existing_id = rename_existing_id - self.modalTitle.setText('Rename Profile') + self.modalTitle.setText(self.tr('Rename Profile')) def _set_status(self, text): self.errorText.setText(text) @@ -36,13 +36,13 @@ class AddProfileWindow(AddProfileBase, AddProfileUI): name = self.profileNameField.text() # A name was entered? if len(name) == 0: - self._set_status('Please enter a profile name.') + self._set_status(self.tr('Please enter a profile name.')) return False # Profile with this name already exists? exists = BackupProfileModel.select().where(BackupProfileModel.name == name).count() if exists > 0: - self._set_status('A profile with this name already exists.') + self._set_status(self.tr('A profile with this name already exists.')) return False return True diff --git a/src/vorta/views/repo_add_dialog.py b/src/vorta/views/repo_add_dialog.py index 3f9ef4a4..3f6fff06 100644 --- a/src/vorta/views/repo_add_dialog.py +++ b/src/vorta/views/repo_add_dialog.py @@ -1,5 +1,7 @@ import re from PyQt5 import uic + +from ..i18n import translate from ..utils import get_private_keys, get_asset, choose_file_dialog from vorta.borg.init import BorgInitThread from vorta.borg.info import BorgInfoThread @@ -41,17 +43,17 @@ class AddRepoWindow(AddRepoBase, AddRepoUI): self.repoURL.setText(folder[0]) self.repoURL.setEnabled(False) self.sshComboBox.setEnabled(False) - self.repoLabel.setText('Repository Path:') + self.repoLabel.setText(self.tr('Repository Path:')) self.is_remote_repo = False - dialog = choose_file_dialog(self, "Choose Location of Borg Repository") + dialog = choose_file_dialog(self, self.tr("Choose Location of Borg Repository")) dialog.open(receive) def use_remote_repo_action(self): self.repoURL.setText('') self.repoURL.setEnabled(True) self.sshComboBox.setEnabled(True) - self.repoLabel.setText('Repository URL:') + self.repoLabel.setText(self.tr('Repository URL:')) self.is_remote_repo = True def run(self): @@ -65,7 +67,7 @@ class AddRepoWindow(AddRepoBase, AddRepoUI): self.thread = thread # Needs to be connected to self for tests to work. self.thread.start() else: - self._set_status(params['message']) + self._set_status(translate(params['message'])) def _set_status(self, text): self.errorText.setText(text) @@ -77,14 +79,19 @@ class AddRepoWindow(AddRepoBase, AddRepoUI): self.result = result self.accept() else: - self._set_status('Unable to add your repository.') + self._set_status(self.tr('Unable to add your repository.')) def init_encryption(self): - self.encryptionComboBox.addItem('Repokey-Blake2 (Recommended, key stored in repository)', 'repokey-blake2') - self.encryptionComboBox.addItem('Repokey', 'repokey') - self.encryptionComboBox.addItem('Keyfile-Blake2 (Key stored in home directory)', 'keyfile-blake2') - self.encryptionComboBox.addItem('Keyfile', 'keyfile') - self.encryptionComboBox.addItem('None (not recommended', 'none') + self.encryptionComboBox.addItem(self.tr('Repokey-Blake2 (Recommended, key stored in repository)'), + 'repokey-blake2') + self.encryptionComboBox.addItem(self.tr('Repokey'), + 'repokey') + self.encryptionComboBox.addItem(self.tr('Keyfile-Blake2 (Key stored in home directory)'), + 'keyfile-blake2') + self.encryptionComboBox.addItem(self.tr('Keyfile'), + 'keyfile') + self.encryptionComboBox.addItem(self.tr('None (not recommended)'), + 'none') def init_ssh_key(self): keys = get_private_keys() @@ -94,13 +101,13 @@ class AddRepoWindow(AddRepoBase, AddRepoUI): def validate(self): """Pre-flight check for valid input and borg binary.""" if self.is_remote_repo and not re.match(r'.+:.+', self.values['repo_url']): - self._set_status('Please enter a valid repo URL or select a local path.') + self._set_status(self.tr('Please enter a valid repo URL or select a local path.')) return False if self.__class__ == AddRepoWindow: if self.values['encryption'] != 'none': if len(self.values['password']) < 8: - self._set_status('Please use a longer password.') + self._set_status(self.tr('Please use a longer password.')) return False return True @@ -111,7 +118,7 @@ class ExistingRepoWindow(AddRepoWindow): super().__init__() self.encryptionComboBox.hide() self.encryptionLabel.hide() - self.title.setText('Connect to existing Repository') + self.title.setText(self.tr('Connect to existing Repository')) def run(self): if self.validate(): @@ -124,4 +131,4 @@ class ExistingRepoWindow(AddRepoWindow): self.thread = thread # Needs to be connected to self for tests to work. self.thread.start() else: - self._set_status(params['message']) + self._set_status(translate(params['message'])) diff --git a/src/vorta/views/repo_tab.py b/src/vorta/views/repo_tab.py index 707b9d8a..4ccbc0b5 100644 --- a/src/vorta/views/repo_tab.py +++ b/src/vorta/views/repo_tab.py @@ -21,8 +21,8 @@ class RepoTab(RepoBase, RepoUI, BackupProfileMixin): # Populate dropdowns self.repoSelector.model().item(0).setEnabled(False) - self.repoSelector.addItem('+ Initialize New Repository', 'new') - self.repoSelector.addItem('+ Add Existing Repository', 'existing') + self.repoSelector.addItem(self.tr('+ Initialize New Repository'), 'new') + self.repoSelector.addItem(self.tr('+ Add Existing Repository'), 'existing') self.repoSelector.insertSeparator(3) for repo in RepoModel.select(): self.repoSelector.addItem(repo.url, repo.id) @@ -30,10 +30,10 @@ class RepoTab(RepoBase, RepoUI, BackupProfileMixin): self.repoSelector.currentIndexChanged.connect(self.repo_select_action) self.repoRemoveToolbutton.clicked.connect(self.repo_unlink_action) - self.repoCompression.addItem('LZ4 (default)', 'lz4') - self.repoCompression.addItem('Zstandard (medium)', 'zstd') - self.repoCompression.addItem('LZMA (high)', 'lzma,6') - self.repoCompression.addItem('No Compression', 'none') + self.repoCompression.addItem(self.tr('LZ4 (default)'), 'lz4') + self.repoCompression.addItem(self.tr('Zstandard (medium)'), 'zstd') + self.repoCompression.addItem(self.tr('LZMA (high)'), 'lzma,6') + self.repoCompression.addItem(self.tr('No Compression'), 'none') self.repoCompression.currentIndexChanged.connect(self.compression_select_action) self.init_ssh() @@ -71,8 +71,8 @@ class RepoTab(RepoBase, RepoUI, BackupProfileMixin): def init_ssh(self): keys = get_private_keys() self.sshComboBox.clear() - self.sshComboBox.addItem('Automatically choose SSH Key (default)', None) - self.sshComboBox.addItem('Create New Key', 'new') + self.sshComboBox.addItem(self.tr('Automatically choose SSH Key (default)'), None) + self.sshComboBox.addItem(self.tr('Create New Key'), 'new') for key in keys: self.sshComboBox.addItem(f'{key["filename"]} ({key["format"]})', key['filename']) @@ -104,15 +104,15 @@ class RepoTab(RepoBase, RepoUI, BackupProfileMixin): clipboard = QApplication.clipboard() clipboard.setText(pub_key) - msg.setText("Public Key Copied to Clipboard") - msg.setInformativeText( + msg.setText(self.tr("Public Key Copied to Clipboard")) + msg.setInformativeText(self.tr( "The selected public SSH key was copied to the clipboard. " - "Use it to set up remote repo permissions.") + "Use it to set up remote repo permissions.")) else: - msg.setText("Couldn't find public key.") + msg.setText(self.tr("Couldn't find public key.")) else: - msg.setText("Select a public key from the dropdown first.") + msg.setText(self.tr("Select a public key from the dropdown first.")) msg.exec_() def compression_select_action(self, index): @@ -166,11 +166,11 @@ class RepoTab(RepoBase, RepoUI, BackupProfileMixin): ArchiveModel.delete().where(ArchiveModel.repo_id == repo.id).execute() profile.repo = None profile.save() - repo.delete_instance(recursive=True) # This also deletes snapshots. + repo.delete_instance(recursive=True) # This also deletes archives. self.repoSelector.setCurrentIndex(0) self.repoSelector.removeItem(selected_repo_index) - msg.setText('Repository was Unlinked') - msg.setInformativeText('You can always connect it again later.') + msg.setText(self.tr('Repository was Unlinked')) + msg.setInformativeText(self.tr('You can always connect it again later.')) msg.exec_() self.repo_changed.emit() diff --git a/src/vorta/views/source_tab.py b/src/vorta/views/source_tab.py index 45b0bee6..7ad4c795 100644 --- a/src/vorta/views/source_tab.py +++ b/src/vorta/views/source_tab.py @@ -43,8 +43,8 @@ class SourceTab(SourceBase, SourceUI, BackupProfileMixin): self.sourceFilesWidget.addItem(dir[0]) new_source.save() - item = "directory" if want_folder else "file" - dialog = choose_file_dialog(self, "Choose %s to back up" % item, want_folder=want_folder) + msg = self.tr("Choose directory to back up") if want_folder else self.tr("Choose file to back up") + dialog = choose_file_dialog(self, msg, want_folder=want_folder) dialog.open(receive) def source_remove(self): diff --git a/src/vorta/views/ssh_dialog.py b/src/vorta/views/ssh_dialog.py index d39839e3..880f0184 100644 --- a/src/vorta/views/ssh_dialog.py +++ b/src/vorta/views/ssh_dialog.py @@ -31,9 +31,9 @@ class SSHAddWindow(SSHAddBase, SSHAddUI): self.init_length() def init_format(self): - self.formatSelect.addItem('ED25519 (Recommended)', 'ed25519') - self.formatSelect.addItem('RSA (Legacy)', 'rsa') - self.formatSelect.addItem('ECDSA', 'ecdsa') + self.formatSelect.addItem(self.tr('ED25519 (Recommended)'), 'ed25519') + self.formatSelect.addItem(self.tr('RSA (Legacy)'), 'rsa') + self.formatSelect.addItem(self.tr('ECDSA'), 'ecdsa') self.outputFileTextBox.setText('~/.ssh/id_ed25519') self.formatSelect.currentIndexChanged.connect(self.format_select_change) @@ -42,8 +42,8 @@ class SSHAddWindow(SSHAddBase, SSHAddUI): self.outputFileTextBox.setText(new_output) def init_length(self): - self.lengthSelect.addItem('High (Recommended)', ('4096', '521')) - self.lengthSelect.addItem('Medium', ('2048', '384')) + self.lengthSelect.addItem(self.tr('High (Recommended)'), ('4096', '521')) + self.lengthSelect.addItem(self.tr('Medium'), ('2048', '384')) def generate_key(self): format = self.formatSelect.currentData() @@ -56,7 +56,7 @@ class SSHAddWindow(SSHAddBase, SSHAddUI): output_path = os.path.expanduser(self.outputFileTextBox.text()) if os.path.isfile(output_path): - self.errors.setText('Key file already exists. Not overwriting.') + self.errors.setText(self.tr('Key file already exists. Not overwriting.')) else: self.sshproc = QProcess(self) self.sshproc.finished.connect(self.generate_key_result) @@ -68,9 +68,9 @@ class SSHAddWindow(SSHAddBase, SSHAddUI): pub_key = open(output_path + '.pub').read().strip() clipboard = QApplication.clipboard() clipboard.setText(pub_key) - self.errors.setText(f'New key was copied to clipboard and written to {output_path}.') + self.errors.setText(self.tr('New key was copied to clipboard and written to %s.') % output_path) else: - self.errors.setText('Error during key generation.') + self.errors.setText(self.tr('Error during key generation.')) def get_values(self): return { diff --git a/tests/borg_json_output/prune_stdout.json b/tests/borg_json_output/prune_stdout.json index e69de29b..6e2ba867 100644 --- a/tests/borg_json_output/prune_stdout.json +++ b/tests/borg_json_output/prune_stdout.json @@ -0,0 +1,3 @@ +{ + "archives": [] +} diff --git a/tests/test_archives.py b/tests/test_archives.py index f0023a00..fbbc73d9 100644 --- a/tests/test_archives.py +++ b/tests/test_archives.py @@ -39,9 +39,9 @@ def test_repo_list(app, qtbot, mocker, borg_json_output): popen_result = mocker.MagicMock(stdout=stdout, stderr=stderr, returncode=0) mocker.patch.object(vorta.borg.borg_thread, 'Popen', return_value=popen_result) - qtbot.waitUntil(lambda: main.createProgressText.text() == 'Refreshing snapshots done.', timeout=3000) + qtbot.waitUntil(lambda: main.createProgressText.text() == 'Refreshing archives done.', timeout=3000) assert ArchiveModel.select().count() == 6 - assert main.createProgressText.text() == 'Refreshing snapshots done.' + assert main.createProgressText.text() == 'Refreshing archives done.' assert tab.checkButton.isEnabled() @@ -56,7 +56,7 @@ def test_repo_prune(app, qtbot, mocker, borg_json_output): qtbot.mouseClick(tab.pruneButton, QtCore.Qt.LeftButton) - qtbot.waitUntil(lambda: main.createProgressText.text().startswith('Refreshing snapshots'), timeout=5000) + qtbot.waitUntil(lambda: main.createProgressText.text().startswith('Refreshing archives done.'), timeout=5000) def test_check(app, mocker, borg_json_output, qtbot):