mirror of
https://github.com/borgbase/vorta
synced 2025-03-04 02:38:02 +00:00
i18n infrastructure and policy (#134)
- Adds support for translating Python strings and .UI files with Qt's `QTranslator` class. - Integrates the Transifex cloud translation service for managing translations. - Adds translation policy and guidelines to `CONTRIBUTING.md`
This commit is contained in:
parent
5ef40693b0
commit
e15675599e
36 changed files with 346 additions and 148 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -15,3 +15,4 @@ vorta.egg-info
|
|||
.vagrant
|
||||
*.log
|
||||
htmlcov
|
||||
*.qm
|
||||
|
|
10
.tx/config
Normal file
10
.tx/config
Normal file
|
@ -0,0 +1,10 @@
|
|||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[vorta.vorta]
|
||||
file_filter = src/vorta/i18n/ts/vorta.<lang>.ts
|
||||
minimum_perc = 0
|
||||
source_file = src/vorta/i18n/ts/vorta.en_US.ts
|
||||
source_lang = en_US
|
||||
type = QT
|
||||
|
|
@ -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
|
||||
|
|
28
Makefile
28
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}'
|
||||
|
|
6
requirements.d/Brewfile
Normal file
6
requirements.d/Brewfile
Normal file
|
@ -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'
|
|
@ -1,3 +1,4 @@
|
|||
transifex-client
|
||||
pytest
|
||||
pytest-qt
|
||||
pytest-mock
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>532</width>
|
||||
<height>289</height>
|
||||
<height>293</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="sizePolicy">
|
||||
|
@ -56,7 +56,7 @@
|
|||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string><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></string>
|
||||
<string><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></string>
|
||||
</property>
|
||||
<property name="wordWrap">
|
||||
<bool>true</bool>
|
||||
|
|
|
@ -244,7 +244,7 @@ font-weight: bold;
|
|||
<item>
|
||||
<widget class="QCheckBox" name="pruneCheckBox">
|
||||
<property name="text">
|
||||
<string>Prune old Snapshots after each backup.</string>
|
||||
<string>Prune old Archives after each backup.</string>
|
||||
</property>
|
||||
<property name="tristate">
|
||||
<bool>false</bool>
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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']
|
||||
|
|
40
src/vorta/i18n/__init__.py
Normal file
40
src/vorta/i18n/__init__.py
Normal file
|
@ -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
|
0
src/vorta/i18n/qm/.gitkeep
Normal file
0
src/vorta/i18n/qm/.gitkeep
Normal file
0
src/vorta/i18n/ts/.gitkeep
Normal file
0
src/vorta/i18n/ts/.gitkeep
Normal file
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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'))
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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']))
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"archives": []
|
||||
}
|
|
@ -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):
|
||||
|
|
Loading…
Add table
Reference in a new issue