Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/translations.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: Translations

on: [push, pull_request]

jobs:
translations:
runs-on: ubuntu-latest
name: Check GUI translations
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
# PySide6 provides both pyside6-lupdate (string extraction) and QtCore
# (used to verify the compiled .qm catalogs).
- name: Install PySide6
run: pip install "PySide6>=6.5.0"
# Fails if any tr() string is missing from a .ts, left untranslated, or if
# a committed .qm is out of sync with its .ts.
- name: Check translation catalogs are complete and in sync
run: python scripts/check_translations.py
11 changes: 11 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,14 @@ repos:
args: [--fix]
# Run the formatter.
- id: ruff-format
# Recompile the GUI .qm catalogs and warn (without modifying the .ts) if the
# code has strings the .ts is missing. Needs the Qt tools (e.g.
# `pip install PySide6`); if absent it warns and skips.
- repo: local
hooks:
- id: compile-translations
name: Compile GUI translation catalogs (.qm) and check for missing strings
entry: python scripts/compile_translations.py
language: system
pass_filenames: false
files: ^moodle_dl/gui/.*\.py$|^moodle_dl/gui/i18n/.*\.ts$
2 changes: 2 additions & 0 deletions moodle_dl/gui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from PySide6.QtWidgets import QApplication

from moodle_dl.gui.i18n import install_translators
from moodle_dl.gui.main_window import MainWindow
from moodle_dl.version import __version__

Expand Down Expand Up @@ -61,6 +62,7 @@ def create_app(opts):
"""Create the QApplication and show the main window."""
register_moodledl_scheme()
app = QApplication(sys.argv)
install_translators(app)
app.setApplicationName('Moodle-DL')
app.setApplicationVersion(__version__)
app.setOrganizationName('Moodle-DL')
Expand Down
34 changes: 17 additions & 17 deletions moodle_dl/gui/dialogs/course_options_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def __init__(self, config: ConfigHelper, opts, course_id: int, course_name: str,
self._sections_worker = None
self._section_checkboxes = {}

self.setWindowTitle(f'Options for: {course_name}')
self.setWindowTitle(self.tr('Options for: {}').format(course_name))
self.setMinimumWidth(450)
self.setMinimumHeight(400)

Expand All @@ -45,26 +45,26 @@ def _setup_ui(self) -> None:

self.custom_name_input = QLineEdit()
self.custom_name_input.setPlaceholderText(self.course_name)
self.custom_name_input.setToolTip('Override the course folder name. Leave empty to use the default.')
form.addRow('Custom Name:', self.custom_name_input)
self.custom_name_input.setToolTip(self.tr('Override the course folder name. Leave empty to use the default.'))
form.addRow(self.tr('Custom Name:'), self.custom_name_input)

self.cb_create_dir = QCheckBox('Create Directory Structure')
self.cb_create_dir = QCheckBox(self.tr('Create Directory Structure'))
self.cb_create_dir.setChecked(True)
self.cb_create_dir.setToolTip('Create subdirectories matching the Moodle course section structure.')
self.cb_create_dir.setToolTip(self.tr('Create subdirectories matching the Moodle course section structure.'))
form.addRow(self.cb_create_dir)

layout.addLayout(form)

# Section Exclusion
sections_group = QGroupBox('Section Exclusion')
sections_group = QGroupBox(self.tr('Section Exclusion'))
sections_layout = QVBoxLayout()

sections_hint = QLabel('Uncheck sections to exclude them from downloads.')
sections_hint = QLabel(self.tr('Uncheck sections to exclude them from downloads.'))
sections_hint.setStyleSheet('color: #666; font-style: italic;')
sections_layout.addWidget(sections_hint)

self.load_sections_btn = QPushButton('Load Sections')
self.load_sections_btn.setToolTip('Fetch available sections from Moodle.')
self.load_sections_btn = QPushButton(self.tr('Load Sections'))
self.load_sections_btn.setToolTip(self.tr('Fetch available sections from Moodle.'))
self.load_sections_btn.clicked.connect(self._on_fetch_sections)
sections_layout.addWidget(self.load_sections_btn)

Expand Down Expand Up @@ -97,9 +97,9 @@ def _load_options(self) -> None:
def _on_fetch_sections(self) -> None:
"""Fetch sections from Moodle for this course."""
self.load_sections_btn.setEnabled(False)
self.load_sections_btn.setText('Fetching\u2026')
self.load_sections_btn.setText(self.tr('Fetching\u2026'))
self.setCursor(QCursor(Qt.CursorShape.BusyCursor))
set_status_text(self.sections_status, 'Fetching sections\u2026', 'info')
set_status_text(self.sections_status, self.tr('Fetching sections\u2026'), 'info')

self._sections_worker = FetchSectionsWorker(self.config, self.opts, self.course_id)
self._sections_worker.sections_fetched.connect(self._on_sections_fetched)
Expand All @@ -109,9 +109,9 @@ def _on_fetch_sections(self) -> None:
def _on_sections_fetched(self, sections: list) -> None:
"""Populate section checkboxes."""
self.load_sections_btn.setEnabled(True)
self.load_sections_btn.setText('Load Sections')
self.load_sections_btn.setText(self.tr('Load Sections'))
self.unsetCursor()
set_status_text(self.sections_status, f'Found {len(sections)} sections.', 'success')
set_status_text(self.sections_status, self.tr('Found {} sections.').format(len(sections)), 'success')

# Clear existing checkboxes
for cb in self._section_checkboxes.values():
Expand All @@ -126,17 +126,17 @@ def _on_sections_fetched(self, sections: list) -> None:

for section in sections:
section_id = section['id']
section_name = section.get('name', f'Section {section_id}')
cb = QCheckBox(f'{section_name} (ID: {section_id})')
section_name = section.get('name', self.tr('Section {}').format(section_id))
cb = QCheckBox(self.tr('{} (ID: {})').format(section_name, section_id))
cb.setChecked(section_id not in excluded)
self.sections_list_layout.addWidget(cb)
self._section_checkboxes[section_id] = cb

def _on_sections_error(self, error_msg: str) -> None:
self.load_sections_btn.setEnabled(True)
self.load_sections_btn.setText('Load Sections')
self.load_sections_btn.setText(self.tr('Load Sections'))
self.unsetCursor()
set_status_text(self.sections_status, f'Error: {error_msg}', 'error')
set_status_text(self.sections_status, self.tr('Error: {}').format(error_msg), 'error')

def _on_save(self) -> None:
"""Save per-course options to config."""
Expand Down
35 changes: 20 additions & 15 deletions moodle_dl/gui/dialogs/database_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ def __init__(self, config: ConfigHelper, opts, parent=None) -> None:
self.opts = opts
self._worker = None

self.setWindowTitle('Missing Files')
self.setWindowTitle(self.tr('Missing Files'))
self.setMinimumSize(700, 500)

self._setup_ui()
Expand All @@ -35,8 +35,10 @@ def _setup_ui(self) -> None:
layout = QVBoxLayout(self)

hint = QLabel(
'These files are tracked in the database but no longer exist on disk. '
'Remove entries here to re-download them on the next scan.'
self.tr(
'These files are tracked in the database but no longer exist on disk. '
'Remove entries here to re-download them on the next scan.'
)
)
hint.setWordWrap(True)
layout.addWidget(hint)
Expand All @@ -45,25 +47,25 @@ def _setup_ui(self) -> None:
layout.addWidget(self.status_label)

self.tree = QTreeWidget()
self.tree.setHeaderLabels(['Name', 'Section', 'Path'])
self.tree.setHeaderLabels([self.tr('Name'), self.tr('Section'), self.tr('Path')])
self.tree.setSelectionMode(QTreeWidget.SelectionMode.ExtendedSelection)
self.tree.setAlternatingRowColors(True)
layout.addWidget(self.tree, 1)

btn_row = QHBoxLayout()

self.refresh_btn = QPushButton('Refresh')
self.refresh_btn = QPushButton(self.tr('Refresh'))
self.refresh_btn.clicked.connect(self._load_files)
btn_row.addWidget(self.refresh_btn)

self.delete_btn = QPushButton('Delete Selected from DB')
self.delete_btn = QPushButton(self.tr('Delete Selected from DB'))
self.delete_btn.setEnabled(False)
self.delete_btn.clicked.connect(self._on_delete_selected)
btn_row.addWidget(self.delete_btn)

btn_row.addStretch()

close_btn = QPushButton('Close')
close_btn = QPushButton(self.tr('Close'))
close_btn.clicked.connect(self.accept)
btn_row.addWidget(close_btn)

Expand All @@ -77,7 +79,7 @@ def _load_files(self) -> None:
self.delete_btn.setEnabled(False)
self.refresh_btn.setEnabled(False)
self.setCursor(QCursor(Qt.CursorShape.BusyCursor))
set_status_text(self.status_label, 'Loading stored files\u2026', 'info')
set_status_text(self.status_label, self.tr('Loading stored files\u2026'), 'info')

self._worker = FetchStoredFilesWorker(self.config, self.opts)
self._worker.files_fetched.connect(self._on_files_fetched)
Expand Down Expand Up @@ -108,17 +110,17 @@ def _on_files_fetched(self, courses: list) -> None:
course_item.setExpanded(True)

if total == 0:
set_status_text(self.status_label, 'No missing files found in the database.', 'success')
set_status_text(self.status_label, self.tr('No missing files found in the database.'), 'success')
else:
set_status_text(self.status_label, f'{total} missing file(s) found.', 'info')
set_status_text(self.status_label, self.tr('{} missing file(s) found.').format(total), 'info')

self.tree.resizeColumnToContents(0)
self.tree.resizeColumnToContents(1)

def _on_error(self, error_msg: str) -> None:
self.refresh_btn.setEnabled(True)
self.unsetCursor()
set_status_text(self.status_label, f'Error: {error_msg}', 'error')
set_status_text(self.status_label, self.tr('Error: {}').format(error_msg), 'error')

def _on_selection_changed(self) -> None:
selected = [item for item in self.tree.selectedItems() if item.data(0, Qt.ItemDataRole.UserRole) is not None]
Expand All @@ -132,9 +134,10 @@ def _on_delete_selected(self) -> None:

reply = QMessageBox.question(
self,
'Delete from Database',
f'Delete {len(selected)} file entry(ies) from the database?\n'
'These files will be re-downloaded on the next scan.',
self.tr('Delete from Database'),
self.tr(
'Delete {} file entry(ies) from the database?\nThese files will be re-downloaded on the next scan.'
).format(len(selected)),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
Expand All @@ -146,5 +149,7 @@ def _on_delete_selected(self) -> None:
files = [item.data(0, Qt.ItemDataRole.UserRole) for item in selected]
database = StateRecorder(self.config, self.opts)
database.batch_delete_files_from_db(files)
set_status_text(self.status_label, f'Deleted {len(files)} entry(ies). Refreshing\u2026', 'success')
set_status_text(
self.status_label, self.tr('Deleted {} entry(ies). Refreshing\u2026').format(len(files)), 'success'
)
self._load_files()
34 changes: 20 additions & 14 deletions moodle_dl/gui/dialogs/old_files_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def __init__(self, config: ConfigHelper, opts, parent=None) -> None:
self.opts = opts
self._worker = None

self.setWindowTitle('Outdated Copies')
self.setWindowTitle(self.tr('Outdated Copies'))
self.setMinimumSize(700, 500)

self._setup_ui()
Expand All @@ -38,8 +38,10 @@ def _setup_ui(self) -> None:
layout = QVBoxLayout(self)

hint = QLabel(
'These are older versions of files that have been replaced by newer ones but are still on disk. '
'Select entries to delete them and free up disk space.'
self.tr(
'These are older versions of files that have been replaced by newer ones but are still on disk. '
'Select entries to delete them and free up disk space.'
)
)
hint.setWordWrap(True)
layout.addWidget(hint)
Expand All @@ -48,25 +50,25 @@ def _setup_ui(self) -> None:
layout.addWidget(self.status_label)

self.tree = QTreeWidget()
self.tree.setHeaderLabels(['Name', 'Section', 'Path'])
self.tree.setHeaderLabels([self.tr('Name'), self.tr('Section'), self.tr('Path')])
self.tree.setSelectionMode(QTreeWidget.SelectionMode.ExtendedSelection)
self.tree.setAlternatingRowColors(True)
layout.addWidget(self.tree, 1)

btn_row = QHBoxLayout()

self.refresh_btn = QPushButton('Refresh')
self.refresh_btn = QPushButton(self.tr('Refresh'))
self.refresh_btn.clicked.connect(self._load_files)
btn_row.addWidget(self.refresh_btn)

self.delete_btn = QPushButton('Delete Selected')
self.delete_btn = QPushButton(self.tr('Delete Selected'))
self.delete_btn.setEnabled(False)
self.delete_btn.clicked.connect(self._on_delete_selected)
btn_row.addWidget(self.delete_btn)

btn_row.addStretch()

close_btn = QPushButton('Close')
close_btn = QPushButton(self.tr('Close'))
close_btn.clicked.connect(self.accept)
btn_row.addWidget(close_btn)

Expand All @@ -80,7 +82,7 @@ def _load_files(self) -> None:
self.delete_btn.setEnabled(False)
self.refresh_btn.setEnabled(False)
self.setCursor(QCursor(Qt.CursorShape.BusyCursor))
set_status_text(self.status_label, 'Loading old files\u2026', 'info')
set_status_text(self.status_label, self.tr('Loading old files\u2026'), 'info')

self._worker = FetchOldFilesWorker(self.config, self.opts)
self._worker.files_fetched.connect(self._on_files_fetched)
Expand Down Expand Up @@ -111,17 +113,17 @@ def _on_files_fetched(self, courses: list) -> None:
course_item.setExpanded(True)

if total == 0:
set_status_text(self.status_label, 'No old file copies found.', 'success')
set_status_text(self.status_label, self.tr('No old file copies found.'), 'success')
else:
set_status_text(self.status_label, f'{total} old file(s) found.', 'info')
set_status_text(self.status_label, self.tr('{} old file(s) found.').format(total), 'info')

self.tree.resizeColumnToContents(0)
self.tree.resizeColumnToContents(1)

def _on_error(self, error_msg: str) -> None:
self.refresh_btn.setEnabled(True)
self.unsetCursor()
set_status_text(self.status_label, f'Error: {error_msg}', 'error')
set_status_text(self.status_label, self.tr('Error: {}').format(error_msg), 'error')

def _on_selection_changed(self) -> None:
selected = [item for item in self.tree.selectedItems() if item.data(0, Qt.ItemDataRole.UserRole) is not None]
Expand All @@ -135,8 +137,10 @@ def _on_delete_selected(self) -> None:

reply = QMessageBox.question(
self,
'Delete Old Files',
f'Delete {len(selected)} old file(s) from disk and database?\nThis action cannot be undone.',
self.tr('Delete Old Files'),
self.tr('Delete {} old file(s) from disk and database?\nThis action cannot be undone.').format(
len(selected)
),
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
QMessageBox.StandardButton.No,
)
Expand All @@ -162,7 +166,9 @@ def _on_delete_selected(self) -> None:
database.batch_delete_files_from_db(files)
set_status_text(
self.status_label,
f'Deleted {deleted_count} file(s) from disk and {len(files)} DB entry(ies). Refreshing\u2026',
self.tr('Deleted {} file(s) from disk and {} DB entry(ies). Refreshing\u2026').format(
deleted_count, len(files)
),
'success',
)
self._load_files()
Loading
Loading