From 793d758377ee089b7192f2c113b1177bf7b52c28 Mon Sep 17 00:00:00 2001 From: "Christopher T. Johnson" Date: Wed, 14 Feb 2024 10:02:27 -0500 Subject: [PATCH] I18N Code Work done. --- lib/books.py | 6 +- lib/person.py | 45 +- lib/preferences.py | 58 +- lib/read.py | 46 +- lib/session.py | 10 +- lib/sounds.py | 4 +- main.py | 69 +- ui/PersonDialog.ui | 8 +- ui/Preferences.py | 76 +- ui/Preferences.ui | 41 +- ui/ReadDialog.py | 40 +- ui/ReadDialog.ui | 12 +- ui/SessionDialog.py | 58 +- ui/SessionDialog.ui | 8 +- ui/WordDialog.py | 28 +- ui/WordDialog.ui | 4 +- ui/assets/KEEP | 0 ui/resources.rcc | Bin 390956 -> 398630 bytes ui/translate/esl_reader_en.qm | Bin 0 -> 33 bytes ui/translate/esl_reader_en.ts | 401 ++ ui/translate/esl_reader_fr.qm | Bin 0 -> 3013 bytes ui/translate/esl_reader_fr.ts | 7498 +++++++++++++++++++++++++++++++++ ui/translate/esl_reader_pt.qm | Bin 0 -> 4352 bytes ui/translate/esl_reader_pt.ts | 506 +++ 24 files changed, 8712 insertions(+), 206 deletions(-) create mode 100644 ui/assets/KEEP create mode 100644 ui/translate/esl_reader_en.qm create mode 100644 ui/translate/esl_reader_en.ts create mode 100644 ui/translate/esl_reader_fr.qm create mode 100644 ui/translate/esl_reader_fr.ts create mode 100644 ui/translate/esl_reader_pt.qm create mode 100644 ui/translate/esl_reader_pt.ts diff --git a/lib/books.py b/lib/books.py index 11a5f23..2da875d 100644 --- a/lib/books.py +++ b/lib/books.py @@ -3,6 +3,7 @@ import os import xml.dom.minidom from typing import Dict, List, cast +from PyQt6.QtCore import QCoreApplication from PyQt6.QtSql import QSqlQuery from main import query_error @@ -20,13 +21,16 @@ class Book: return def load(self, book_id: int) -> None: + translate = QCoreApplication.translate query = QSqlQuery() query.prepare("SELECT * FROM books where book_id = :book_id") query.bindValue(":book_id", book_id) if not query.exec(): query_error(query) if not query.next(): - raise Exception(f"Missing book? book_id={book_id}") + raise Exception( + translate("Book", "Missing book? book_id=") + f"{book_id}" + ) self.metadata = { "title": query.value("title"), "creator": query.value("author"), diff --git a/lib/person.py b/lib/person.py index adf8fe5..638a665 100644 --- a/lib/person.py +++ b/lib/person.py @@ -17,7 +17,7 @@ from PyQt6.QtSql import QSqlQuery, QSqlQueryModel from PyQt6.QtWidgets import QDialog, QDialogButtonBox, QStyledItemDelegate from main import query_error -from ui.PeopleDialog import Ui_Dialog +from ui.PeopleDialog import Ui_PersonDialog class blockHandler(HTMLParser): @@ -104,7 +104,7 @@ class MLStripper(HTMLParser): return self.text.getvalue() -class PersonDialog(QDialog, Ui_Dialog): +class PersonDialog(QDialog, Ui_PersonDialog): SectionIdRole = Qt.ItemDataRole.UserRole SectionSequenceRole = Qt.ItemDataRole.UserRole + 1 BookIdRole = Qt.ItemDataRole.UserRole + 2 @@ -125,12 +125,12 @@ class PersonDialog(QDialog, Ui_Dialog): if not query.exec(): query_error(query) model.setQuery(query) - self.bookCombo.setPlaceholderText("Select A Book") + self.bookCombo.setPlaceholderText(self.tr("Select A Book")) self.bookCombo.setModel(model) self.bookCombo.setModelColumn(1) self.bookCombo.setCurrentIndex(-1) model: QStandardItemModel = QStandardItemModel() # type: ignore[no-redef] - self.sectionCombo.setPlaceholderText("Select A Section") + self.sectionCombo.setPlaceholderText(self.tr("Select A Section")) self.sectionCombo.setModel(model) self.sectionCombo.setEnabled(False) self.sectionCombo.setCurrentIndex(-1) @@ -168,7 +168,7 @@ class PersonDialog(QDialog, Ui_Dialog): if not query.exec(): query_error(query) if not query.next(): - raise Exception(f"No person record for {person_id}") + raise Exception(self.tr("No person record for ") + f"{person_id}") self.person_id = person_id self.nameEdit.setText(query.value("name")) self.orgEdit.setText(query.value("organization")) @@ -183,7 +183,8 @@ class PersonDialog(QDialog, Ui_Dialog): ) if len(matches) != 1: raise Exception( - f"Match failed looking for book_id: {query.value('book_id')}" + self.tr("Match failed looking for book_id: ") + + f"{query.value('book_id')}" ) row = int(matches[0].row()) self.bookCombo.setCurrentIndex(row) @@ -215,10 +216,14 @@ class PersonDialog(QDialog, Ui_Dialog): ) html += "\n" html += f"

{title}

\n" + html += self.makeNotes() html += self.makeDefinitions() html += self.makeText() html += "\n\n" + # + # XXX - Use the sound module, don't do this by hand + # if self.sender() == self.printBtn: dev = None for output in QMediaDevices.audioOutputs(): @@ -312,9 +317,8 @@ class PersonDialog(QDialog, Ui_Dialog): book_id = model.data(model.createIndex(row, 0)) query.bindValue(":book_id", book_id) section_id = self.sectionCombo.currentData(PersonDialog.SectionIdRole) - print(f"section_id: {section_id}") if not section_id: - raise Exception(f"Section id is null") + raise Exception(self.tr("Section id is null")) if not query.exec(): query_error(query) if self.person_id <= 0: @@ -426,7 +430,9 @@ class PersonDialog(QDialog, Ui_Dialog): if not section_query.exec(): query_error(section_query) if not section_query.next(): - raise Exception(f"Missing section {section_id}") + raise Exception( + self.tr("Missing section ") + f"{section_id}" + ) section = blockHandler() section.feed(section_query.value("content")) html += section.get_block(query.value("block")) + "\n" @@ -437,3 +443,24 @@ class PersonDialog(QDialog, Ui_Dialog): html = '
' html += "
\n" return html + + def makeNotes(self) -> str: + html = '
' + query = QSqlQuery() + query.prepare( + "SELECT * FROM sessions " "WHERE session_id = :session_id" + ) + row = self.sessionCombo.currentIndex() + model = self.sessionCombo.model() + index = model.index(row, 0) + session_id = index.data() + query.bindValue(":session_id", session_id) + if not query.exec(): + query_error(query) + first = True + if not query.next(): + return "" + html += "

" + self.tr("Notes") + "

\n" + html += query.value("notes") + html += "
" + return html diff --git a/lib/preferences.py b/lib/preferences.py index 451acba..ffa9055 100644 --- a/lib/preferences.py +++ b/lib/preferences.py @@ -1,19 +1,28 @@ import json import os -from typing import Any, Dict, List, Optional, Type, cast +import re +from typing import Any, Dict, List, Optional, Self, Type, cast -from PyQt6.QtCore import Qt, pyqtSlot +from PyQt6.QtCore import ( + QCoreApplication, + QLocale, + QResource, + Qt, + QTranslator, + pyqtSlot, +) from PyQt6.QtMultimedia import QMediaDevices from PyQt6.QtWidgets import QAbstractItemView, QDialog, QListWidgetItem, QWidget -from ui.Preferences import Ui_Dialog +from ui.Preferences import Ui_Preferences -class Preferences(QDialog, Ui_Dialog): +class Preferences(QDialog, Ui_Preferences): + translator: Optional[QTranslator] _instance = None preferences: Dict[str, str | List[str]] - def __new__(cls: Type[Preferences]) -> Preferences: + def __new__(cls: Type[Self]) -> Self: if cls._instance: return cls._instance cls._instance = super(Preferences, cls).__new__(cls) @@ -59,6 +68,15 @@ class Preferences(QDialog, Ui_Dialog): description = output.description() self.alertList.addItem(description) self.playerList.addItem(description) + resource = QResource("/translate") + translator = QTranslator() + for child in resource.children(): + fn = f":/translate/{child}" + if translator.load(fn): + locale = QLocale(translator.language()) + lang_code = locale.language() + language = QLocale.languageToString(lang_code) + self.languageCombo.addItem(language, translator.language()) self.setCurrent() return @@ -72,6 +90,7 @@ class Preferences(QDialog, Ui_Dialog): "phoneticFont": "Gentium", "alertOutputs": ["default"], "playerOutputs": ["Feed for virtual microphone"], + "language": "pt", } for output in self.preferences["alertOutputs"]: if output == "default": @@ -87,6 +106,17 @@ class Preferences(QDialog, Ui_Dialog): output, Qt.MatchFlag.MatchExactly ): item.setSelected(True) + index = self.languageCombo.findData(self.preferences["language"]) + if index >= 0: + self.languageCombo.setCurrentIndex(index) + self.translator = QTranslator() + if self.translator.load( + f":/translate/esl_reader_{self.preferences['language']}" + ): + assert self.translator is not None + QCoreApplication.installTranslator(self.translator) + else: + self.translator = None index = self.readerCombo.findText(self.preferences["readerFont"]) if index >= 0: self.readerCombo.setCurrentIndex(index) @@ -107,6 +137,7 @@ class Preferences(QDialog, Ui_Dialog): self.preferences[ "phoneticFont" ] = self.phoneticsCombo.currentFont().family() + self.preferences["language"] = self.languageCombo.currentData() self.preferences["alertOutputs"] = [ x.data(Qt.ItemDataRole.DisplayRole) for x in self.alertList.selectedItems() @@ -117,5 +148,22 @@ class Preferences(QDialog, Ui_Dialog): ] with open("preferences.json", "w") as f: json.dump(self.preferences, f, indent=2) + if self.translator: + if self.translator.language() != self.preferences["language"]: + full_code: str = cast(str, self.preferences["language"]) + locale = QLocale(full_code) + language = locale.language() + code = QLocale.languageToCode( + language, QLocale.LanguageCodeType.ISO639Part1 + ) + print(language, code) + fn = f":/translate/esl_reader_{code}.qm" + translator = QTranslator() + if translator.load(fn): + QCoreApplication.removeTranslator(self.translator) + self.translator = translator + QCoreApplication.installTranslator(self.translator) + else: + print(self.tr("Unable to load translation file ") + f"{fn}") super().accept() return diff --git a/lib/read.py b/lib/read.py index d407e50..e81b8f6 100644 --- a/lib/read.py +++ b/lib/read.py @@ -36,10 +36,10 @@ from lib.preferences import Preferences from lib.session import SessionDialog from lib.sounds import SoundOff from main import query_error -from ui.ReadDialog import Ui_Dialog +from ui.ReadDialog import Ui_ReadDialog -class ReadDialog(QDialog, Ui_Dialog): +class ReadDialog(QDialog, Ui_ReadDialog): playSound = pyqtSignal(str) playAlert = pyqtSignal() block: int @@ -112,9 +112,9 @@ class ReadDialog(QDialog, Ui_Dialog): @pyqtSlot() def timerAction(self) -> None: if self.session.isActive(): # We are stopping - self.sessionBtn.setText("Start") + self.sessionBtn.setText(self.tr("Start")) else: - self.sessionBtn.setText("Stop") + self.sessionBtn.setText(self.tr("Stop")) self.session.timerAction() self.newParagraph.emit(self.section_id, self.block) return @@ -153,7 +153,6 @@ class ReadDialog(QDialog, Ui_Dialog): @pyqtSlot() def playAction(self) -> None: - print("playAction") idx = self.stackedWidget.currentIndex() if idx == 0: # Reading # find word @@ -188,11 +187,11 @@ class ReadDialog(QDialog, Ui_Dialog): self.phonetics = None if not self.phonetics: return - print("Searching for audio file") + print(self.tr("Searching for audio file")) for entry in self.phonetics: if len(entry["audio"]) > 0: # self.parent().playAlert.emit() - print(f"playing {entry['audio']}") + print(self.tr("playing ") + f"{entry['audio']}") self.playSound.emit(entry["audio"]) return @@ -279,16 +278,23 @@ class ReadDialog(QDialog, Ui_Dialog): return def defToHtml(self, word: str, definition: Dict[str, Any]) -> str: + SPEAKER = "\U0001F508" html = f'

{word}

' + "\n" try: words: List[str] = [] for phonetic in definition["phonetics"]: + # XXX - Some phonetics have text and audio but audio is empty, + # some have just text and some have just audio if phonetic["text"] in words: continue words.append(phonetic["text"]) - html += f'

{phonetic["text"]}

' + "\n" + html += f'

{phonetic["text"]}' + if "audio" in phonetic: + html += f'{SPEAKER}' + html += "

\n" except Exception: pass + print(html + "\n") html += '