From f578d562e6030a3920ec83f9c85e8ec0da460062 Mon Sep 17 00:00:00 2001 From: Miguel Jacq Date: Wed, 12 Nov 2025 13:58:58 +1100 Subject: [PATCH] Add translation capability, offer English and French as options --- CHANGELOG.md | 4 ++ bouquin/db.py | 19 ++++-- bouquin/find_bar.py | 12 ++-- bouquin/history_dialog.py | 22 ++++--- bouquin/key_prompt.py | 8 ++- bouquin/locales/en.json | 113 ++++++++++++++++++++++++++++++++++++ bouquin/locales/fr.json | 114 ++++++++++++++++++++++++++++++++++++ bouquin/lock_overlay.py | 5 +- bouquin/main.py | 2 + bouquin/main_window.py | 108 ++++++++++++++++++---------------- bouquin/save_dialog.py | 10 ++-- bouquin/search.py | 4 +- bouquin/settings.py | 9 ++- bouquin/settings_dialog.py | 116 ++++++++++++++++++++++++------------- bouquin/strings.py | 42 ++++++++++++++ bouquin/toolbar.py | 36 ++++++------ pyproject.toml | 4 +- 17 files changed, 490 insertions(+), 138 deletions(-) create mode 100644 bouquin/locales/en.json create mode 100644 bouquin/locales/fr.json create mode 100644 bouquin/strings.py diff --git a/CHANGELOG.md b/CHANGELOG.md index 1012b17..6ac1c2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +# 0.2.1.8 + + * Translate all strings, add French, add locale choice in settings + # 0.2.1.7 * Fix being able to set bold, italic and strikethrough at the same time. diff --git a/bouquin/db.py b/bouquin/db.py index af75247..58143b6 100644 --- a/bouquin/db.py +++ b/bouquin/db.py @@ -9,6 +9,8 @@ from pathlib import Path from sqlcipher3 import dbapi2 as sqlite from typing import List, Sequence, Tuple +from . import strings + Entry = Tuple[str, str] @@ -19,6 +21,7 @@ class DBConfig: idle_minutes: int = 15 # 0 = never lock theme: str = "system" move_todos: bool = False + locale: str = "en" class DBManager: @@ -62,8 +65,12 @@ class DBManager: # Not OK: rows of problems returned details = "; ".join(str(r[0]) for r in rows if r and r[0] is not None) raise sqlite.IntegrityError( - "SQLCipher integrity check failed" - + (f": {details}" if details else f" ({len(rows)} issue(s) reported)") + strings._("db_sqlcipher_integrity_check_failed") + + ( + f": {details}" + if details + else f" ({len(rows)} {strings._('db_issues_reported')})" + ) ) def _ensure_schema(self) -> None: @@ -115,7 +122,7 @@ class DBManager: self.conn = None self.cfg.key = new_key if not self.connect(): - raise sqlite.Error("Re-open failed after rekey") + raise sqlite.Error(strings._("db_reopen_failed_after_rekey")) def get_entry(self, date_iso: str) -> str: """ @@ -251,7 +258,9 @@ class DBManager: "SELECT date FROM versions WHERE id=?;", (version_id,) ).fetchone() if row is None or row["date"] != date_iso: - raise ValueError("version_id does not belong to the given date") + raise ValueError( + strings._("db_version_id_does_not_belong_to_the_given_date") + ) with self.conn: cur.execute( @@ -375,7 +384,7 @@ class DBManager: cur = self.conn.cursor() cur.execute("VACUUM") except Exception as e: - print(f"Error: {e}") + print(f"{strings._('error')}: {e}") def close(self) -> None: if self.conn is not None: diff --git a/bouquin/find_bar.py b/bouquin/find_bar.py index 8136c06..5332beb 100644 --- a/bouquin/find_bar.py +++ b/bouquin/find_bar.py @@ -17,6 +17,8 @@ from PySide6.QtWidgets import ( QTextEdit, ) +from . import strings + class FindBar(QWidget): """Widget for finding text in the Editor""" @@ -41,17 +43,17 @@ class FindBar(QWidget): layout = QHBoxLayout(self) layout.setContentsMargins(6, 0, 6, 0) - layout.addWidget(QLabel("Find:")) + layout.addWidget(QLabel(strings._("find"))) self.edit = QLineEdit(self) - self.edit.setPlaceholderText("Type to search") + self.edit.setPlaceholderText(strings._("find_bar_type_to_search")) layout.addWidget(self.edit) - self.case = QCheckBox("Match case", self) + self.case = QCheckBox(strings._("find_bar_match_case"), self) layout.addWidget(self.case) - self.prevBtn = QPushButton("Prev", self) - self.nextBtn = QPushButton("Next", self) + self.prevBtn = QPushButton(strings._("previous"), self) + self.nextBtn = QPushButton(strings._("next"), self) self.closeBtn = QPushButton("✕", self) self.closeBtn.setFlat(True) layout.addWidget(self.prevBtn) diff --git a/bouquin/history_dialog.py b/bouquin/history_dialog.py index 1127852..5e2e9ab 100644 --- a/bouquin/history_dialog.py +++ b/bouquin/history_dialog.py @@ -15,6 +15,8 @@ from PySide6.QtWidgets import ( QTabWidget, ) +from . import strings + def _markdown_to_text(s: str) -> str: """Convert markdown to plain text for diff comparison.""" @@ -43,7 +45,9 @@ def _colored_unified_diff_html(old_md: str, new_md: str) -> str: """Return HTML with colored unified diff (+ green, - red, context gray).""" a = _markdown_to_text(old_md).splitlines() b = _markdown_to_text(new_md).splitlines() - ud = difflib.unified_diff(a, b, fromfile="current", tofile="selected", lineterm="") + ud = difflib.unified_diff( + a, b, fromfile=strings._("current"), tofile=strings._("selected"), lineterm="" + ) lines = [] for line in ud: if line.startswith("+") and not line.startswith("+++"): @@ -67,7 +71,7 @@ class HistoryDialog(QDialog): def __init__(self, db, date_iso: str, parent=None): super().__init__(parent) - self.setWindowTitle(f"History — {date_iso}") + self.setWindowTitle(f"{strings._('history')} — {date_iso}") self._db = db self._date = date_iso self._versions = [] # list[dict] from DB @@ -88,8 +92,8 @@ class HistoryDialog(QDialog): self.preview.setOpenExternalLinks(True) self.diff = QTextBrowser() self.diff.setOpenExternalLinks(False) - self.tabs.addTab(self.preview, "Preview") - self.tabs.addTab(self.diff, "Diff") + self.tabs.addTab(self.preview, strings._("history_dialog_preview")) + self.tabs.addTab(self.diff, strings._("history_dialog_diff")) self.tabs.setMinimumSize(500, 650) top.addWidget(self.tabs, 2) @@ -98,9 +102,9 @@ class HistoryDialog(QDialog): # Buttons row = QHBoxLayout() row.addStretch(1) - self.btn_revert = QPushButton("Revert to Selected") + self.btn_revert = QPushButton(strings._("history_dialog_revert_to_selected")) self.btn_revert.clicked.connect(self._revert) - self.btn_close = QPushButton("Close") + self.btn_close = QPushButton(strings._("close")) self.btn_close.clicked.connect(self.reject) row.addWidget(self.btn_revert) row.addWidget(self.btn_close) @@ -126,7 +130,7 @@ class HistoryDialog(QDialog): if v.get("note"): label += f" · {v['note']}" if v["is_current"]: - label += " **(current)**" + label += " **(" + strings._("current") + ")**" it = QListWidgetItem(label) it.setData(Qt.UserRole, v["id"]) self.list.addItem(it) @@ -168,6 +172,8 @@ class HistoryDialog(QDialog): try: self._db.revert_to_version(self._date, version_id=sel_id) except Exception as e: - QMessageBox.critical(self, "Revert failed", str(e)) + QMessageBox.critical( + self, strings._("history_dialog_revert_failed"), str(e) + ) return self.accept() diff --git a/bouquin/key_prompt.py b/bouquin/key_prompt.py index bef0571..b29101c 100644 --- a/bouquin/key_prompt.py +++ b/bouquin/key_prompt.py @@ -9,13 +9,15 @@ from PySide6.QtWidgets import ( QDialogButtonBox, ) +from . import strings + class KeyPrompt(QDialog): def __init__( self, parent=None, - title: str = "Enter key", - message: str = "Enter key", + title: str = strings._("key_prompt_enter_key"), + message: str = strings._("key_prompt_enter_key"), ): """ Prompt the user for the key required to decrypt the database. @@ -30,7 +32,7 @@ class KeyPrompt(QDialog): self.edit = QLineEdit() self.edit.setEchoMode(QLineEdit.Password) v.addWidget(self.edit) - toggle = QPushButton("Show") + toggle = QPushButton(strings._("show")) toggle.setCheckable(True) toggle.toggled.connect( lambda c: self.edit.setEchoMode( diff --git a/bouquin/locales/en.json b/bouquin/locales/en.json new file mode 100644 index 0000000..f924559 --- /dev/null +++ b/bouquin/locales/en.json @@ -0,0 +1,113 @@ +{ + "db_sqlcipher_integrity_check_failed": "SQLCipher integrity check failed", + "db_issues_reported": "issue(s) reported", + "db_reopen_failed_after_rekey": "Re-open failed after rekey", + "db_version_id_does_not_belong_to_the_given_date": "version_id does not belong to the given date", + "db_key_incorrect": "The key is probably incorrect", + "db_database_error": "Database error", + "database_path": "Database path", + "database_maintenance": "Database maintenance", + "database_compact": "Compact the database", + "database_compact_explanation": "Compacting runs VACUUM on the database. This can help reduce its size.", + "database_compacted_successfully": "Database compacted successfully!", + "encryption": "Encryption", + "remember_key": "Remember key", + "change_encryption_key": "Change encryption key", + "enter_a_new_encryption_key": "Enter a new encryption key", + "reenter_the_new_key": "Re-enter the new key", + "key_mismatch": "Key mismatch", + "key_mismatch_explanation": "The two entries did not match.", + "empty_key": "Empty key", + "empty_key_explanation": "The key cannot be empty.", + "key_changed": "Key changed", + "key_changed_explanation": "The notebook was re-encrypted with the new key!", + "error": "Error", + "success": "Success", + "close": "Close", + "find": "Find", + "file": "File", + "locale": "Locale", + "locale_restart": "Please restart the application to load the new language.", + "settings": "Settings", + "theme": "Theme", + "system": "System", + "light": "Light", + "dark": "Dark", + "behaviour": "Behaviour", + "never": "Never", + "browse": "Browse", + "previous": "Previous", + "previous_day": "Previous day", + "next": "Next", + "next_day": "Next day", + "today": "Today", + "show": "Show", + "history": "History", + "view_history": "View History", + "export": "Export", + "export_accessible_flag": "&Export", + "export_entries": "Export entries", + "export_complete": "Export complete", + "export_failed": "Export failed", + "backup": "Backup", + "backup_complete": "Backup complete", + "backup_failed": "Backup failed", + "quit": "Quit", + "help": "Help", + "saved": "Saved", + "saved_to": "Saved to", + "documentation": "Documentation", + "couldnt_open": "Couldn't open", + "report_a_bug": "Report a bug", + "navigate": "Navigate", + "current": "current", + "selected": "selected", + "find_on_page": "Find on page", + "find_next": "Find next", + "find_previous": "Find previous", + "find_bar_type_to_search": "Type to search", + "find_bar_match_case": "Match case", + "history_dialog_preview": "Preview", + "history_dialog_diff": "Diff", + "history_dialog_revert_to_selected": "Revert to selected", + "history_dialog_revert_failed": "Revert failed", + "key_prompt_enter_key": "Enter key", + "lock_overlay_locked_due_to_inactivity": "Locked due to inactivity", + "lock_overlay_unlock": "Unlock", + "main_window_ready": "Ready", + "main_window_save_a_version": "Save a version", + "main_window_settings_accessible_flag": "Settin&gs", + "set_an_encryption_key": "Set an encryption key", + "set_an_encryption_key_explanation": "Bouquin encrypts your data.\n\nPlease create a strong passphrase to encrypt the notebook.\n\nYou can always change it later!", + "unlock_encrypted_notebook": "Unlock encrypted notebook", + "unlock_encrypted_notebook_explanation": "Enter your key to unlock the notebook", + "open_in_new_tab": "Open in new tab", + "autosave": "autosave", + "unchecked_checkbox_items_moved_to_next_day": "Unchecked checkbox items moved to next day", + "move_yesterdays_unchecked_todos_to_today_on_startup": "Move yesterday's unchecked TODOs to today on startup", + "insert_images": "Insert images", + "images": "Images", + "reopen_failed": "Re-open failed", + "unlock_failed": "Unlock failed", + "could_not_unlock_database_at_new_path": "Could not unlock database at new path.", + "unencrypted_export": "Unencrypted export", + "unencrypted_export_warning": "Exporting the database will be unencrypted!\nAre you sure you want to continue?\nIf you want an encrypted backup, choose Backup instead of Export.", + "unrecognised_extension": "Unrecognised extension!", + "backup_encrypted_notebook": "Backup encrypted notebook", + "enter_a_name_for_this_version": "Enter a name for this version", + "new_version_i_saved_at": "New version I saved at", + "save_key_warning": "If you don't want to be prompted for your encryption key, check this to remember it.\nWARNING: the key is saved to disk and could be recoverable if your disk is compromised.", + "lock_screen_when_idle": "Lock screen when idle", + "autolock_explanation": "Bouquin will automatically lock the notepad after this length of time, after which you'll need to re-enter the key to unlock it.'nSet to 0 (never) to never lock.", + "search_for_notes_here": "Search for notes here", + "toolbar_format": "Format", + "toolbar_bold": "Bold", + "toolbar_italic": "Italic", + "toolbar_strikethrough": "Strikethrough", + "toolbar_normal_paragraph_text": "Normal paragraph text", + "toolbar_bulleted_list": "Bulleted list", + "toolbar_numbered_list": "Numbered list", + "toolbar_code_block": "Code block", + "toolbar_heading": "Heading", + "toolbar_toggle_checkboxes": "Toggle checkboxes" +} diff --git a/bouquin/locales/fr.json b/bouquin/locales/fr.json new file mode 100644 index 0000000..fe55464 --- /dev/null +++ b/bouquin/locales/fr.json @@ -0,0 +1,114 @@ +{ + "db_sqlcipher_integrity_check_failed": "Échec de la vérification d'intégrité SQLCipher", + "db_issues_reported": "problème(s) signalé(s)", + "db_reopen_failed_after_rekey": "Échec de la réouverture après modification de la clé", + "db_version_id_does_not_belong_to_the_given_date": "version_id ne correspond pas à la date indiquée", + "db_key_incorrect": "La clé est peut-être incorrecte", + "db_database_error": "Erreur de base de données", + "database_path": "Chemin de la base de données", + "database_maintenance": "Maintenance de la base de données", + "database_compact": "Compacter la base de données", + "database_compact_explanation": "La compaction exécute VACUUM sur la base de données. Cela peut aider à réduire sa taille.", + "database_compacted_successfully": "Base de données compactée avec succès !", + "encryption": "Chiffrement", + "remember_key": "Se souvenir de la clé", + "change_encryption_key": "Changer la clé de chiffrement", + "enter_a_new_encryption_key": "Saisir une nouvelle clé de chiffrement", + "reenter_the_new_key": "Saisir de nouveau la nouvelle clé", + "key_mismatch": "Les clés ne correspondent pas", + "key_mismatch_explanation": "Les deux saisies ne correspondent pas.", + "empty_key": "Clé est vide", + "empty_key_explanation": "La clé ne peut pas être vide.", + "key_changed": "Clé modifiée", + "key_changed_explanation": "Le bouquin a été rechiffré avec la nouvelle clé !", + "error": "Erreur", + "success": "Succès", + "close": "Fermer", + "find": "Rechercher", + "file": "Fichier", + "locale": "Langue", + "locale_restart": "Veuillez redémarrer l’application pour appliquer la nouvelle langue.", + "settings": "Paramètres", + "theme": "Thème", + "system": "Système", + "light": "Clair", + "dark": "Sombre", + "behaviour": "Comportement", + "never": "Jamais", + "browse": "Parcourir", + "previous": "Précédent", + "previous_day": "Jour précédent", + "next": "Suivant", + "next_day": "Jour suivant", + "today": "Aujourd'hui", + "show": "Afficher", + "history": "Historique", + "view_history": "Afficher l'historique", + "export": "Exporter", + "export_accessible_flag": "E&xporter", + "export_entries": "Exporter les entrées", + "export_complete": "Exportation terminée", + "export_failed": "Échec de l’exportation", + "backup": "Sauvegarder", + "backup_complete": "Sauvegarde terminée", + "backup_failed": "Échec de la sauvegarde", + "quit": "Quitter", + "help": "Aide", + "saved": "Enregistré", + "saved_to": "Enregistré dans", + "documentation": "Documentation", + "couldnt_open": "Impossible d’ouvrir", + "report_a_bug": "Signaler un bug", + "navigate": "Naviguer", + "current": "actuel", + "selected": "sélectionné", + "find_on_page": "Rechercher dans la page", + "find_next": "Rechercher suivant", + "find_previous": "Rechercher précédent", + "find_bar_type_to_search": "Tapez pour rechercher", + "find_bar_match_case": "Respecter la casse", + "history_dialog_preview": "Aperçu", + "history_dialog_diff": "Différences", + "history_dialog_revert_to_selected": "Revenir à la sélection", + "history_dialog_revert_failed": "Échec de la restauration", + "key_prompt_enter_key": "Saisir la clé", + "lock_overlay_locked_due_to_inactivity": "Verrouillé pour cause d’inactivité", + "lock_overlay_unlock": "Déverrouiller", + "main_window_ready": "Prêt", + "main_window_save_a_version": "Enregistrer une version", + "main_window_settings_accessible_flag": "&Paramètres", + "set_an_encryption_key": "Définir une clé de chiffrement", + "set_an_encryption_key_explanation": "Bouquin chiffre vos données.\n\nVeuillez créer une phrase de passe robuste pour chiffrer le bouquin.\n\nVous pourrez toujours la modifier plus tard !", + "unlock_encrypted_notebook": "Déverrouiller le bouquin chiffré", + "unlock_encrypted_notebook_explanation": "Saisissez votre clé pour déverrouiller le bouquin", + "open_in_new_tab": "Ouvrir dans un nouvel onglet", + "autosave": "enregistrement automatique", + "unchecked_checkbox_items_moved_to_next_day": "Les cases non cochées ont été reportées au jour suivant", + "move_yesterdays_unchecked_todos_to_today_on_startup": "Au démarrage, déplacer les TODO non cochés d’hier vers aujourd’hui", + "insert_images": "Insérer des images", + "images": "Images", + "reopen_failed": "Échec de la réouverture", + "unlock_failed": "Échec du déverrouillage", + "could_not_unlock_database_at_new_path": "Impossible de déverrouiller la base de données au nouveau chemin.", + "unencrypted_export": "Export non chiffré", + "unencrypted_export_warning": "L’export de la base de données ne sera pas chiffré !\nÊtes-vous sûr de vouloir continuer ?'nSi vous voulez une sauvegarde chiffrée, choisissez Sauvegarde plutôt qu’Export.", + "unrecognised_extension": "Extension non reconnue !", + "backup_encrypted_notebook": "Sauvegarder le bouquin chiffré", + "enter_a_name_for_this_version": "Saisir un nom pour cette version", + "new_version_i_saved_at": "Nouvelle version que j’ai enregistrée à", + "save_key_warning": "Si vous ne voulez pas que l’on vous demande votre clé de chiffrement, cochez ceci pour la mémoriser.\nAVERTISSEMENT : la clé est enregistrée sur le disque et pourrait être récupérée si votre disque est compromis.", + "lock_screen_when_idle": "Verrouiller l’écran en cas d’inactivité", + "autolock_explanation": "Bouquin verrouillera automatiquement le bouquin après cette durée ; vous devrez alors ressaisir la clé pour le déverrouiller.\nMettre à 0 (jamais) pour ne jamais verrouiller.", + "search_for_notes_here": "Recherchez des notes", + "toolbar_format": "Format", + "toolbar_bold": "Gras", + "toolbar_italic": "Italique", + "toolbar_strikethrough": "Barré", + "toolbar_normal_paragraph_text": "Texte normale", + "toolbar_bulleted_list": "Liste à puces", + "toolbar_numbered_list": "Liste numérotée", + "toolbar_code_block": "Bloc de code", + "toolbar_heading": "Titre", + "toolbar_toggle_checkboxes": "Cocher/Décocher les cases" +} + diff --git a/bouquin/lock_overlay.py b/bouquin/lock_overlay.py index 5534b62..f40e7f5 100644 --- a/bouquin/lock_overlay.py +++ b/bouquin/lock_overlay.py @@ -3,6 +3,7 @@ from __future__ import annotations from PySide6.QtCore import Qt, QEvent from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton +from . import strings from .theme import ThemeManager @@ -22,11 +23,11 @@ class LockOverlay(QWidget): lay = QVBoxLayout(self) lay.addStretch(1) - msg = QLabel("Locked due to inactivity", self) + msg = QLabel(strings._("lock_overlay_locked_due_to_inactivity"), self) msg.setObjectName("lockLabel") msg.setAlignment(Qt.AlignCenter) - self._btn = QPushButton("Unlock", self) + self._btn = QPushButton(strings._("lock_overlay_unlock"), self) self._btn.setObjectName("unlockButton") self._btn.setFixedWidth(200) self._btn.setCursor(Qt.PointingHandCursor) diff --git a/bouquin/main.py b/bouquin/main.py index a481480..693917e 100644 --- a/bouquin/main.py +++ b/bouquin/main.py @@ -6,6 +6,7 @@ from PySide6.QtWidgets import QApplication from .settings import APP_NAME, APP_ORG, get_settings from .main_window import MainWindow from .theme import Theme, ThemeConfig, ThemeManager +from . import strings def main(): @@ -19,6 +20,7 @@ def main(): themes = ThemeManager(app, cfg) themes.apply(cfg.theme) + strings.load_strings(s.value("ui/locale", "en")) win = MainWindow(themes=themes) win.show() sys.exit(app.exec()) diff --git a/bouquin/main_window.py b/bouquin/main_window.py index 9c96c5b..f82f533 100644 --- a/bouquin/main_window.py +++ b/bouquin/main_window.py @@ -54,6 +54,7 @@ from .save_dialog import SaveDialog from .search import Search from .settings import APP_ORG, APP_NAME, load_db_config, save_db_config from .settings_dialog import SettingsDialog +from . import strings from .toolbar import ToolBar from .theme import ThemeManager @@ -169,7 +170,7 @@ class MainWindow(QMainWindow): ) # Status bar for feedback - self.statusBar().showMessage("Ready", 800) + self.statusBar().showMessage(strings._("main_window_ready"), 800) # Add findBar and add it to the statusBar # FindBar will get the current editor dynamically via a callable self.findBar = FindBar(lambda: self.editor, shortcut_parent=self, parent=self) @@ -179,84 +180,84 @@ class MainWindow(QMainWindow): # Menu bar (File) mb = self.menuBar() - file_menu = mb.addMenu("&File") - act_save = QAction("&Save a version", self) + file_menu = mb.addMenu("&" + strings._("file")) + act_save = QAction("&" + strings._("main_window_save_a_version"), self) act_save.setShortcut("Ctrl+S") act_save.triggered.connect(lambda: self._save_current(explicit=True)) file_menu.addAction(act_save) - act_history = QAction("History", self) + act_history = QAction("&" + strings._("history"), self) act_history.setShortcut("Ctrl+H") act_history.setShortcutContext(Qt.ApplicationShortcut) act_history.triggered.connect(self._open_history) file_menu.addAction(act_history) - act_settings = QAction("Settin&gs", self) + act_settings = QAction(strings._("main_window_settings_accessible_flag"), self) act_settings.setShortcut("Ctrl+G") act_settings.triggered.connect(self._open_settings) file_menu.addAction(act_settings) - act_export = QAction("&Export", self) + act_export = QAction(strings._("export_accessible_flag"), self) act_export.setShortcut("Ctrl+E") act_export.triggered.connect(self._export) file_menu.addAction(act_export) - act_backup = QAction("&Backup", self) + act_backup = QAction("&" + strings._("backup"), self) act_backup.setShortcut("Ctrl+Shift+B") act_backup.triggered.connect(self._backup) file_menu.addAction(act_backup) file_menu.addSeparator() - act_quit = QAction("&Quit", self) + act_quit = QAction("&" + strings._("quit"), self) act_quit.setShortcut("Ctrl+Q") act_quit.triggered.connect(self.close) file_menu.addAction(act_quit) # Navigate menu with next/previous/today - nav_menu = mb.addMenu("&Navigate") - act_prev = QAction("Previous Day", self) + nav_menu = mb.addMenu("&" + strings._("navigate")) + act_prev = QAction(strings._("previous_day"), self) act_prev.setShortcut("Ctrl+Shift+P") act_prev.setShortcutContext(Qt.ApplicationShortcut) act_prev.triggered.connect(lambda: self._adjust_day(-1)) nav_menu.addAction(act_prev) self.addAction(act_prev) - act_next = QAction("Next Day", self) + act_next = QAction(strings._("next_day"), self) act_next.setShortcut("Ctrl+Shift+N") act_next.setShortcutContext(Qt.ApplicationShortcut) act_next.triggered.connect(lambda: self._adjust_day(1)) nav_menu.addAction(act_next) self.addAction(act_next) - act_today = QAction("Today", self) + act_today = QAction(strings._("today"), self) act_today.setShortcut("Ctrl+Shift+T") act_today.setShortcutContext(Qt.ApplicationShortcut) act_today.triggered.connect(self._adjust_today) nav_menu.addAction(act_today) self.addAction(act_today) - act_find = QAction("Find on page", self) + act_find = QAction(strings._("find_on_page"), self) act_find.setShortcut(QKeySequence.Find) act_find.triggered.connect(self.findBar.show_bar) nav_menu.addAction(act_find) self.addAction(act_find) - act_find_next = QAction("Find Next", self) + act_find_next = QAction(strings._("find_next"), self) act_find_next.setShortcut(QKeySequence.FindNext) act_find_next.triggered.connect(self.findBar.find_next) nav_menu.addAction(act_find_next) self.addAction(act_find_next) - act_find_prev = QAction("Find Previous", self) + act_find_prev = QAction(strings._("find_previous"), self) act_find_prev.setShortcut(QKeySequence.FindPrevious) act_find_prev.triggered.connect(self.findBar.find_prev) nav_menu.addAction(act_find_prev) self.addAction(act_find_prev) # Help menu with drop-down - help_menu = mb.addMenu("&Help") - act_docs = QAction("Documentation", self) + help_menu = mb.addMenu("&" + strings._("help")) + act_docs = QAction(strings._("documentation"), self) act_docs.setShortcut("Ctrl+D") act_docs.setShortcutContext(Qt.ApplicationShortcut) act_docs.triggered.connect(self._open_docs) help_menu.addAction(act_docs) self.addAction(act_docs) - act_bugs = QAction("Report a bug", self) + act_bugs = QAction(strings._("report_a_bug"), self) act_bugs.setShortcut("Ctrl+R") act_bugs.setShortcutContext(Qt.ApplicationShortcut) act_bugs.triggered.connect(self._open_bugs) @@ -308,10 +309,10 @@ class MainWindow(QMainWindow): ok = self.db.connect() except Exception as e: if str(e) == "file is not a database": - error = "The key is probably incorrect." + error = strings._("db_key_incorrect") else: error = str(e) - QMessageBox.critical(self, "Database Error", error) + QMessageBox.critical(self, strings._("db_database_error"), error) return False return ok @@ -320,11 +321,11 @@ class MainWindow(QMainWindow): Prompt for the SQLCipher key. """ if first_time: - title = "Set an encryption key" - message = "Bouquin encrypts your data.\n\nPlease create a strong passphrase to encrypt the notebook.\n\nYou can always change it later!" + title = strings._("set_an_encryption_key") + message = strings._("set_an_encryption_key_explanation") else: - title = "Unlock encrypted notebook" - message = "Enter your key to unlock the notebook" + title = strings._("unlock_encrypted_notebook") + message = strings._("unlock_encrypted_notebook_explanation") while True: dlg = KeyPrompt(self, title, message) if dlg.exec() != QDialog.Accepted: @@ -591,7 +592,7 @@ class MainWindow(QMainWindow): clicked_date = self._date_from_calendar_pos(pos) menu = QMenu(self) - open_in_new_tab_action = menu.addAction("Open in New Tab") + open_in_new_tab_action = menu.addAction(strings._("open_in_new_tab")) action = menu.exec_(self.calendar.mapToGlobal(pos)) self._showing_context_menu = False @@ -678,7 +679,7 @@ class MainWindow(QMainWindow): return date_iso = editor.current_date.toString("yyyy-MM-dd") md = editor.to_markdown() - self.db.save_new_version(date_iso, md, note="autosave") + self.db.save_new_version(date_iso, md, note=strings._("autosave")) def _on_text_changed(self): self._dirty = True @@ -723,7 +724,7 @@ class MainWindow(QMainWindow): self.db.save_new_version( yesterday_str, modified_text, - "Unchecked checkbox items moved to next day", + strings._("unchecked_checkbox_items_moved_to_next_day"), ) # Join unchecked items into markdown format @@ -782,7 +783,7 @@ class MainWindow(QMainWindow): from datetime import datetime as _dt self.statusBar().showMessage( - f"Saved {date_iso} at {_dt.now().strftime('%H:%M:%S')}", 2000 + strings._("saved") + f" {date_iso}: {_dt.now().strftime('%H:%M:%S')}", 2000 ) def _save_current(self, explicit: bool = False): @@ -799,7 +800,7 @@ class MainWindow(QMainWindow): return note = dlg.note_text() else: - note = "autosave" + note = strings._("autosave") # Save the current editor's date date_iso = self.editor.current_date.toString("yyyy-MM-dd") self._save_date(date_iso, explicit, note) @@ -965,9 +966,9 @@ class MainWindow(QMainWindow): # Let the user pick one or many images paths, _ = QFileDialog.getOpenFileNames( self, - "Insert image(s)", + strings._("insert_images"), "", - "Images (*.png *.jpg *.jpeg *.bmp *.gif *.webp)", + strings._("images") + "(*.png *.jpg *.jpeg *.bmp *.gif *.webp)", ) if not paths: return @@ -990,6 +991,7 @@ class MainWindow(QMainWindow): self.cfg.idle_minutes = getattr(new_cfg, "idle_minutes", self.cfg.idle_minutes) self.cfg.theme = getattr(new_cfg, "theme", self.cfg.theme) self.cfg.move_todos = getattr(new_cfg, "move_todos", self.cfg.move_todos) + self.cfg.locale = getattr(new_cfg, "locale", self.cfg.locale) # Persist once save_db_config(self.cfg) @@ -1002,7 +1004,9 @@ class MainWindow(QMainWindow): self.db.close() if not self._prompt_for_key_until_valid(first_time=False): QMessageBox.warning( - self, "Reopen failed", "Could not unlock database at new path." + self, + strings._("reopen_failed"), + strings._("could_not_unlock_database_at_new_path"), ) return self._load_selected_date() @@ -1045,14 +1049,8 @@ class MainWindow(QMainWindow): # ----------------- Export handler ----------------- # @Slot() def _export(self): - warning_title = "Unencrypted export" - warning_message = """ -Exporting the database will be unencrypted! - -Are you sure you want to continue? - -If you want an encrypted backup, choose Backup instead of Export. -""" + warning_title = strings._("unencrypted_export") + warning_message = strings._("unencrypted_export_warning") dlg = QMessageBox() dlg.setWindowTitle(warning_title) dlg.setText(warning_message) @@ -1074,7 +1072,7 @@ If you want an encrypted backup, choose Backup instead of Export. start_dir = os.path.join(os.path.expanduser("~"), "Documents") filename, selected_filter = QFileDialog.getSaveFileName( - self, "Export entries", start_dir, filters + self, strings._("export_entries"), start_dir, filters ) if not filename: return # user cancelled @@ -1106,11 +1104,15 @@ If you want an encrypted backup, choose Backup instead of Export. elif selected_filter.startswith("SQL"): self.db.export_sql(filename) else: - raise ValueError("Unrecognised extension!") + raise ValueError(strings._("unrecognised_extension")) - QMessageBox.information(self, "Export complete", f"Saved to:\n{filename}") + QMessageBox.information( + self, + strings._("export_complete"), + strings._("saved_to") + f" {filename}", + ) except Exception as e: - QMessageBox.critical(self, "Export failed", str(e)) + QMessageBox.critical(self, strings._("export_failed"), str(e)) # ----------------- Backup handler ----------------- # @Slot() @@ -1122,7 +1124,7 @@ If you want an encrypted backup, choose Backup instead of Export. os.path.expanduser("~"), "Documents", f"bouquin_backup_{now}.db" ) filename, selected_filter = QFileDialog.getSaveFileName( - self, "Backup encrypted notebook", start_dir, filters + self, strings._("backup_encrypted_notebook"), start_dir, filters ) if not filename: return # user cancelled @@ -1138,10 +1140,12 @@ If you want an encrypted backup, choose Backup instead of Export. if selected_filter.startswith("SQL"): self.db.export_sqlcipher(filename) QMessageBox.information( - self, "Backup complete", f"Saved to:\n{filename}" + self, + strings._("backup_complete"), + strings._("saved_to") + f" {filename}", ) except Exception as e: - QMessageBox.critical(self, "Backup failed", str(e)) + QMessageBox.critical(self, strings._("backup_failed"), str(e)) # ----------------- Help handlers ----------------- # @@ -1150,7 +1154,9 @@ If you want an encrypted backup, choose Backup instead of Export. url = QUrl.fromUserInput(url_str) if not QDesktopServices.openUrl(url): QMessageBox.warning( - self, "Open Documentation", f"Couldn't open:\n{url.toDisplayString()}" + self, + strings._("documentation"), + strings._("couldnt_open") + url.toDisplayString(), ) def _open_bugs(self): @@ -1158,7 +1164,9 @@ If you want an encrypted backup, choose Backup instead of Export. url = QUrl.fromUserInput(url_str) if not QDesktopServices.openUrl(url): QMessageBox.warning( - self, "Open Documentation", f"Couldn't open:\n{url.toDisplayString()}" + self, + strings._("report_a_bug"), + strings._("couldnt_open") + url.toDisplayString(), ) # ----------------- Idle handlers ----------------- # @@ -1219,7 +1227,7 @@ If you want an encrypted backup, choose Backup instead of Export. try: ok = self._prompt_for_key_until_valid(first_time=False) except Exception as e: - QMessageBox.critical(self, "Unlock failed", str(e)) + QMessageBox.critical(self, strings._("unlock_failed"), str(e)) return if ok: self._locked = False diff --git a/bouquin/save_dialog.py b/bouquin/save_dialog.py index 27feeaf..bc40cc7 100644 --- a/bouquin/save_dialog.py +++ b/bouquin/save_dialog.py @@ -10,24 +10,24 @@ from PySide6.QtWidgets import ( QDialogButtonBox, ) +from . import strings + class SaveDialog(QDialog): def __init__( self, parent=None, - title: str = "Enter a name for this version", - message: str = "Enter a name for this version?", ): """ Used for explicitly saving a new version of a page. """ super().__init__(parent) - self.setWindowTitle(title) + self.setWindowTitle(strings._("enter_a_name_for_this_version")) v = QVBoxLayout(self) - v.addWidget(QLabel(message)) + v.addWidget(QLabel(strings._("enter_a_name_for_this_version"))) self.note = QLineEdit() now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") - self.note.setText(f"New version I saved at {now}") + self.note.setText(strings._("new_version_i_saved_at") + f" {now}") v.addWidget(self.note) bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) bb.accepted.connect(self.accept) diff --git a/bouquin/search.py b/bouquin/search.py index 71329c0..95a94de 100644 --- a/bouquin/search.py +++ b/bouquin/search.py @@ -16,6 +16,8 @@ from PySide6.QtWidgets import ( QWidget, ) +from . import strings + Row = Tuple[str, str] @@ -30,7 +32,7 @@ class Search(QWidget): self._db = db self.search = QLineEdit() - self.search.setPlaceholderText("Search for notes here") + self.search.setPlaceholderText(strings._("search_for_notes_here")) self.search.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed) self.search.textChanged.connect(self._search) diff --git a/bouquin/settings.py b/bouquin/settings.py index b21835c..cc9b794 100644 --- a/bouquin/settings.py +++ b/bouquin/settings.py @@ -25,8 +25,14 @@ def load_db_config() -> DBConfig: idle = s.value("ui/idle_minutes", 15, type=int) theme = s.value("ui/theme", "system", type=str) move_todos = s.value("ui/move_todos", False, type=bool) + locale = s.value("ui/locale", "en", type=str) return DBConfig( - path=path, key=key, idle_minutes=idle, theme=theme, move_todos=move_todos + path=path, + key=key, + idle_minutes=idle, + theme=theme, + move_todos=move_todos, + locale=locale, ) @@ -37,3 +43,4 @@ def save_db_config(cfg: DBConfig) -> None: s.setValue("ui/idle_minutes", str(cfg.idle_minutes)) s.setValue("ui/theme", str(cfg.theme)) s.setValue("ui/move_todos", str(cfg.move_todos)) + s.setValue("ui/locale", str(cfg.locale)) diff --git a/bouquin/settings_dialog.py b/bouquin/settings_dialog.py index 3091e14..f3beccf 100644 --- a/bouquin/settings_dialog.py +++ b/bouquin/settings_dialog.py @@ -4,6 +4,7 @@ from pathlib import Path from PySide6.QtWidgets import ( QCheckBox, + QComboBox, QDialog, QFormLayout, QFrame, @@ -30,11 +31,13 @@ from .settings import load_db_config, save_db_config from .theme import Theme from .key_prompt import KeyPrompt +from . import strings + class SettingsDialog(QDialog): def __init__(self, cfg: DBConfig, db: DBManager, parent=None): super().__init__(parent) - self.setWindowTitle("Settings") + self.setWindowTitle(strings._("settings")) self._cfg = DBConfig(path=cfg.path, key="") self._db = db self.key = "" @@ -47,12 +50,12 @@ class SettingsDialog(QDialog): current_settings = load_db_config() # Add theme selection - theme_group = QGroupBox("Theme") + theme_group = QGroupBox(strings._("theme")) theme_layout = QVBoxLayout(theme_group) - self.theme_system = QRadioButton("System") - self.theme_light = QRadioButton("Light") - self.theme_dark = QRadioButton("Dark") + self.theme_system = QRadioButton(strings._("system")) + self.theme_light = QRadioButton(strings._("light")) + self.theme_dark = QRadioButton(strings._("dark")) # Load current theme from settings current_theme = current_settings.theme @@ -69,12 +72,37 @@ class SettingsDialog(QDialog): form.addRow(theme_group) + # Locale settings + locale_group = QGroupBox(strings._("locale")) + locale_layout = QVBoxLayout(locale_group) + locale_layout.setContentsMargins(12, 8, 12, 12) + locale_layout.setSpacing(6) + + self.locale_combobox = QComboBox() + self.locale_combobox.addItems(strings._AVAILABLE) + self.locale_combobox.setCurrentText(current_settings.locale) + locale_layout.addWidget(self.locale_combobox, 0, Qt.AlignLeft) + + # Explanation for locale + self.locale_label = QLabel(strings._("locale_restart")) + self.locale_label.setWordWrap(True) + self.locale_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) + # make it look secondary + lpal = self.locale_label.palette() + self.locale_label.setForegroundRole(QPalette.PlaceholderText) + self.locale_label.setPalette(lpal) + locale_row = QHBoxLayout() + locale_row.setContentsMargins(24, 0, 0, 0) + locale_row.addWidget(self.locale_label) + locale_layout.addLayout(locale_row) + form.addRow(locale_group) + # Add Behaviour - behaviour_group = QGroupBox("Behaviour") + behaviour_group = QGroupBox(strings._("behaviour")) behaviour_layout = QVBoxLayout(behaviour_group) self.move_todos = QCheckBox( - "Move yesterday's unchecked TODOs to today on startup" + strings._("move_yesterdays_unchecked_todos_to_today_on_startup") ) self.move_todos.setChecked(current_settings.move_todos) self.move_todos.setCursor(Qt.PointingHandCursor) @@ -84,7 +112,7 @@ class SettingsDialog(QDialog): self.path_edit = QLineEdit(str(self._cfg.path)) self.path_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - browse_btn = QPushButton("Browse…") + browse_btn = QPushButton(strings._("browse")) browse_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) browse_btn.clicked.connect(self._browse) path_row = QWidget() @@ -94,16 +122,16 @@ class SettingsDialog(QDialog): h.addWidget(browse_btn, 0) h.setStretch(0, 1) h.setStretch(1, 0) - form.addRow("Database path", path_row) + form.addRow(strings._("database_path"), path_row) # Encryption settings - enc_group = QGroupBox("Encryption") + enc_group = QGroupBox(strings._("encryption")) enc = QVBoxLayout(enc_group) enc.setContentsMargins(12, 8, 12, 12) enc.setSpacing(6) # Checkbox to remember key - self.save_key_btn = QCheckBox("Remember key") + self.save_key_btn = QCheckBox(strings._("remember_key")) self.key = current_settings.key or "" self.save_key_btn.setChecked(bool(self.key)) self.save_key_btn.setCursor(Qt.PointingHandCursor) @@ -111,10 +139,7 @@ class SettingsDialog(QDialog): enc.addWidget(self.save_key_btn, 0, Qt.AlignLeft) # Explanation for remembering key - self.save_key_label = QLabel( - "If you don't want to be prompted for your encryption key, check this to remember it. " - "WARNING: the key is saved to disk and could be recoverable if your disk is compromised." - ) + self.save_key_label = QLabel(strings._("save_key_warning")) self.save_key_label.setWordWrap(True) self.save_key_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) # make it look secondary @@ -133,7 +158,7 @@ class SettingsDialog(QDialog): enc.addWidget(line) # Change key button - self.rekey_btn = QPushButton("Change encryption key") + self.rekey_btn = QPushButton(strings._("change_encryption_key")) self.rekey_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.rekey_btn.clicked.connect(self._change_key) @@ -142,7 +167,7 @@ class SettingsDialog(QDialog): form.addRow(enc_group) # Privacy settings - priv_group = QGroupBox("Lock screen when idle") + priv_group = QGroupBox(strings._("lock_screen_when_idle")) priv = QVBoxLayout(priv_group) priv.setContentsMargins(12, 8, 12, 12) priv.setSpacing(6) @@ -152,15 +177,11 @@ class SettingsDialog(QDialog): self.idle_spin.setSingleStep(1) self.idle_spin.setAccelerated(True) self.idle_spin.setSuffix(" min") - self.idle_spin.setSpecialValueText("Never") + self.idle_spin.setSpecialValueText(strings._("Never")) self.idle_spin.setValue(getattr(cfg, "idle_minutes", 15)) priv.addWidget(self.idle_spin, 0, Qt.AlignLeft) # Explanation for idle option (autolock) - self.idle_spin_label = QLabel( - "Bouquin will automatically lock the notepad after this length of time, " - "after which you'll need to re-enter the key to unlock it. " - "Set to 0 (never) to never lock." - ) + self.idle_spin_label = QLabel(strings._("autolock_explanation")) self.idle_spin_label.setWordWrap(True) self.idle_spin_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) # make it look secondary @@ -176,21 +197,19 @@ class SettingsDialog(QDialog): form.addRow(priv_group) # Maintenance settings - maint_group = QGroupBox("Database maintenance") + maint_group = QGroupBox(strings._("database_maintenance")) maint = QVBoxLayout(maint_group) maint.setContentsMargins(12, 8, 12, 12) maint.setSpacing(6) - self.compact_btn = QPushButton("Compact database") + self.compact_btn = QPushButton(strings._("database_compact")) self.compact_btn.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed) self.compact_btn.clicked.connect(self._compact_btn_clicked) maint.addWidget(self.compact_btn, 0, Qt.AlignLeft) - # Explanation for compating button - self.compact_label = QLabel( - "Compacting runs VACUUM on the database. This can help reduce its size." - ) + # Explanation for compacting button + self.compact_label = QLabel(strings._("database_compact_explanation")) self.compact_label.setWordWrap(True) self.compact_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) # make it look secondary @@ -220,9 +239,9 @@ class SettingsDialog(QDialog): def _browse(self): p, _ = QFileDialog.getSaveFileName( self, - "Choose database file", + strings._("database_path"), self.path_edit.text(), - "DB Files (*.db);;All Files (*)", + "(*.db);;(*)", ) if p: self.path_edit.setText(p) @@ -244,6 +263,7 @@ class SettingsDialog(QDialog): idle_minutes=self.idle_spin.value(), theme=selected_theme.value, move_todos=self.move_todos.isChecked(), + locale=self.locale_combobox.currentText(), ) save_db_config(self._cfg) @@ -251,27 +271,39 @@ class SettingsDialog(QDialog): self.accept() def _change_key(self): - p1 = KeyPrompt(self, title="Change key", message="Enter a new encryption key") + p1 = KeyPrompt( + self, + title=strings._("change_encryption_key"), + message=strings._("enter_a_new_encryption_key"), + ) if p1.exec() != QDialog.Accepted: return new_key = p1.key() - p2 = KeyPrompt(self, title="Change key", message="Re-enter the new key") + p2 = KeyPrompt( + self, + title=strings._("change_encryption_key"), + message=strings._("reenter_the_new_key"), + ) if p2.exec() != QDialog.Accepted: return if new_key != p2.key(): - QMessageBox.warning(self, "Key mismatch", "The two entries did not match.") + QMessageBox.warning( + self, strings._("key_mismatch"), strings._("key_mismatch_explanation") + ) return if not new_key: - QMessageBox.warning(self, "Empty key", "Key cannot be empty.") + QMessageBox.warning( + self, strings._("empty_key"), strings._("empty_key_explanation") + ) return try: self.key = new_key self._db.rekey(new_key) QMessageBox.information( - self, "Key changed", "The notebook was re-encrypted with the new key!" + self, strings._("key_changed"), strings._("key_changed_explanation") ) except Exception as e: - QMessageBox.critical(self, "Error", e) + QMessageBox.critical(self, strings._("error"), e) @Slot(bool) def _save_key_btn_clicked(self, checked: bool): @@ -279,7 +311,9 @@ class SettingsDialog(QDialog): if checked: if not self.key: p1 = KeyPrompt( - self, title="Enter your key", message="Enter the encryption key" + self, + title=strings._("unlock_encrypted_notebook_explanation"), + message=strings._("unlock_encrypted_notebook_explanation"), ) if p1.exec() != QDialog.Accepted: self.save_key_btn.blockSignals(True) @@ -292,9 +326,11 @@ class SettingsDialog(QDialog): def _compact_btn_clicked(self): try: self._db.compact() - QMessageBox.information(self, "Success", "Database compacted successfully!") + QMessageBox.information( + self, strings._("success"), strings._("database_compacted_successfully") + ) except Exception as e: - QMessageBox.critical(self, "Error", e) + QMessageBox.critical(self, strings._("error"), e) @property def config(self) -> DBConfig: diff --git a/bouquin/strings.py b/bouquin/strings.py new file mode 100644 index 0000000..306183a --- /dev/null +++ b/bouquin/strings.py @@ -0,0 +1,42 @@ +from importlib.resources import files +import json + +_AVAILABLE = ("en", "fr") +_DEFAULT = "en" + +strings = {} +translations = {} + + +def load_strings(current_locale: str | None = None) -> None: + global strings, translations + translations = {} + + # read json resources from bouquin/locales/*.json + root = files("bouquin") / "locales" + for loc in _AVAILABLE: + data = (root / f"{loc}.json").read_text(encoding="utf-8") + translations[loc] = json.loads(data) + + # Load in the system's locale if not passed in somehow from settings + if not current_locale: + try: + from PySide6.QtCore import QLocale + + current_locale = QLocale.system().name().split("_")[0] + except Exception: + current_locale = _DEFAULT + + if current_locale not in translations: + current_locale = _DEFAULT + + base = translations[_DEFAULT] + cur = translations.get(current_locale, {}) + strings = {k: (cur.get(k) or base[k]) for k in base} + + +def translated(k: str) -> str: + return strings.get(k, k) + + +_ = translated diff --git a/bouquin/toolbar.py b/bouquin/toolbar.py index acf0413..89999b8 100644 --- a/bouquin/toolbar.py +++ b/bouquin/toolbar.py @@ -4,6 +4,8 @@ from PySide6.QtCore import Signal, Qt from PySide6.QtGui import QAction, QKeySequence, QFont, QFontDatabase, QActionGroup from PySide6.QtWidgets import QToolBar +from . import strings + class ToolBar(QToolBar): boldRequested = Signal() @@ -18,79 +20,79 @@ class ToolBar(QToolBar): insertImageRequested = Signal() def __init__(self, parent=None): - super().__init__("Format", parent) - self.setObjectName("Format") + super().__init__(strings._("toolbar_format"), parent) + self.setObjectName(strings._("toolbar_format")) self.setToolButtonStyle(Qt.ToolButtonTextOnly) self._build_actions() self._apply_toolbar_styles() def _build_actions(self): self.actBold = QAction("B", self) - self.actBold.setToolTip("Bold") + self.actBold.setToolTip(strings._("toolbar_bold")) self.actBold.setCheckable(True) self.actBold.setShortcut(QKeySequence.Bold) self.actBold.triggered.connect(self.boldRequested) self.actItalic = QAction("I", self) - self.actItalic.setToolTip("Italic") + self.actItalic.setToolTip(strings._("toolbar_italic")) self.actItalic.setCheckable(True) self.actItalic.setShortcut(QKeySequence.Italic) self.actItalic.triggered.connect(self.italicRequested) self.actStrike = QAction("S", self) - self.actStrike.setToolTip("Strikethrough") + self.actStrike.setToolTip(strings._("toolbar_strikethrough")) self.actStrike.setCheckable(True) self.actStrike.setShortcut("Ctrl+-") self.actStrike.triggered.connect(self.strikeRequested) self.actCode = QAction("", self) - self.actCode.setToolTip("Code block") + self.actCode.setToolTip(strings._("toolbar_code_block")) self.actCode.setShortcut("Ctrl+`") self.actCode.triggered.connect(self.codeRequested) # Headings self.actH1 = QAction("H1", self) - self.actH1.setToolTip("Heading 1") + self.actH1.setToolTip(strings._("toolbar_heading") + " 1") self.actH1.setCheckable(True) self.actH1.setShortcut("Ctrl+1") self.actH1.triggered.connect(lambda: self.headingRequested.emit(24)) self.actH2 = QAction("H2", self) - self.actH2.setToolTip("Heading 2") + self.actH2.setToolTip(strings._("toolbar_heading") + " 2") self.actH2.setCheckable(True) self.actH2.setShortcut("Ctrl+2") self.actH2.triggered.connect(lambda: self.headingRequested.emit(18)) self.actH3 = QAction("H3", self) - self.actH3.setToolTip("Heading 3") + self.actH3.setToolTip(strings._("toolbar_heading") + " 3") self.actH3.setCheckable(True) self.actH3.setShortcut("Ctrl+3") self.actH3.triggered.connect(lambda: self.headingRequested.emit(14)) self.actNormal = QAction("N", self) - self.actNormal.setToolTip("Normal paragraph text") + self.actNormal.setToolTip(strings._("toolbar_normal_paragraph_text")) self.actNormal.setCheckable(True) self.actNormal.setShortcut("Ctrl+N") self.actNormal.triggered.connect(lambda: self.headingRequested.emit(0)) # Lists self.actBullets = QAction("•", self) - self.actBullets.setToolTip("Bulleted list") + self.actBullets.setToolTip(strings._("toolbar_bulleted_list")) self.actBullets.setCheckable(True) self.actBullets.triggered.connect(self.bulletsRequested) self.actNumbers = QAction("1.", self) - self.actNumbers.setToolTip("Numbered list") + self.actNumbers.setToolTip(strings._("toolbar_numbered_list")) self.actNumbers.setCheckable(True) self.actNumbers.triggered.connect(self.numbersRequested) self.actCheckboxes = QAction("☐", self) - self.actCheckboxes.setToolTip("Toggle checkboxes") + self.actCheckboxes.setToolTip(strings._("toolbar_toggle_checkboxes")) self.actCheckboxes.triggered.connect(self.checkboxesRequested) # Images - self.actInsertImg = QAction("Image", self) - self.actInsertImg.setToolTip("Insert image") + self.actInsertImg = QAction(strings._("images"), self) + self.actInsertImg.setToolTip(strings._("insert_images")) self.actInsertImg.setShortcut("Ctrl+Shift+I") self.actInsertImg.triggered.connect(self.insertImageRequested) # History button - self.actHistory = QAction("History", self) + self.actHistory = QAction(strings._("history"), self) self.actHistory.triggered.connect(self.historyRequested) # Set exclusive buttons in QActionGroups @@ -151,7 +153,7 @@ class ToolBar(QToolBar): self._style_letter_button(self.actNumbers, "1.") # History - self._style_letter_button(self.actHistory, "View History") + self._style_letter_button(self.actHistory, strings._("view_history")) def _style_letter_button( self, diff --git a/pyproject.toml b/pyproject.toml index d88389d..c71caf9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,11 +1,13 @@ [tool.poetry] name = "bouquin" -version = "0.2.1.7" +version = "0.2.1.8" description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher." authors = ["Miguel Jacq "] readme = "README.md" license = "GPL-3.0-or-later" repository = "https://git.mig5.net/mig5/bouquin" +packages = [{ include = "bouquin" }] +include = ["bouquin/locales/*.json"] [tool.poetry.dependencies] python = ">=3.9,<3.14"