Add translation capability, offer English and French as options

This commit is contained in:
Miguel Jacq 2025-11-12 13:58:58 +11:00
parent 54a6be835f
commit f578d562e6
Signed by: mig5
GPG key ID: 59B3F0C24135C6A9
17 changed files with 490 additions and 138 deletions

View file

@ -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.

View file

@ -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:

View file

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

View file

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

View file

@ -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(

113
bouquin/locales/en.json Normal file
View file

@ -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"
}

114
bouquin/locales/fr.json Normal file
View file

@ -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 lapplication 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 lexportation",
"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 douvrir",
"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 dinactivité",
"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 dhier vers aujourdhui",
"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": "Lexport 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 quExport.",
"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 jai enregistrée à",
"save_key_warning": "Si vous ne voulez pas que lon 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 dinactivité",
"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"
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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:

42
bouquin/strings.py Normal file
View file

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

View file

@ -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,

View file

@ -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 <mig@mig5.net>"]
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"