diff --git a/CHANGELOG.md b/CHANGELOG.md index dab732b..b62cb6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ * Fix a few small matters identified with tests * Make locales dynamically detected from the locales dir rather than hardcoded * Add version information in the navigation + * Increase line spacing between lines (except for code blocks) + * Add Italian translations (thanks @mdaleo404) # 0.2.1.8 diff --git a/bouquin/locales/it.json b/bouquin/locales/it.json new file mode 100644 index 0000000..5c9f8c0 --- /dev/null +++ b/bouquin/locales/it.json @@ -0,0 +1,114 @@ +{ + "db_sqlcipher_integrity_check_failed": "Controllo di integrità SQLCipher fallito", + "db_issues_reported": "problema/i segnalato/i", + "db_reopen_failed_after_rekey": "Riapertura fallita dopo il cambio chiave", + "db_version_id_does_not_belong_to_the_given_date": "version_id non appartiene alla data indicata", + "db_key_incorrect": "La chiave è probabilmente errata", + "db_database_error": "Errore del database", + "database_path": "Percorso del database", + "database_maintenance": "Manutenzione del database", + "database_compact": "Compatta il database", + "database_compact_explanation": "La compattazione esegue VACUUM sul database. Può aiutare a ridurne le dimensioni.", + "database_compacted_successfully": "Database compattato con successo!", + "encryption": "Crittografia", + "remember_key": "Ricorda la chiave", + "change_encryption_key": "Cambia chiave di crittografia", + "enter_a_new_encryption_key": "Inserisci una nuova chiave di crittografia", + "reenter_the_new_key": "Reinserisci la nuova chiave", + "key_mismatch": "Le chiavi non corrispondono", + "key_mismatch_explanation": "Le due chiavi inserite non corrispondono.", + "empty_key": "Chiave vuota", + "empty_key_explanation": "La chiave non può essere vuota.", + "key_changed": "Chiave cambiata", + "key_changed_explanation": "Il blocco note è stato criptato nuovamente con la nuova chiave!", + "error": "Errore", + "success": "Successo", + "close": "Chiudi", + "find": "Trova", + "file": "File", + "locale": "Lingua", + "locale_restart": "Per favore riavvia l'applicazione per caricare la nuova lingua.", + "settings": "Impostazioni", + "theme": "Tema", + "system": "Sistema", + "light": "Chiaro", + "dark": "Scuro", + "behaviour": "Comportamento", + "never": "Mai", + "browse": "Sfoglia", + "previous": "Precedente", + "previous_day": "Giorno precedente", + "next": "Successivo", + "next_day": "Giorno successivo", + "today": "Oggi", + "show": "Mostra", + "history": "Cronologia", + "view_history": "Visualizza cronologia", + "export": "Esporta", + "export_accessible_flag": "&Esporta", + "export_entries": "Esporta voci", + "export_complete": "Esportazione completata", + "export_failed": "Esportazione fallita", + "backup": "Backup", + "backup_complete": "Backup completato", + "backup_failed": "Backup fallito", + "quit": "Esci", + "help": "Aiuto", + "saved": "Salvato", + "saved_to": "Salvato in", + "documentation": "Documentazione", + "couldnt_open": "Impossibile aprire", + "report_a_bug": "Segnala un bug", + "version": "Versione", + "navigate": "Naviga", + "current": "corrente", + "selected": "selezionato", + "find_on_page": "Trova nella pagina", + "find_next": "Trova successivo", + "find_previous": "Trova precedente", + "find_bar_type_to_search": "Digita per cercare", + "find_bar_match_case": "Distingui maiuscole/minuscole", + "history_dialog_preview": "Anteprima", + "history_dialog_diff": "Differenze", + "history_dialog_revert_to_selected": "Ripristina alla versione selezionata", + "history_dialog_revert_failed": "Ripristino fallito", + "key_prompt_enter_key": "Inserisci la chiave", + "lock_overlay_locked_due_to_inactivity": "Bloccato per inattività", + "lock_overlay_unlock": "Sblocca", + "main_window_ready": "Pronto", + "main_window_save_a_version": "Salva una versione", + "main_window_settings_accessible_flag": "Impo&stazioni", + "set_an_encryption_key": "Imposta una chiave di crittografia", + "set_an_encryption_key_explanation": "Bouquin cripta i tuoi dati.\n\nCrea una passphrase sicura per criptare il blocco note.\n\nPuoi sempre cambiarla in seguito!", + "unlock_encrypted_notebook": "Sblocca il blocco note criptato", + "unlock_encrypted_notebook_explanation": "Inserisci la chiave per sbloccare il blocco note", + "open_in_new_tab": "Apri in una nuova scheda", + "autosave": "salvataggio automatico", + "unchecked_checkbox_items_moved_to_next_day": "Le caselle non spuntate sono state spostate al giorno successivo", + "move_yesterdays_unchecked_todos_to_today_on_startup": "Sposta i TODO non completati di ieri a oggi all'avvio", + "insert_images": "Inserisci immagini", + "images": "Immagini", + "reopen_failed": "Riapertura fallita", + "unlock_failed": "Sblocco fallito", + "could_not_unlock_database_at_new_path": "Impossibile sbloccare il database nel nuovo percorso.", + "unencrypted_export": "Esportazione non criptata", + "unencrypted_export_warning": "L'esportazione del database sarà non criptata!\nVuoi davvero continuare?\nSe desideri un backup criptato, scegli Backup invece di Esporta.", + "unrecognised_extension": "Estensione non riconosciuta!", + "backup_encrypted_notebook": "Backup del blocco note criptato", + "enter_a_name_for_this_version": "Inserisci un nome per questa versione", + "new_version_i_saved_at": "Nuova versione salvata il", + "save_key_warning": "Se non vuoi che ti venga richiesta la chiave di crittografia, seleziona questa opzione per ricordarla.\nATTENZIONE: la chiave viene salvata sul disco e potrebbe essere recuperabile se il disco fosse compromesso.", + "lock_screen_when_idle": "Blocca lo schermo quando inattivo", + "autolock_explanation": "Bouquin bloccherà automaticamente il blocco note dopo questo intervallo di tempo, dopodiché sarà necessario reinserire la chiave per sbloccarlo.\nImposta a 0 (mai) per non bloccarlo mai.", + "search_for_notes_here": "Cerca note qui", + "toolbar_format": "Formato", + "toolbar_bold": "Grassetto", + "toolbar_italic": "Corsivo", + "toolbar_strikethrough": "Barrato", + "toolbar_normal_paragraph_text": "Testo normale", + "toolbar_bulleted_list": "Elenco puntato", + "toolbar_numbered_list": "Elenco numerato", + "toolbar_code_block": "Blocco di codice", + "toolbar_heading": "Titolo", + "toolbar_toggle_checkboxes": "Attiva/disattiva caselle di controllo" +} diff --git a/bouquin/markdown_editor.py b/bouquin/markdown_editor.py index a38ca1f..bd2bb98 100644 --- a/bouquin/markdown_editor.py +++ b/bouquin/markdown_editor.py @@ -12,6 +12,7 @@ from PySide6.QtGui import ( QTextCursor, QTextDocument, QTextFormat, + QTextBlockFormat, QTextImageFormat, ) from PySide6.QtCore import Qt, QRect, QTimer @@ -43,6 +44,8 @@ class MarkdownEditor(QTextEdit): font.setPointSize(10) self.setFont(font) + self._apply_line_spacing() # 1.25× initial spacing + # Checkbox characters (Unicode for display, markdown for storage) self._CHECK_UNCHECKED_DISPLAY = "☐" self._CHECK_CHECKED_DISPLAY = "☑" @@ -73,6 +76,8 @@ class MarkdownEditor(QTextEdit): # reattach the highlighter to the new document if hasattr(self, "highlighter") and self.highlighter: self.highlighter.setDocument(self.document()) + self._apply_line_spacing() + self._apply_code_block_spacing() QTimer.singleShot(0, self._update_code_block_row_backgrounds) def showEvent(self, e): @@ -140,9 +145,10 @@ class MarkdownEditor(QTextEdit): def _update_code_block_row_backgrounds(self): """Paint a full-width background for each line that is in a fenced code block.""" doc = self.document() - sels = [] + if doc is None: + return - # Use the same bg color as the highlighter's code block + sels = [] bg_brush = self.highlighter.code_block_format.background() inside = False @@ -158,16 +164,12 @@ class MarkdownEditor(QTextEdit): fmt = QTextCharFormat() fmt.setBackground(bg_brush) fmt.setProperty(QTextFormat.FullWidthSelection, True) - # mark so we can merge with other selections safely fmt.setProperty(QTextFormat.UserProperty, "codeblock_bg") sel.format = fmt cur = QTextCursor(doc) - cur.setPosition( - block.position() - ) # collapsed cursor = whole line when FullWidthSelection + cur.setPosition(block.position()) sel.cursor = cur - sels.append(sel) if is_fence: @@ -182,6 +184,60 @@ class MarkdownEditor(QTextEdit): ] self.setExtraSelections(others + sels) + def _apply_line_spacing(self, height: float = 125.0): + """Apply proportional line spacing to the whole document.""" + doc = self.document() + if doc is None: + return + + cursor = QTextCursor(doc) + cursor.beginEditBlock() + cursor.select(QTextCursor.Document) + + fmt = QTextBlockFormat() + fmt.setLineHeight( + height, # 125.0 = 1.25× + QTextBlockFormat.LineHeightTypes.ProportionalHeight.value, + ) + cursor.mergeBlockFormat(fmt) + cursor.endEditBlock() + + def _apply_code_block_spacing(self): + """ + Make all fenced code-block lines (including ``` fences) single-spaced. + Call this AFTER _apply_line_spacing(). + """ + doc = self.document() + if doc is None: + return + + cursor = QTextCursor(doc) + cursor.beginEditBlock() + + inside = False + block = doc.begin() + while block.isValid(): + text = block.text() + stripped = text.strip() + is_fence = stripped.startswith("```") + is_code_line = is_fence or inside + + if is_code_line: + fmt = block.blockFormat() + fmt.setLineHeight( + 0.0, + QTextBlockFormat.LineHeightTypes.SingleHeight.value, + ) + cursor.setPosition(block.position()) + cursor.setBlockFormat(fmt) + + if is_fence: + inside = not inside + + block = block.next() + + cursor.endEditBlock() + def to_markdown(self) -> str: """Export current content as markdown.""" # First, extract any embedded images and convert to markdown @@ -255,6 +311,9 @@ class MarkdownEditor(QTextEdit): finally: self._updating = False + self._apply_line_spacing() + self._apply_code_block_spacing() + # Render any embedded images self._render_images() @@ -452,9 +511,33 @@ class MarkdownEditor(QTextEdit): block_state = current_block.userState() - # If current line is opening code fence, or we're inside a code block - if current_line.strip().startswith("```") or block_state == 1: - # Just insert a regular newline - the highlighter will format it as code + stripped = current_line.strip() + is_fence_line = stripped.startswith("```") + + if is_fence_line: + # Work out if this fence is closing (inside block before it) + inside_before = self._is_inside_code_block(current_block.previous()) + + # Insert the newline as usual + super().keyPressEvent(event) + + if inside_before: + # We were on the *closing* fence; the new line is outside the block. + # Give that new block normal 1.25× spacing. + new_block = self.textCursor().block() + fmt = new_block.blockFormat() + fmt.setLineHeight( + 125.0, + QTextBlockFormat.LineHeightTypes.ProportionalHeight.value, + ) + cur2 = self.textCursor() + cur2.setBlockFormat(fmt) + self.setTextCursor(cur2) + + return + + # Inside a code block (but not on a fence): newline stays code-style + if block_state == 1: super().keyPressEvent(event) return @@ -646,6 +729,9 @@ class MarkdownEditor(QTextEdit): c.insertText(f"```\n{selected.rstrip()}\n```\n") if hasattr(self, "_update_code_block_row_backgrounds"): self._update_code_block_row_backgrounds() + # tighten spacing for the new code block + self._apply_code_block_spacing() + self.setFocus() return @@ -710,6 +796,10 @@ class MarkdownEditor(QTextEdit): if hasattr(self, "_update_code_block_row_backgrounds"): self._update_code_block_row_backgrounds() + + # tighten spacing for the new code block + self._apply_code_block_spacing() + self.setFocus() def apply_heading(self, size: int): diff --git a/tests/test_strings.py b/tests/test_strings.py index 5aa79c9..ec2c445 100644 --- a/tests/test_strings.py +++ b/tests/test_strings.py @@ -5,3 +5,13 @@ def test_load_strings_uses_system_locale_and_fallback(): # pass a bogus locale to trigger fallback-to-default strings.load_strings("zz") assert strings._("next") # key exists in base translations + + +def test_load_strings_french(): + strings.load_strings("fr") + assert strings._("today") == "Aujourd'hui" # translation exists in French + + +def test_load_strings_italian(): + strings.load_strings("it") + assert strings._("today") == "Oggi" # translation exists in Italian