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
|
||||
|
||||
* 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 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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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
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.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)
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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
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.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,
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue