Add translation capability, offer English and French as options
This commit is contained in:
parent
54a6be835f
commit
f578d562e6
17 changed files with 490 additions and 138 deletions
|
|
@ -1,3 +1,7 @@
|
||||||
|
# 0.2.1.8
|
||||||
|
|
||||||
|
* Translate all strings, add French, add locale choice in settings
|
||||||
|
|
||||||
# 0.2.1.7
|
# 0.2.1.7
|
||||||
|
|
||||||
* Fix being able to set bold, italic and strikethrough at the same time.
|
* Fix being able to set bold, italic and strikethrough at the same time.
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ from pathlib import Path
|
||||||
from sqlcipher3 import dbapi2 as sqlite
|
from sqlcipher3 import dbapi2 as sqlite
|
||||||
from typing import List, Sequence, Tuple
|
from typing import List, Sequence, Tuple
|
||||||
|
|
||||||
|
from . import strings
|
||||||
|
|
||||||
Entry = Tuple[str, str]
|
Entry = Tuple[str, str]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -19,6 +21,7 @@ class DBConfig:
|
||||||
idle_minutes: int = 15 # 0 = never lock
|
idle_minutes: int = 15 # 0 = never lock
|
||||||
theme: str = "system"
|
theme: str = "system"
|
||||||
move_todos: bool = False
|
move_todos: bool = False
|
||||||
|
locale: str = "en"
|
||||||
|
|
||||||
|
|
||||||
class DBManager:
|
class DBManager:
|
||||||
|
|
@ -62,8 +65,12 @@ class DBManager:
|
||||||
# Not OK: rows of problems returned
|
# Not OK: rows of problems returned
|
||||||
details = "; ".join(str(r[0]) for r in rows if r and r[0] is not None)
|
details = "; ".join(str(r[0]) for r in rows if r and r[0] is not None)
|
||||||
raise sqlite.IntegrityError(
|
raise sqlite.IntegrityError(
|
||||||
"SQLCipher integrity check failed"
|
strings._("db_sqlcipher_integrity_check_failed")
|
||||||
+ (f": {details}" if details else f" ({len(rows)} issue(s) reported)")
|
+ (
|
||||||
|
f": {details}"
|
||||||
|
if details
|
||||||
|
else f" ({len(rows)} {strings._('db_issues_reported')})"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
def _ensure_schema(self) -> None:
|
def _ensure_schema(self) -> None:
|
||||||
|
|
@ -115,7 +122,7 @@ class DBManager:
|
||||||
self.conn = None
|
self.conn = None
|
||||||
self.cfg.key = new_key
|
self.cfg.key = new_key
|
||||||
if not self.connect():
|
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:
|
def get_entry(self, date_iso: str) -> str:
|
||||||
"""
|
"""
|
||||||
|
|
@ -251,7 +258,9 @@ class DBManager:
|
||||||
"SELECT date FROM versions WHERE id=?;", (version_id,)
|
"SELECT date FROM versions WHERE id=?;", (version_id,)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
if row is None or row["date"] != date_iso:
|
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:
|
with self.conn:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
|
|
@ -375,7 +384,7 @@ class DBManager:
|
||||||
cur = self.conn.cursor()
|
cur = self.conn.cursor()
|
||||||
cur.execute("VACUUM")
|
cur.execute("VACUUM")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f"Error: {e}")
|
print(f"{strings._('error')}: {e}")
|
||||||
|
|
||||||
def close(self) -> None:
|
def close(self) -> None:
|
||||||
if self.conn is not None:
|
if self.conn is not None:
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ from PySide6.QtWidgets import (
|
||||||
QTextEdit,
|
QTextEdit,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from . import strings
|
||||||
|
|
||||||
|
|
||||||
class FindBar(QWidget):
|
class FindBar(QWidget):
|
||||||
"""Widget for finding text in the Editor"""
|
"""Widget for finding text in the Editor"""
|
||||||
|
|
@ -41,17 +43,17 @@ class FindBar(QWidget):
|
||||||
layout = QHBoxLayout(self)
|
layout = QHBoxLayout(self)
|
||||||
layout.setContentsMargins(6, 0, 6, 0)
|
layout.setContentsMargins(6, 0, 6, 0)
|
||||||
|
|
||||||
layout.addWidget(QLabel("Find:"))
|
layout.addWidget(QLabel(strings._("find")))
|
||||||
|
|
||||||
self.edit = QLineEdit(self)
|
self.edit = QLineEdit(self)
|
||||||
self.edit.setPlaceholderText("Type to search")
|
self.edit.setPlaceholderText(strings._("find_bar_type_to_search"))
|
||||||
layout.addWidget(self.edit)
|
layout.addWidget(self.edit)
|
||||||
|
|
||||||
self.case = QCheckBox("Match case", self)
|
self.case = QCheckBox(strings._("find_bar_match_case"), self)
|
||||||
layout.addWidget(self.case)
|
layout.addWidget(self.case)
|
||||||
|
|
||||||
self.prevBtn = QPushButton("Prev", self)
|
self.prevBtn = QPushButton(strings._("previous"), self)
|
||||||
self.nextBtn = QPushButton("Next", self)
|
self.nextBtn = QPushButton(strings._("next"), self)
|
||||||
self.closeBtn = QPushButton("✕", self)
|
self.closeBtn = QPushButton("✕", self)
|
||||||
self.closeBtn.setFlat(True)
|
self.closeBtn.setFlat(True)
|
||||||
layout.addWidget(self.prevBtn)
|
layout.addWidget(self.prevBtn)
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,8 @@ from PySide6.QtWidgets import (
|
||||||
QTabWidget,
|
QTabWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from . import strings
|
||||||
|
|
||||||
|
|
||||||
def _markdown_to_text(s: str) -> str:
|
def _markdown_to_text(s: str) -> str:
|
||||||
"""Convert markdown to plain text for diff comparison."""
|
"""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)."""
|
"""Return HTML with colored unified diff (+ green, - red, context gray)."""
|
||||||
a = _markdown_to_text(old_md).splitlines()
|
a = _markdown_to_text(old_md).splitlines()
|
||||||
b = _markdown_to_text(new_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 = []
|
lines = []
|
||||||
for line in ud:
|
for line in ud:
|
||||||
if line.startswith("+") and not line.startswith("+++"):
|
if line.startswith("+") and not line.startswith("+++"):
|
||||||
|
|
@ -67,7 +71,7 @@ class HistoryDialog(QDialog):
|
||||||
|
|
||||||
def __init__(self, db, date_iso: str, parent=None):
|
def __init__(self, db, date_iso: str, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle(f"History — {date_iso}")
|
self.setWindowTitle(f"{strings._('history')} — {date_iso}")
|
||||||
self._db = db
|
self._db = db
|
||||||
self._date = date_iso
|
self._date = date_iso
|
||||||
self._versions = [] # list[dict] from DB
|
self._versions = [] # list[dict] from DB
|
||||||
|
|
@ -88,8 +92,8 @@ class HistoryDialog(QDialog):
|
||||||
self.preview.setOpenExternalLinks(True)
|
self.preview.setOpenExternalLinks(True)
|
||||||
self.diff = QTextBrowser()
|
self.diff = QTextBrowser()
|
||||||
self.diff.setOpenExternalLinks(False)
|
self.diff.setOpenExternalLinks(False)
|
||||||
self.tabs.addTab(self.preview, "Preview")
|
self.tabs.addTab(self.preview, strings._("history_dialog_preview"))
|
||||||
self.tabs.addTab(self.diff, "Diff")
|
self.tabs.addTab(self.diff, strings._("history_dialog_diff"))
|
||||||
self.tabs.setMinimumSize(500, 650)
|
self.tabs.setMinimumSize(500, 650)
|
||||||
top.addWidget(self.tabs, 2)
|
top.addWidget(self.tabs, 2)
|
||||||
|
|
||||||
|
|
@ -98,9 +102,9 @@ class HistoryDialog(QDialog):
|
||||||
# Buttons
|
# Buttons
|
||||||
row = QHBoxLayout()
|
row = QHBoxLayout()
|
||||||
row.addStretch(1)
|
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_revert.clicked.connect(self._revert)
|
||||||
self.btn_close = QPushButton("Close")
|
self.btn_close = QPushButton(strings._("close"))
|
||||||
self.btn_close.clicked.connect(self.reject)
|
self.btn_close.clicked.connect(self.reject)
|
||||||
row.addWidget(self.btn_revert)
|
row.addWidget(self.btn_revert)
|
||||||
row.addWidget(self.btn_close)
|
row.addWidget(self.btn_close)
|
||||||
|
|
@ -126,7 +130,7 @@ class HistoryDialog(QDialog):
|
||||||
if v.get("note"):
|
if v.get("note"):
|
||||||
label += f" · {v['note']}"
|
label += f" · {v['note']}"
|
||||||
if v["is_current"]:
|
if v["is_current"]:
|
||||||
label += " **(current)**"
|
label += " **(" + strings._("current") + ")**"
|
||||||
it = QListWidgetItem(label)
|
it = QListWidgetItem(label)
|
||||||
it.setData(Qt.UserRole, v["id"])
|
it.setData(Qt.UserRole, v["id"])
|
||||||
self.list.addItem(it)
|
self.list.addItem(it)
|
||||||
|
|
@ -168,6 +172,8 @@ class HistoryDialog(QDialog):
|
||||||
try:
|
try:
|
||||||
self._db.revert_to_version(self._date, version_id=sel_id)
|
self._db.revert_to_version(self._date, version_id=sel_id)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(self, "Revert failed", str(e))
|
QMessageBox.critical(
|
||||||
|
self, strings._("history_dialog_revert_failed"), str(e)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,15 @@ from PySide6.QtWidgets import (
|
||||||
QDialogButtonBox,
|
QDialogButtonBox,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from . import strings
|
||||||
|
|
||||||
|
|
||||||
class KeyPrompt(QDialog):
|
class KeyPrompt(QDialog):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
parent=None,
|
parent=None,
|
||||||
title: str = "Enter key",
|
title: str = strings._("key_prompt_enter_key"),
|
||||||
message: str = "Enter key",
|
message: str = strings._("key_prompt_enter_key"),
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Prompt the user for the key required to decrypt the database.
|
Prompt the user for the key required to decrypt the database.
|
||||||
|
|
@ -30,7 +32,7 @@ class KeyPrompt(QDialog):
|
||||||
self.edit = QLineEdit()
|
self.edit = QLineEdit()
|
||||||
self.edit.setEchoMode(QLineEdit.Password)
|
self.edit.setEchoMode(QLineEdit.Password)
|
||||||
v.addWidget(self.edit)
|
v.addWidget(self.edit)
|
||||||
toggle = QPushButton("Show")
|
toggle = QPushButton(strings._("show"))
|
||||||
toggle.setCheckable(True)
|
toggle.setCheckable(True)
|
||||||
toggle.toggled.connect(
|
toggle.toggled.connect(
|
||||||
lambda c: self.edit.setEchoMode(
|
lambda c: self.edit.setEchoMode(
|
||||||
|
|
|
||||||
113
bouquin/locales/en.json
Normal file
113
bouquin/locales/en.json
Normal 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
114
bouquin/locales/fr.json
Normal 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 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"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
||||||
from PySide6.QtCore import Qt, QEvent
|
from PySide6.QtCore import Qt, QEvent
|
||||||
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton
|
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QPushButton
|
||||||
|
|
||||||
|
from . import strings
|
||||||
from .theme import ThemeManager
|
from .theme import ThemeManager
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -22,11 +23,11 @@ class LockOverlay(QWidget):
|
||||||
lay = QVBoxLayout(self)
|
lay = QVBoxLayout(self)
|
||||||
lay.addStretch(1)
|
lay.addStretch(1)
|
||||||
|
|
||||||
msg = QLabel("Locked due to inactivity", self)
|
msg = QLabel(strings._("lock_overlay_locked_due_to_inactivity"), self)
|
||||||
msg.setObjectName("lockLabel")
|
msg.setObjectName("lockLabel")
|
||||||
msg.setAlignment(Qt.AlignCenter)
|
msg.setAlignment(Qt.AlignCenter)
|
||||||
|
|
||||||
self._btn = QPushButton("Unlock", self)
|
self._btn = QPushButton(strings._("lock_overlay_unlock"), self)
|
||||||
self._btn.setObjectName("unlockButton")
|
self._btn.setObjectName("unlockButton")
|
||||||
self._btn.setFixedWidth(200)
|
self._btn.setFixedWidth(200)
|
||||||
self._btn.setCursor(Qt.PointingHandCursor)
|
self._btn.setCursor(Qt.PointingHandCursor)
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ from PySide6.QtWidgets import QApplication
|
||||||
from .settings import APP_NAME, APP_ORG, get_settings
|
from .settings import APP_NAME, APP_ORG, get_settings
|
||||||
from .main_window import MainWindow
|
from .main_window import MainWindow
|
||||||
from .theme import Theme, ThemeConfig, ThemeManager
|
from .theme import Theme, ThemeConfig, ThemeManager
|
||||||
|
from . import strings
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|
@ -19,6 +20,7 @@ def main():
|
||||||
themes = ThemeManager(app, cfg)
|
themes = ThemeManager(app, cfg)
|
||||||
themes.apply(cfg.theme)
|
themes.apply(cfg.theme)
|
||||||
|
|
||||||
|
strings.load_strings(s.value("ui/locale", "en"))
|
||||||
win = MainWindow(themes=themes)
|
win = MainWindow(themes=themes)
|
||||||
win.show()
|
win.show()
|
||||||
sys.exit(app.exec())
|
sys.exit(app.exec())
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,7 @@ from .save_dialog import SaveDialog
|
||||||
from .search import Search
|
from .search import Search
|
||||||
from .settings import APP_ORG, APP_NAME, load_db_config, save_db_config
|
from .settings import APP_ORG, APP_NAME, load_db_config, save_db_config
|
||||||
from .settings_dialog import SettingsDialog
|
from .settings_dialog import SettingsDialog
|
||||||
|
from . import strings
|
||||||
from .toolbar import ToolBar
|
from .toolbar import ToolBar
|
||||||
from .theme import ThemeManager
|
from .theme import ThemeManager
|
||||||
|
|
||||||
|
|
@ -169,7 +170,7 @@ class MainWindow(QMainWindow):
|
||||||
)
|
)
|
||||||
|
|
||||||
# Status bar for feedback
|
# 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
|
# Add findBar and add it to the statusBar
|
||||||
# FindBar will get the current editor dynamically via a callable
|
# FindBar will get the current editor dynamically via a callable
|
||||||
self.findBar = FindBar(lambda: self.editor, shortcut_parent=self, parent=self)
|
self.findBar = FindBar(lambda: self.editor, shortcut_parent=self, parent=self)
|
||||||
|
|
@ -179,84 +180,84 @@ class MainWindow(QMainWindow):
|
||||||
|
|
||||||
# Menu bar (File)
|
# Menu bar (File)
|
||||||
mb = self.menuBar()
|
mb = self.menuBar()
|
||||||
file_menu = mb.addMenu("&File")
|
file_menu = mb.addMenu("&" + strings._("file"))
|
||||||
act_save = QAction("&Save a version", self)
|
act_save = QAction("&" + strings._("main_window_save_a_version"), self)
|
||||||
act_save.setShortcut("Ctrl+S")
|
act_save.setShortcut("Ctrl+S")
|
||||||
act_save.triggered.connect(lambda: self._save_current(explicit=True))
|
act_save.triggered.connect(lambda: self._save_current(explicit=True))
|
||||||
file_menu.addAction(act_save)
|
file_menu.addAction(act_save)
|
||||||
act_history = QAction("History", self)
|
act_history = QAction("&" + strings._("history"), self)
|
||||||
act_history.setShortcut("Ctrl+H")
|
act_history.setShortcut("Ctrl+H")
|
||||||
act_history.setShortcutContext(Qt.ApplicationShortcut)
|
act_history.setShortcutContext(Qt.ApplicationShortcut)
|
||||||
act_history.triggered.connect(self._open_history)
|
act_history.triggered.connect(self._open_history)
|
||||||
file_menu.addAction(act_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.setShortcut("Ctrl+G")
|
||||||
act_settings.triggered.connect(self._open_settings)
|
act_settings.triggered.connect(self._open_settings)
|
||||||
file_menu.addAction(act_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.setShortcut("Ctrl+E")
|
||||||
act_export.triggered.connect(self._export)
|
act_export.triggered.connect(self._export)
|
||||||
file_menu.addAction(act_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.setShortcut("Ctrl+Shift+B")
|
||||||
act_backup.triggered.connect(self._backup)
|
act_backup.triggered.connect(self._backup)
|
||||||
file_menu.addAction(act_backup)
|
file_menu.addAction(act_backup)
|
||||||
file_menu.addSeparator()
|
file_menu.addSeparator()
|
||||||
act_quit = QAction("&Quit", self)
|
act_quit = QAction("&" + strings._("quit"), self)
|
||||||
act_quit.setShortcut("Ctrl+Q")
|
act_quit.setShortcut("Ctrl+Q")
|
||||||
act_quit.triggered.connect(self.close)
|
act_quit.triggered.connect(self.close)
|
||||||
file_menu.addAction(act_quit)
|
file_menu.addAction(act_quit)
|
||||||
|
|
||||||
# Navigate menu with next/previous/today
|
# Navigate menu with next/previous/today
|
||||||
nav_menu = mb.addMenu("&Navigate")
|
nav_menu = mb.addMenu("&" + strings._("navigate"))
|
||||||
act_prev = QAction("Previous Day", self)
|
act_prev = QAction(strings._("previous_day"), self)
|
||||||
act_prev.setShortcut("Ctrl+Shift+P")
|
act_prev.setShortcut("Ctrl+Shift+P")
|
||||||
act_prev.setShortcutContext(Qt.ApplicationShortcut)
|
act_prev.setShortcutContext(Qt.ApplicationShortcut)
|
||||||
act_prev.triggered.connect(lambda: self._adjust_day(-1))
|
act_prev.triggered.connect(lambda: self._adjust_day(-1))
|
||||||
nav_menu.addAction(act_prev)
|
nav_menu.addAction(act_prev)
|
||||||
self.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.setShortcut("Ctrl+Shift+N")
|
||||||
act_next.setShortcutContext(Qt.ApplicationShortcut)
|
act_next.setShortcutContext(Qt.ApplicationShortcut)
|
||||||
act_next.triggered.connect(lambda: self._adjust_day(1))
|
act_next.triggered.connect(lambda: self._adjust_day(1))
|
||||||
nav_menu.addAction(act_next)
|
nav_menu.addAction(act_next)
|
||||||
self.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.setShortcut("Ctrl+Shift+T")
|
||||||
act_today.setShortcutContext(Qt.ApplicationShortcut)
|
act_today.setShortcutContext(Qt.ApplicationShortcut)
|
||||||
act_today.triggered.connect(self._adjust_today)
|
act_today.triggered.connect(self._adjust_today)
|
||||||
nav_menu.addAction(act_today)
|
nav_menu.addAction(act_today)
|
||||||
self.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.setShortcut(QKeySequence.Find)
|
||||||
act_find.triggered.connect(self.findBar.show_bar)
|
act_find.triggered.connect(self.findBar.show_bar)
|
||||||
nav_menu.addAction(act_find)
|
nav_menu.addAction(act_find)
|
||||||
self.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.setShortcut(QKeySequence.FindNext)
|
||||||
act_find_next.triggered.connect(self.findBar.find_next)
|
act_find_next.triggered.connect(self.findBar.find_next)
|
||||||
nav_menu.addAction(act_find_next)
|
nav_menu.addAction(act_find_next)
|
||||||
self.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.setShortcut(QKeySequence.FindPrevious)
|
||||||
act_find_prev.triggered.connect(self.findBar.find_prev)
|
act_find_prev.triggered.connect(self.findBar.find_prev)
|
||||||
nav_menu.addAction(act_find_prev)
|
nav_menu.addAction(act_find_prev)
|
||||||
self.addAction(act_find_prev)
|
self.addAction(act_find_prev)
|
||||||
|
|
||||||
# Help menu with drop-down
|
# Help menu with drop-down
|
||||||
help_menu = mb.addMenu("&Help")
|
help_menu = mb.addMenu("&" + strings._("help"))
|
||||||
act_docs = QAction("Documentation", self)
|
act_docs = QAction(strings._("documentation"), self)
|
||||||
act_docs.setShortcut("Ctrl+D")
|
act_docs.setShortcut("Ctrl+D")
|
||||||
act_docs.setShortcutContext(Qt.ApplicationShortcut)
|
act_docs.setShortcutContext(Qt.ApplicationShortcut)
|
||||||
act_docs.triggered.connect(self._open_docs)
|
act_docs.triggered.connect(self._open_docs)
|
||||||
help_menu.addAction(act_docs)
|
help_menu.addAction(act_docs)
|
||||||
self.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.setShortcut("Ctrl+R")
|
||||||
act_bugs.setShortcutContext(Qt.ApplicationShortcut)
|
act_bugs.setShortcutContext(Qt.ApplicationShortcut)
|
||||||
act_bugs.triggered.connect(self._open_bugs)
|
act_bugs.triggered.connect(self._open_bugs)
|
||||||
|
|
@ -308,10 +309,10 @@ class MainWindow(QMainWindow):
|
||||||
ok = self.db.connect()
|
ok = self.db.connect()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if str(e) == "file is not a database":
|
if str(e) == "file is not a database":
|
||||||
error = "The key is probably incorrect."
|
error = strings._("db_key_incorrect")
|
||||||
else:
|
else:
|
||||||
error = str(e)
|
error = str(e)
|
||||||
QMessageBox.critical(self, "Database Error", error)
|
QMessageBox.critical(self, strings._("db_database_error"), error)
|
||||||
return False
|
return False
|
||||||
return ok
|
return ok
|
||||||
|
|
||||||
|
|
@ -320,11 +321,11 @@ class MainWindow(QMainWindow):
|
||||||
Prompt for the SQLCipher key.
|
Prompt for the SQLCipher key.
|
||||||
"""
|
"""
|
||||||
if first_time:
|
if first_time:
|
||||||
title = "Set an encryption key"
|
title = strings._("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!"
|
message = strings._("set_an_encryption_key_explanation")
|
||||||
else:
|
else:
|
||||||
title = "Unlock encrypted notebook"
|
title = strings._("unlock_encrypted_notebook")
|
||||||
message = "Enter your key to unlock the notebook"
|
message = strings._("unlock_encrypted_notebook_explanation")
|
||||||
while True:
|
while True:
|
||||||
dlg = KeyPrompt(self, title, message)
|
dlg = KeyPrompt(self, title, message)
|
||||||
if dlg.exec() != QDialog.Accepted:
|
if dlg.exec() != QDialog.Accepted:
|
||||||
|
|
@ -591,7 +592,7 @@ class MainWindow(QMainWindow):
|
||||||
clicked_date = self._date_from_calendar_pos(pos)
|
clicked_date = self._date_from_calendar_pos(pos)
|
||||||
|
|
||||||
menu = QMenu(self)
|
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))
|
action = menu.exec_(self.calendar.mapToGlobal(pos))
|
||||||
|
|
||||||
self._showing_context_menu = False
|
self._showing_context_menu = False
|
||||||
|
|
@ -678,7 +679,7 @@ class MainWindow(QMainWindow):
|
||||||
return
|
return
|
||||||
date_iso = editor.current_date.toString("yyyy-MM-dd")
|
date_iso = editor.current_date.toString("yyyy-MM-dd")
|
||||||
md = editor.to_markdown()
|
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):
|
def _on_text_changed(self):
|
||||||
self._dirty = True
|
self._dirty = True
|
||||||
|
|
@ -723,7 +724,7 @@ class MainWindow(QMainWindow):
|
||||||
self.db.save_new_version(
|
self.db.save_new_version(
|
||||||
yesterday_str,
|
yesterday_str,
|
||||||
modified_text,
|
modified_text,
|
||||||
"Unchecked checkbox items moved to next day",
|
strings._("unchecked_checkbox_items_moved_to_next_day"),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Join unchecked items into markdown format
|
# Join unchecked items into markdown format
|
||||||
|
|
@ -782,7 +783,7 @@ class MainWindow(QMainWindow):
|
||||||
from datetime import datetime as _dt
|
from datetime import datetime as _dt
|
||||||
|
|
||||||
self.statusBar().showMessage(
|
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):
|
def _save_current(self, explicit: bool = False):
|
||||||
|
|
@ -799,7 +800,7 @@ class MainWindow(QMainWindow):
|
||||||
return
|
return
|
||||||
note = dlg.note_text()
|
note = dlg.note_text()
|
||||||
else:
|
else:
|
||||||
note = "autosave"
|
note = strings._("autosave")
|
||||||
# Save the current editor's date
|
# Save the current editor's date
|
||||||
date_iso = self.editor.current_date.toString("yyyy-MM-dd")
|
date_iso = self.editor.current_date.toString("yyyy-MM-dd")
|
||||||
self._save_date(date_iso, explicit, note)
|
self._save_date(date_iso, explicit, note)
|
||||||
|
|
@ -965,9 +966,9 @@ class MainWindow(QMainWindow):
|
||||||
# Let the user pick one or many images
|
# Let the user pick one or many images
|
||||||
paths, _ = QFileDialog.getOpenFileNames(
|
paths, _ = QFileDialog.getOpenFileNames(
|
||||||
self,
|
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:
|
if not paths:
|
||||||
return
|
return
|
||||||
|
|
@ -990,6 +991,7 @@ class MainWindow(QMainWindow):
|
||||||
self.cfg.idle_minutes = getattr(new_cfg, "idle_minutes", self.cfg.idle_minutes)
|
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.theme = getattr(new_cfg, "theme", self.cfg.theme)
|
||||||
self.cfg.move_todos = getattr(new_cfg, "move_todos", self.cfg.move_todos)
|
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
|
# Persist once
|
||||||
save_db_config(self.cfg)
|
save_db_config(self.cfg)
|
||||||
|
|
@ -1002,7 +1004,9 @@ class MainWindow(QMainWindow):
|
||||||
self.db.close()
|
self.db.close()
|
||||||
if not self._prompt_for_key_until_valid(first_time=False):
|
if not self._prompt_for_key_until_valid(first_time=False):
|
||||||
QMessageBox.warning(
|
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
|
return
|
||||||
self._load_selected_date()
|
self._load_selected_date()
|
||||||
|
|
@ -1045,14 +1049,8 @@ class MainWindow(QMainWindow):
|
||||||
# ----------------- Export handler ----------------- #
|
# ----------------- Export handler ----------------- #
|
||||||
@Slot()
|
@Slot()
|
||||||
def _export(self):
|
def _export(self):
|
||||||
warning_title = "Unencrypted export"
|
warning_title = strings._("unencrypted_export")
|
||||||
warning_message = """
|
warning_message = strings._("unencrypted_export_warning")
|
||||||
Exporting the database will be unencrypted!
|
|
||||||
|
|
||||||
Are you sure you want to continue?
|
|
||||||
|
|
||||||
If you want an encrypted backup, choose Backup instead of Export.
|
|
||||||
"""
|
|
||||||
dlg = QMessageBox()
|
dlg = QMessageBox()
|
||||||
dlg.setWindowTitle(warning_title)
|
dlg.setWindowTitle(warning_title)
|
||||||
dlg.setText(warning_message)
|
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")
|
start_dir = os.path.join(os.path.expanduser("~"), "Documents")
|
||||||
filename, selected_filter = QFileDialog.getSaveFileName(
|
filename, selected_filter = QFileDialog.getSaveFileName(
|
||||||
self, "Export entries", start_dir, filters
|
self, strings._("export_entries"), start_dir, filters
|
||||||
)
|
)
|
||||||
if not filename:
|
if not filename:
|
||||||
return # user cancelled
|
return # user cancelled
|
||||||
|
|
@ -1106,11 +1104,15 @@ If you want an encrypted backup, choose Backup instead of Export.
|
||||||
elif selected_filter.startswith("SQL"):
|
elif selected_filter.startswith("SQL"):
|
||||||
self.db.export_sql(filename)
|
self.db.export_sql(filename)
|
||||||
else:
|
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:
|
except Exception as e:
|
||||||
QMessageBox.critical(self, "Export failed", str(e))
|
QMessageBox.critical(self, strings._("export_failed"), str(e))
|
||||||
|
|
||||||
# ----------------- Backup handler ----------------- #
|
# ----------------- Backup handler ----------------- #
|
||||||
@Slot()
|
@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"
|
os.path.expanduser("~"), "Documents", f"bouquin_backup_{now}.db"
|
||||||
)
|
)
|
||||||
filename, selected_filter = QFileDialog.getSaveFileName(
|
filename, selected_filter = QFileDialog.getSaveFileName(
|
||||||
self, "Backup encrypted notebook", start_dir, filters
|
self, strings._("backup_encrypted_notebook"), start_dir, filters
|
||||||
)
|
)
|
||||||
if not filename:
|
if not filename:
|
||||||
return # user cancelled
|
return # user cancelled
|
||||||
|
|
@ -1138,10 +1140,12 @@ If you want an encrypted backup, choose Backup instead of Export.
|
||||||
if selected_filter.startswith("SQL"):
|
if selected_filter.startswith("SQL"):
|
||||||
self.db.export_sqlcipher(filename)
|
self.db.export_sqlcipher(filename)
|
||||||
QMessageBox.information(
|
QMessageBox.information(
|
||||||
self, "Backup complete", f"Saved to:\n{filename}"
|
self,
|
||||||
|
strings._("backup_complete"),
|
||||||
|
strings._("saved_to") + f" {filename}",
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(self, "Backup failed", str(e))
|
QMessageBox.critical(self, strings._("backup_failed"), str(e))
|
||||||
|
|
||||||
# ----------------- Help handlers ----------------- #
|
# ----------------- Help handlers ----------------- #
|
||||||
|
|
||||||
|
|
@ -1150,7 +1154,9 @@ If you want an encrypted backup, choose Backup instead of Export.
|
||||||
url = QUrl.fromUserInput(url_str)
|
url = QUrl.fromUserInput(url_str)
|
||||||
if not QDesktopServices.openUrl(url):
|
if not QDesktopServices.openUrl(url):
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self, "Open Documentation", f"Couldn't open:\n{url.toDisplayString()}"
|
self,
|
||||||
|
strings._("documentation"),
|
||||||
|
strings._("couldnt_open") + url.toDisplayString(),
|
||||||
)
|
)
|
||||||
|
|
||||||
def _open_bugs(self):
|
def _open_bugs(self):
|
||||||
|
|
@ -1158,7 +1164,9 @@ If you want an encrypted backup, choose Backup instead of Export.
|
||||||
url = QUrl.fromUserInput(url_str)
|
url = QUrl.fromUserInput(url_str)
|
||||||
if not QDesktopServices.openUrl(url):
|
if not QDesktopServices.openUrl(url):
|
||||||
QMessageBox.warning(
|
QMessageBox.warning(
|
||||||
self, "Open Documentation", f"Couldn't open:\n{url.toDisplayString()}"
|
self,
|
||||||
|
strings._("report_a_bug"),
|
||||||
|
strings._("couldnt_open") + url.toDisplayString(),
|
||||||
)
|
)
|
||||||
|
|
||||||
# ----------------- Idle handlers ----------------- #
|
# ----------------- Idle handlers ----------------- #
|
||||||
|
|
@ -1219,7 +1227,7 @@ If you want an encrypted backup, choose Backup instead of Export.
|
||||||
try:
|
try:
|
||||||
ok = self._prompt_for_key_until_valid(first_time=False)
|
ok = self._prompt_for_key_until_valid(first_time=False)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(self, "Unlock failed", str(e))
|
QMessageBox.critical(self, strings._("unlock_failed"), str(e))
|
||||||
return
|
return
|
||||||
if ok:
|
if ok:
|
||||||
self._locked = False
|
self._locked = False
|
||||||
|
|
|
||||||
|
|
@ -10,24 +10,24 @@ from PySide6.QtWidgets import (
|
||||||
QDialogButtonBox,
|
QDialogButtonBox,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from . import strings
|
||||||
|
|
||||||
|
|
||||||
class SaveDialog(QDialog):
|
class SaveDialog(QDialog):
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
parent=None,
|
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.
|
Used for explicitly saving a new version of a page.
|
||||||
"""
|
"""
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle(title)
|
self.setWindowTitle(strings._("enter_a_name_for_this_version"))
|
||||||
v = QVBoxLayout(self)
|
v = QVBoxLayout(self)
|
||||||
v.addWidget(QLabel(message))
|
v.addWidget(QLabel(strings._("enter_a_name_for_this_version")))
|
||||||
self.note = QLineEdit()
|
self.note = QLineEdit()
|
||||||
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
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)
|
v.addWidget(self.note)
|
||||||
bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
bb = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
|
||||||
bb.accepted.connect(self.accept)
|
bb.accepted.connect(self.accept)
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ from PySide6.QtWidgets import (
|
||||||
QWidget,
|
QWidget,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from . import strings
|
||||||
|
|
||||||
Row = Tuple[str, str]
|
Row = Tuple[str, str]
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -30,7 +32,7 @@ class Search(QWidget):
|
||||||
self._db = db
|
self._db = db
|
||||||
|
|
||||||
self.search = QLineEdit()
|
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.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||||||
self.search.textChanged.connect(self._search)
|
self.search.textChanged.connect(self._search)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -25,8 +25,14 @@ def load_db_config() -> DBConfig:
|
||||||
idle = s.value("ui/idle_minutes", 15, type=int)
|
idle = s.value("ui/idle_minutes", 15, type=int)
|
||||||
theme = s.value("ui/theme", "system", type=str)
|
theme = s.value("ui/theme", "system", type=str)
|
||||||
move_todos = s.value("ui/move_todos", False, type=bool)
|
move_todos = s.value("ui/move_todos", False, type=bool)
|
||||||
|
locale = s.value("ui/locale", "en", type=str)
|
||||||
return DBConfig(
|
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/idle_minutes", str(cfg.idle_minutes))
|
||||||
s.setValue("ui/theme", str(cfg.theme))
|
s.setValue("ui/theme", str(cfg.theme))
|
||||||
s.setValue("ui/move_todos", str(cfg.move_todos))
|
s.setValue("ui/move_todos", str(cfg.move_todos))
|
||||||
|
s.setValue("ui/locale", str(cfg.locale))
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QCheckBox,
|
QCheckBox,
|
||||||
|
QComboBox,
|
||||||
QDialog,
|
QDialog,
|
||||||
QFormLayout,
|
QFormLayout,
|
||||||
QFrame,
|
QFrame,
|
||||||
|
|
@ -30,11 +31,13 @@ from .settings import load_db_config, save_db_config
|
||||||
from .theme import Theme
|
from .theme import Theme
|
||||||
from .key_prompt import KeyPrompt
|
from .key_prompt import KeyPrompt
|
||||||
|
|
||||||
|
from . import strings
|
||||||
|
|
||||||
|
|
||||||
class SettingsDialog(QDialog):
|
class SettingsDialog(QDialog):
|
||||||
def __init__(self, cfg: DBConfig, db: DBManager, parent=None):
|
def __init__(self, cfg: DBConfig, db: DBManager, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setWindowTitle("Settings")
|
self.setWindowTitle(strings._("settings"))
|
||||||
self._cfg = DBConfig(path=cfg.path, key="")
|
self._cfg = DBConfig(path=cfg.path, key="")
|
||||||
self._db = db
|
self._db = db
|
||||||
self.key = ""
|
self.key = ""
|
||||||
|
|
@ -47,12 +50,12 @@ class SettingsDialog(QDialog):
|
||||||
current_settings = load_db_config()
|
current_settings = load_db_config()
|
||||||
|
|
||||||
# Add theme selection
|
# Add theme selection
|
||||||
theme_group = QGroupBox("Theme")
|
theme_group = QGroupBox(strings._("theme"))
|
||||||
theme_layout = QVBoxLayout(theme_group)
|
theme_layout = QVBoxLayout(theme_group)
|
||||||
|
|
||||||
self.theme_system = QRadioButton("System")
|
self.theme_system = QRadioButton(strings._("system"))
|
||||||
self.theme_light = QRadioButton("Light")
|
self.theme_light = QRadioButton(strings._("light"))
|
||||||
self.theme_dark = QRadioButton("Dark")
|
self.theme_dark = QRadioButton(strings._("dark"))
|
||||||
|
|
||||||
# Load current theme from settings
|
# Load current theme from settings
|
||||||
current_theme = current_settings.theme
|
current_theme = current_settings.theme
|
||||||
|
|
@ -69,12 +72,37 @@ class SettingsDialog(QDialog):
|
||||||
|
|
||||||
form.addRow(theme_group)
|
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
|
# Add Behaviour
|
||||||
behaviour_group = QGroupBox("Behaviour")
|
behaviour_group = QGroupBox(strings._("behaviour"))
|
||||||
behaviour_layout = QVBoxLayout(behaviour_group)
|
behaviour_layout = QVBoxLayout(behaviour_group)
|
||||||
|
|
||||||
self.move_todos = QCheckBox(
|
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.setChecked(current_settings.move_todos)
|
||||||
self.move_todos.setCursor(Qt.PointingHandCursor)
|
self.move_todos.setCursor(Qt.PointingHandCursor)
|
||||||
|
|
@ -84,7 +112,7 @@ class SettingsDialog(QDialog):
|
||||||
|
|
||||||
self.path_edit = QLineEdit(str(self._cfg.path))
|
self.path_edit = QLineEdit(str(self._cfg.path))
|
||||||
self.path_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
|
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.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||||
browse_btn.clicked.connect(self._browse)
|
browse_btn.clicked.connect(self._browse)
|
||||||
path_row = QWidget()
|
path_row = QWidget()
|
||||||
|
|
@ -94,16 +122,16 @@ class SettingsDialog(QDialog):
|
||||||
h.addWidget(browse_btn, 0)
|
h.addWidget(browse_btn, 0)
|
||||||
h.setStretch(0, 1)
|
h.setStretch(0, 1)
|
||||||
h.setStretch(1, 0)
|
h.setStretch(1, 0)
|
||||||
form.addRow("Database path", path_row)
|
form.addRow(strings._("database_path"), path_row)
|
||||||
|
|
||||||
# Encryption settings
|
# Encryption settings
|
||||||
enc_group = QGroupBox("Encryption")
|
enc_group = QGroupBox(strings._("encryption"))
|
||||||
enc = QVBoxLayout(enc_group)
|
enc = QVBoxLayout(enc_group)
|
||||||
enc.setContentsMargins(12, 8, 12, 12)
|
enc.setContentsMargins(12, 8, 12, 12)
|
||||||
enc.setSpacing(6)
|
enc.setSpacing(6)
|
||||||
|
|
||||||
# Checkbox to remember key
|
# 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.key = current_settings.key or ""
|
||||||
self.save_key_btn.setChecked(bool(self.key))
|
self.save_key_btn.setChecked(bool(self.key))
|
||||||
self.save_key_btn.setCursor(Qt.PointingHandCursor)
|
self.save_key_btn.setCursor(Qt.PointingHandCursor)
|
||||||
|
|
@ -111,10 +139,7 @@ class SettingsDialog(QDialog):
|
||||||
enc.addWidget(self.save_key_btn, 0, Qt.AlignLeft)
|
enc.addWidget(self.save_key_btn, 0, Qt.AlignLeft)
|
||||||
|
|
||||||
# Explanation for remembering key
|
# Explanation for remembering key
|
||||||
self.save_key_label = QLabel(
|
self.save_key_label = QLabel(strings._("save_key_warning"))
|
||||||
"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.setWordWrap(True)
|
self.save_key_label.setWordWrap(True)
|
||||||
self.save_key_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
self.save_key_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||||
# make it look secondary
|
# make it look secondary
|
||||||
|
|
@ -133,7 +158,7 @@ class SettingsDialog(QDialog):
|
||||||
enc.addWidget(line)
|
enc.addWidget(line)
|
||||||
|
|
||||||
# Change key button
|
# 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.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||||
self.rekey_btn.clicked.connect(self._change_key)
|
self.rekey_btn.clicked.connect(self._change_key)
|
||||||
|
|
||||||
|
|
@ -142,7 +167,7 @@ class SettingsDialog(QDialog):
|
||||||
form.addRow(enc_group)
|
form.addRow(enc_group)
|
||||||
|
|
||||||
# Privacy settings
|
# Privacy settings
|
||||||
priv_group = QGroupBox("Lock screen when idle")
|
priv_group = QGroupBox(strings._("lock_screen_when_idle"))
|
||||||
priv = QVBoxLayout(priv_group)
|
priv = QVBoxLayout(priv_group)
|
||||||
priv.setContentsMargins(12, 8, 12, 12)
|
priv.setContentsMargins(12, 8, 12, 12)
|
||||||
priv.setSpacing(6)
|
priv.setSpacing(6)
|
||||||
|
|
@ -152,15 +177,11 @@ class SettingsDialog(QDialog):
|
||||||
self.idle_spin.setSingleStep(1)
|
self.idle_spin.setSingleStep(1)
|
||||||
self.idle_spin.setAccelerated(True)
|
self.idle_spin.setAccelerated(True)
|
||||||
self.idle_spin.setSuffix(" min")
|
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))
|
self.idle_spin.setValue(getattr(cfg, "idle_minutes", 15))
|
||||||
priv.addWidget(self.idle_spin, 0, Qt.AlignLeft)
|
priv.addWidget(self.idle_spin, 0, Qt.AlignLeft)
|
||||||
# Explanation for idle option (autolock)
|
# Explanation for idle option (autolock)
|
||||||
self.idle_spin_label = QLabel(
|
self.idle_spin_label = QLabel(strings._("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. "
|
|
||||||
"Set to 0 (never) to never lock."
|
|
||||||
)
|
|
||||||
self.idle_spin_label.setWordWrap(True)
|
self.idle_spin_label.setWordWrap(True)
|
||||||
self.idle_spin_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
self.idle_spin_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||||
# make it look secondary
|
# make it look secondary
|
||||||
|
|
@ -176,21 +197,19 @@ class SettingsDialog(QDialog):
|
||||||
form.addRow(priv_group)
|
form.addRow(priv_group)
|
||||||
|
|
||||||
# Maintenance settings
|
# Maintenance settings
|
||||||
maint_group = QGroupBox("Database maintenance")
|
maint_group = QGroupBox(strings._("database_maintenance"))
|
||||||
maint = QVBoxLayout(maint_group)
|
maint = QVBoxLayout(maint_group)
|
||||||
maint.setContentsMargins(12, 8, 12, 12)
|
maint.setContentsMargins(12, 8, 12, 12)
|
||||||
maint.setSpacing(6)
|
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.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||||
self.compact_btn.clicked.connect(self._compact_btn_clicked)
|
self.compact_btn.clicked.connect(self._compact_btn_clicked)
|
||||||
|
|
||||||
maint.addWidget(self.compact_btn, 0, Qt.AlignLeft)
|
maint.addWidget(self.compact_btn, 0, Qt.AlignLeft)
|
||||||
|
|
||||||
# Explanation for compating button
|
# Explanation for compacting button
|
||||||
self.compact_label = QLabel(
|
self.compact_label = QLabel(strings._("database_compact_explanation"))
|
||||||
"Compacting runs VACUUM on the database. This can help reduce its size."
|
|
||||||
)
|
|
||||||
self.compact_label.setWordWrap(True)
|
self.compact_label.setWordWrap(True)
|
||||||
self.compact_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
self.compact_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum)
|
||||||
# make it look secondary
|
# make it look secondary
|
||||||
|
|
@ -220,9 +239,9 @@ class SettingsDialog(QDialog):
|
||||||
def _browse(self):
|
def _browse(self):
|
||||||
p, _ = QFileDialog.getSaveFileName(
|
p, _ = QFileDialog.getSaveFileName(
|
||||||
self,
|
self,
|
||||||
"Choose database file",
|
strings._("database_path"),
|
||||||
self.path_edit.text(),
|
self.path_edit.text(),
|
||||||
"DB Files (*.db);;All Files (*)",
|
"(*.db);;(*)",
|
||||||
)
|
)
|
||||||
if p:
|
if p:
|
||||||
self.path_edit.setText(p)
|
self.path_edit.setText(p)
|
||||||
|
|
@ -244,6 +263,7 @@ class SettingsDialog(QDialog):
|
||||||
idle_minutes=self.idle_spin.value(),
|
idle_minutes=self.idle_spin.value(),
|
||||||
theme=selected_theme.value,
|
theme=selected_theme.value,
|
||||||
move_todos=self.move_todos.isChecked(),
|
move_todos=self.move_todos.isChecked(),
|
||||||
|
locale=self.locale_combobox.currentText(),
|
||||||
)
|
)
|
||||||
|
|
||||||
save_db_config(self._cfg)
|
save_db_config(self._cfg)
|
||||||
|
|
@ -251,27 +271,39 @@ class SettingsDialog(QDialog):
|
||||||
self.accept()
|
self.accept()
|
||||||
|
|
||||||
def _change_key(self):
|
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:
|
if p1.exec() != QDialog.Accepted:
|
||||||
return
|
return
|
||||||
new_key = p1.key()
|
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:
|
if p2.exec() != QDialog.Accepted:
|
||||||
return
|
return
|
||||||
if new_key != p2.key():
|
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
|
return
|
||||||
if not new_key:
|
if not new_key:
|
||||||
QMessageBox.warning(self, "Empty key", "Key cannot be empty.")
|
QMessageBox.warning(
|
||||||
|
self, strings._("empty_key"), strings._("empty_key_explanation")
|
||||||
|
)
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
self.key = new_key
|
self.key = new_key
|
||||||
self._db.rekey(new_key)
|
self._db.rekey(new_key)
|
||||||
QMessageBox.information(
|
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:
|
except Exception as e:
|
||||||
QMessageBox.critical(self, "Error", e)
|
QMessageBox.critical(self, strings._("error"), e)
|
||||||
|
|
||||||
@Slot(bool)
|
@Slot(bool)
|
||||||
def _save_key_btn_clicked(self, checked: bool):
|
def _save_key_btn_clicked(self, checked: bool):
|
||||||
|
|
@ -279,7 +311,9 @@ class SettingsDialog(QDialog):
|
||||||
if checked:
|
if checked:
|
||||||
if not self.key:
|
if not self.key:
|
||||||
p1 = KeyPrompt(
|
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:
|
if p1.exec() != QDialog.Accepted:
|
||||||
self.save_key_btn.blockSignals(True)
|
self.save_key_btn.blockSignals(True)
|
||||||
|
|
@ -292,9 +326,11 @@ class SettingsDialog(QDialog):
|
||||||
def _compact_btn_clicked(self):
|
def _compact_btn_clicked(self):
|
||||||
try:
|
try:
|
||||||
self._db.compact()
|
self._db.compact()
|
||||||
QMessageBox.information(self, "Success", "Database compacted successfully!")
|
QMessageBox.information(
|
||||||
|
self, strings._("success"), strings._("database_compacted_successfully")
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
QMessageBox.critical(self, "Error", e)
|
QMessageBox.critical(self, strings._("error"), e)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def config(self) -> DBConfig:
|
def config(self) -> DBConfig:
|
||||||
|
|
|
||||||
42
bouquin/strings.py
Normal file
42
bouquin/strings.py
Normal 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
|
||||||
|
|
@ -4,6 +4,8 @@ from PySide6.QtCore import Signal, Qt
|
||||||
from PySide6.QtGui import QAction, QKeySequence, QFont, QFontDatabase, QActionGroup
|
from PySide6.QtGui import QAction, QKeySequence, QFont, QFontDatabase, QActionGroup
|
||||||
from PySide6.QtWidgets import QToolBar
|
from PySide6.QtWidgets import QToolBar
|
||||||
|
|
||||||
|
from . import strings
|
||||||
|
|
||||||
|
|
||||||
class ToolBar(QToolBar):
|
class ToolBar(QToolBar):
|
||||||
boldRequested = Signal()
|
boldRequested = Signal()
|
||||||
|
|
@ -18,79 +20,79 @@ class ToolBar(QToolBar):
|
||||||
insertImageRequested = Signal()
|
insertImageRequested = Signal()
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__("Format", parent)
|
super().__init__(strings._("toolbar_format"), parent)
|
||||||
self.setObjectName("Format")
|
self.setObjectName(strings._("toolbar_format"))
|
||||||
self.setToolButtonStyle(Qt.ToolButtonTextOnly)
|
self.setToolButtonStyle(Qt.ToolButtonTextOnly)
|
||||||
self._build_actions()
|
self._build_actions()
|
||||||
self._apply_toolbar_styles()
|
self._apply_toolbar_styles()
|
||||||
|
|
||||||
def _build_actions(self):
|
def _build_actions(self):
|
||||||
self.actBold = QAction("B", self)
|
self.actBold = QAction("B", self)
|
||||||
self.actBold.setToolTip("Bold")
|
self.actBold.setToolTip(strings._("toolbar_bold"))
|
||||||
self.actBold.setCheckable(True)
|
self.actBold.setCheckable(True)
|
||||||
self.actBold.setShortcut(QKeySequence.Bold)
|
self.actBold.setShortcut(QKeySequence.Bold)
|
||||||
self.actBold.triggered.connect(self.boldRequested)
|
self.actBold.triggered.connect(self.boldRequested)
|
||||||
|
|
||||||
self.actItalic = QAction("I", self)
|
self.actItalic = QAction("I", self)
|
||||||
self.actItalic.setToolTip("Italic")
|
self.actItalic.setToolTip(strings._("toolbar_italic"))
|
||||||
self.actItalic.setCheckable(True)
|
self.actItalic.setCheckable(True)
|
||||||
self.actItalic.setShortcut(QKeySequence.Italic)
|
self.actItalic.setShortcut(QKeySequence.Italic)
|
||||||
self.actItalic.triggered.connect(self.italicRequested)
|
self.actItalic.triggered.connect(self.italicRequested)
|
||||||
|
|
||||||
self.actStrike = QAction("S", self)
|
self.actStrike = QAction("S", self)
|
||||||
self.actStrike.setToolTip("Strikethrough")
|
self.actStrike.setToolTip(strings._("toolbar_strikethrough"))
|
||||||
self.actStrike.setCheckable(True)
|
self.actStrike.setCheckable(True)
|
||||||
self.actStrike.setShortcut("Ctrl+-")
|
self.actStrike.setShortcut("Ctrl+-")
|
||||||
self.actStrike.triggered.connect(self.strikeRequested)
|
self.actStrike.triggered.connect(self.strikeRequested)
|
||||||
|
|
||||||
self.actCode = QAction("</>", self)
|
self.actCode = QAction("</>", self)
|
||||||
self.actCode.setToolTip("Code block")
|
self.actCode.setToolTip(strings._("toolbar_code_block"))
|
||||||
self.actCode.setShortcut("Ctrl+`")
|
self.actCode.setShortcut("Ctrl+`")
|
||||||
self.actCode.triggered.connect(self.codeRequested)
|
self.actCode.triggered.connect(self.codeRequested)
|
||||||
|
|
||||||
# Headings
|
# Headings
|
||||||
self.actH1 = QAction("H1", self)
|
self.actH1 = QAction("H1", self)
|
||||||
self.actH1.setToolTip("Heading 1")
|
self.actH1.setToolTip(strings._("toolbar_heading") + " 1")
|
||||||
self.actH1.setCheckable(True)
|
self.actH1.setCheckable(True)
|
||||||
self.actH1.setShortcut("Ctrl+1")
|
self.actH1.setShortcut("Ctrl+1")
|
||||||
self.actH1.triggered.connect(lambda: self.headingRequested.emit(24))
|
self.actH1.triggered.connect(lambda: self.headingRequested.emit(24))
|
||||||
self.actH2 = QAction("H2", self)
|
self.actH2 = QAction("H2", self)
|
||||||
self.actH2.setToolTip("Heading 2")
|
self.actH2.setToolTip(strings._("toolbar_heading") + " 2")
|
||||||
self.actH2.setCheckable(True)
|
self.actH2.setCheckable(True)
|
||||||
self.actH2.setShortcut("Ctrl+2")
|
self.actH2.setShortcut("Ctrl+2")
|
||||||
self.actH2.triggered.connect(lambda: self.headingRequested.emit(18))
|
self.actH2.triggered.connect(lambda: self.headingRequested.emit(18))
|
||||||
self.actH3 = QAction("H3", self)
|
self.actH3 = QAction("H3", self)
|
||||||
self.actH3.setToolTip("Heading 3")
|
self.actH3.setToolTip(strings._("toolbar_heading") + " 3")
|
||||||
self.actH3.setCheckable(True)
|
self.actH3.setCheckable(True)
|
||||||
self.actH3.setShortcut("Ctrl+3")
|
self.actH3.setShortcut("Ctrl+3")
|
||||||
self.actH3.triggered.connect(lambda: self.headingRequested.emit(14))
|
self.actH3.triggered.connect(lambda: self.headingRequested.emit(14))
|
||||||
self.actNormal = QAction("N", self)
|
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.setCheckable(True)
|
||||||
self.actNormal.setShortcut("Ctrl+N")
|
self.actNormal.setShortcut("Ctrl+N")
|
||||||
self.actNormal.triggered.connect(lambda: self.headingRequested.emit(0))
|
self.actNormal.triggered.connect(lambda: self.headingRequested.emit(0))
|
||||||
|
|
||||||
# Lists
|
# Lists
|
||||||
self.actBullets = QAction("•", self)
|
self.actBullets = QAction("•", self)
|
||||||
self.actBullets.setToolTip("Bulleted list")
|
self.actBullets.setToolTip(strings._("toolbar_bulleted_list"))
|
||||||
self.actBullets.setCheckable(True)
|
self.actBullets.setCheckable(True)
|
||||||
self.actBullets.triggered.connect(self.bulletsRequested)
|
self.actBullets.triggered.connect(self.bulletsRequested)
|
||||||
self.actNumbers = QAction("1.", self)
|
self.actNumbers = QAction("1.", self)
|
||||||
self.actNumbers.setToolTip("Numbered list")
|
self.actNumbers.setToolTip(strings._("toolbar_numbered_list"))
|
||||||
self.actNumbers.setCheckable(True)
|
self.actNumbers.setCheckable(True)
|
||||||
self.actNumbers.triggered.connect(self.numbersRequested)
|
self.actNumbers.triggered.connect(self.numbersRequested)
|
||||||
self.actCheckboxes = QAction("☐", self)
|
self.actCheckboxes = QAction("☐", self)
|
||||||
self.actCheckboxes.setToolTip("Toggle checkboxes")
|
self.actCheckboxes.setToolTip(strings._("toolbar_toggle_checkboxes"))
|
||||||
self.actCheckboxes.triggered.connect(self.checkboxesRequested)
|
self.actCheckboxes.triggered.connect(self.checkboxesRequested)
|
||||||
|
|
||||||
# Images
|
# Images
|
||||||
self.actInsertImg = QAction("Image", self)
|
self.actInsertImg = QAction(strings._("images"), self)
|
||||||
self.actInsertImg.setToolTip("Insert image")
|
self.actInsertImg.setToolTip(strings._("insert_images"))
|
||||||
self.actInsertImg.setShortcut("Ctrl+Shift+I")
|
self.actInsertImg.setShortcut("Ctrl+Shift+I")
|
||||||
self.actInsertImg.triggered.connect(self.insertImageRequested)
|
self.actInsertImg.triggered.connect(self.insertImageRequested)
|
||||||
|
|
||||||
# History button
|
# History button
|
||||||
self.actHistory = QAction("History", self)
|
self.actHistory = QAction(strings._("history"), self)
|
||||||
self.actHistory.triggered.connect(self.historyRequested)
|
self.actHistory.triggered.connect(self.historyRequested)
|
||||||
|
|
||||||
# Set exclusive buttons in QActionGroups
|
# Set exclusive buttons in QActionGroups
|
||||||
|
|
@ -151,7 +153,7 @@ class ToolBar(QToolBar):
|
||||||
self._style_letter_button(self.actNumbers, "1.")
|
self._style_letter_button(self.actNumbers, "1.")
|
||||||
|
|
||||||
# History
|
# History
|
||||||
self._style_letter_button(self.actHistory, "View History")
|
self._style_letter_button(self.actHistory, strings._("view_history"))
|
||||||
|
|
||||||
def _style_letter_button(
|
def _style_letter_button(
|
||||||
self,
|
self,
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "bouquin"
|
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."
|
description = "Bouquin is a simple, opinionated notebook application written in Python, PyQt and SQLCipher."
|
||||||
authors = ["Miguel Jacq <mig@mig5.net>"]
|
authors = ["Miguel Jacq <mig@mig5.net>"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
license = "GPL-3.0-or-later"
|
license = "GPL-3.0-or-later"
|
||||||
repository = "https://git.mig5.net/mig5/bouquin"
|
repository = "https://git.mig5.net/mig5/bouquin"
|
||||||
|
packages = [{ include = "bouquin" }]
|
||||||
|
include = ["bouquin/locales/*.json"]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = ">=3.9,<3.14"
|
python = ">=3.9,<3.14"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue