From b26a399e8a6962b8e5449a951393840839cb12e5 Mon Sep 17 00:00:00 2001 From: Faraphel Date: Fri, 29 Dec 2023 20:13:09 +0100 Subject: [PATCH] added translation support --- main.py | 6 +- source/survey/IntegerQuestion.py | 5 +- source/survey/MultipleChoiceQuestion.py | 13 +-- source/survey/SingleChoiceQuestion.py | 13 +-- source/survey/Text.py | 9 +- source/survey/TextQuestion.py | 5 +- source/survey/WebMission.py | 5 +- source/translate.py | 45 ++++++++++ surveys.json | 105 ++++++++++++------------ 9 files changed, 130 insertions(+), 76 deletions(-) create mode 100644 source/translate.py diff --git a/main.py b/main.py index 5a8b38d..a587738 100644 --- a/main.py +++ b/main.py @@ -3,6 +3,7 @@ import sys from PyQt6.QtCore import QTranslator, QLocale from PyQt6.QtWidgets import QApplication +import translate from source import assets_path from source.widget import MyMainWindow @@ -13,7 +14,10 @@ if __name__ == "__main__": # get the user language local = QLocale() - language_code: str = local.languageToCode(local.language()) + language_code: str = local.languageToCode(local.language(), QLocale.LanguageCodeType.ISO639) + + # apply the language on the translator + translate.set_language(language_code) # load the translator to support multiple languages translator = QTranslator() diff --git a/source/survey/IntegerQuestion.py b/source/survey/IntegerQuestion.py index 450c0ba..3bb9bde 100644 --- a/source/survey/IntegerQuestion.py +++ b/source/survey/IntegerQuestion.py @@ -4,13 +4,14 @@ from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QFont from PyQt6.QtWidgets import QVBoxLayout, QLabel, QSpinBox +import translate from source.survey.base import BaseSurvey class IntegerQuestion(BaseSurvey): def __init__( self, - title: str, + title: translate.Translatable, default: Optional[int] = None, minimum: Optional[int] = None, maximum: Optional[int] = None, @@ -30,7 +31,7 @@ class IntegerQuestion(BaseSurvey): # question title self.label_question = QLabel() self._layout.addWidget(self.label_question) - self.label_question.setText(title) + self.label_question.setText(translate.translate(title)) self.label_question.setAlignment(Qt.AlignmentFlag.AlignCenter) font_title = self.label_question.font() diff --git a/source/survey/MultipleChoiceQuestion.py b/source/survey/MultipleChoiceQuestion.py index ba397d5..a7a9684 100644 --- a/source/survey/MultipleChoiceQuestion.py +++ b/source/survey/MultipleChoiceQuestion.py @@ -4,17 +4,18 @@ from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QFont from PyQt6.QtWidgets import QFrame, QVBoxLayout, QLabel, QCheckBox, QLineEdit +import translate from source.survey.base import BaseSurvey class MultipleChoiceQuestion(BaseSurvey): def __init__( self, - title: str, - choices: dict[Any, str], + title: translate.Translatable, + choices: dict[Any, translate.Translatable], details_choice_enabled: bool = None, details_choice_id: str = None, - details_choice_text: str = None, + details_choice_text: translate.Translatable = None, signals: dict[str, pyqtSignal] = None ): super().__init__() @@ -31,7 +32,7 @@ class MultipleChoiceQuestion(BaseSurvey): # question title self.label_question = QLabel() self._layout.addWidget(self.label_question) - self.label_question.setText(title) + self.label_question.setText(translate.translate(title)) self.label_question.setAlignment(Qt.AlignmentFlag.AlignCenter) font_title = self.label_question.font() @@ -51,7 +52,7 @@ class MultipleChoiceQuestion(BaseSurvey): for choice_id, choice_text in choices.items(): # create a radio button for that choice button = QCheckBox() - button.setText(choice_text) + button.setText(translate.translate(choice_text)) # add the button to the frame self._layout_responses.addWidget(button) @@ -62,7 +63,7 @@ class MultipleChoiceQuestion(BaseSurvey): if self.details_choice_enabled: self.button_response_other = QCheckBox() self._layout_responses.addWidget(self.button_response_other) - self.button_response_other.setText(self.details_choice_text) + self.button_response_other.setText(translate.translate(self.details_choice_text)) self.button_response_other.toggled.connect(self._on_response_other_check) # NOQA: connect exist self.entry_response_other = QLineEdit() diff --git a/source/survey/SingleChoiceQuestion.py b/source/survey/SingleChoiceQuestion.py index 112491c..5db79c1 100644 --- a/source/survey/SingleChoiceQuestion.py +++ b/source/survey/SingleChoiceQuestion.py @@ -4,17 +4,18 @@ from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QFont from PyQt6.QtWidgets import QFrame, QVBoxLayout, QLabel, QRadioButton, QButtonGroup, QLineEdit, QAbstractButton +import translate from source.survey.base import BaseSurvey class SingleChoiceQuestion(BaseSurvey): def __init__( self, - title: str, + title: translate.Translatable, details_choice_enabled: bool = None, details_choice_id: str = None, - details_choice_text: str = None, - choices: dict[Any, str] = None, + details_choice_text: translate.Translatable = None, + choices: dict[Any, translate.Translatable] = None, signals: dict[str, pyqtSignal] = None ): super().__init__() @@ -33,7 +34,7 @@ class SingleChoiceQuestion(BaseSurvey): # question title self.label_question = QLabel() self._layout.addWidget(self.label_question) - self.label_question.setText(title) + self.label_question.setText(translate.translate(title)) self.label_question.setAlignment(Qt.AlignmentFlag.AlignCenter) font_title = self.label_question.font() @@ -59,7 +60,7 @@ class SingleChoiceQuestion(BaseSurvey): for choice_id, choice_text in choices.items(): # create a radio button for that choice button = QRadioButton() - button.setText(choice_text) + button.setText(translate.translate(choice_text)) # add the button to the frame self._layout_responses.addWidget(button) @@ -73,7 +74,7 @@ class SingleChoiceQuestion(BaseSurvey): self._layout_responses.addWidget(self.button_response_other) self.button_responses_id[self.button_response_other] = self.details_choice_id - self.button_response_other.setText(self.details_choice_text) + self.button_response_other.setText(translate.translate(self.details_choice_text)) self.group_responses.addButton(self.button_response_other) self.button_response_other.toggled.connect(self._on_response_other_check) # NOQA: connect exist diff --git a/source/survey/Text.py b/source/survey/Text.py index 8e60450..be195ee 100644 --- a/source/survey/Text.py +++ b/source/survey/Text.py @@ -4,14 +4,15 @@ from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QFont from PyQt6.QtWidgets import QVBoxLayout, QLabel +import translate from source.survey.base import BaseSurvey class Text(BaseSurvey): def __init__( self, - title: str, - description: Optional[str], + title: translate.Translatable, + description: Optional[translate.Translatable], abandonable: bool = None, signals: dict[str, pyqtSignal] = None ): @@ -27,7 +28,7 @@ class Text(BaseSurvey): # prepare the title self.label_title = QLabel() self._layout.addWidget(self.label_title) - self.label_title.setText(title) + self.label_title.setText(translate.translate(title)) self.label_title.setAlignment(Qt.AlignmentFlag.AlignCenter) font_title = self.label_title.font() @@ -39,7 +40,7 @@ class Text(BaseSurvey): # prepare the description self.label_description = QLabel() self._layout.addWidget(self.label_description) - self.label_description.setText(description) + self.label_description.setText(translate.translate(description)) self.label_description.setAlignment(Qt.AlignmentFlag.AlignCenter) @classmethod diff --git a/source/survey/TextQuestion.py b/source/survey/TextQuestion.py index 5212ed8..652364d 100644 --- a/source/survey/TextQuestion.py +++ b/source/survey/TextQuestion.py @@ -4,11 +4,12 @@ from PyQt6.QtCore import Qt, pyqtSignal from PyQt6.QtGui import QFont from PyQt6.QtWidgets import QVBoxLayout, QLabel, QTextEdit +import translate from source.survey.base import BaseSurvey class TextQuestion(BaseSurvey): - def __init__(self, title: str, signals: dict[str, pyqtSignal] = None): + def __init__(self, title: translate.Translatable, signals: dict[str, pyqtSignal] = None): super().__init__() self.signals = signals if signals is not None else {} @@ -20,7 +21,7 @@ class TextQuestion(BaseSurvey): # question title self.label_question = QLabel() self._layout.addWidget(self.label_question) - self.label_question.setText(title) + self.label_question.setText(translate.translate(title)) self.label_question.setAlignment(Qt.AlignmentFlag.AlignCenter) font_title = self.label_question.font() diff --git a/source/survey/WebMission.py b/source/survey/WebMission.py index 7ab2ee2..65f3608 100644 --- a/source/survey/WebMission.py +++ b/source/survey/WebMission.py @@ -6,6 +6,7 @@ from PyQt6.QtCore import Qt, QTimer, pyqtSignal, QUrl, QEvent, QObject from PyQt6.QtGui import QFont, QMouseEvent, QResizeEvent, QKeyEvent from PyQt6.QtWidgets import QLabel, QVBoxLayout, QSizePolicy +import translate from source import assets_path from source.survey.base import BaseSurvey from source.widget import Browser @@ -16,7 +17,7 @@ page_success_path: Path = assets_path / "web/success.html" class WebMission(BaseSurvey): def __init__( self, - title: str, + title: translate.Translatable, url: str, check_condition: Optional[str] = None, skip_time: Optional[float] = None, @@ -42,7 +43,7 @@ class WebMission(BaseSurvey): # mission title self.label_title = QLabel() self._layout.addWidget(self.label_title) - self.label_title.setText(title) + self.label_title.setText(translate.translate(title)) self.label_title.setAlignment(Qt.AlignmentFlag.AlignCenter) font_title = self.label_title.font() diff --git a/source/translate.py b/source/translate.py new file mode 100644 index 0000000..433edd6 --- /dev/null +++ b/source/translate.py @@ -0,0 +1,45 @@ +import warnings +from typing import Optional + + +Translatable = dict[str, str] + +current_language_code: str = "en" + + +def set_language(language_code: str) -> None: + """ + Set the current language of the application + :param language_code: the new language code + """ + + global current_language_code + current_language_code = language_code + + +def get_language() -> str: + """ + Get the current language of the application + :return: the current language of the application + """ + + return current_language_code + + +def translate(language_data: Translatable) -> str: + """ + Get the translation of a text to the current set language + :param language_data: a dictionary with every translation associated to the language + :return: the translation for the current language + """ + + translation: Optional[str] = language_data.get(current_language_code) + + if translation is None: + warnings.warn(f"No translation for language {current_language_code!r} for text {language_data!r}") + translation = language_data.get("en") + + if translation is None: + raise Exception(f"No translation available for text {language_data!r}") + + return translation diff --git a/surveys.json b/surveys.json index 43434a1..c7816d1 100644 --- a/surveys.json +++ b/surveys.json @@ -3,20 +3,20 @@ "text-welcome": { "type": "text", - "title": "Bienvenue dans nôtre enquête !", - "description": "Nous somme des étudiants en Informatique à l'UPJV.\nNous réalisons une étude sur l'ergonomie des interfaces graphique.\nCette étude concerne la plateforme de jeu vidéo \"Steam\"." + "title": {"fr": "Bienvenue dans nôtre enquête !"}, + "description": {"fr": "Nous somme des étudiants en Informatique à l'UPJV.\nNous réalisons une étude sur l'ergonomie des interfaces graphique.\nCette étude concerne la plateforme de jeu vidéo \"Steam\"."} }, "text-data-collection": { "type": "text", - "title": "Collection des données", - "description": "Notre étude demande de collecter des données comme vos mouvements de souris et votre clavier lorsque vous naviguerez sur cette application.\nAucune autre donnée ne sera collecté en dehors de cette application.\nCes données sont anonyme et ne seront destiné exclusivement qu'à notre recherche.\nSi vous ne souhaitez pas continuer ce questionnaire, appuyez sur \"Abandonner\"", + "title": {"fr": "Collection des données"}, + "description": {"fr": "Notre étude demande de collecter des données comme vos mouvements de souris et votre clavier lorsque vous naviguerez sur cette application.\nAucune autre donnée ne sera collecté en dehors de cette application.\nCes données sont anonyme et ne seront destiné exclusivement qu'à notre recherche.\nSi vous ne souhaitez pas continuer ce questionnaire, appuyez sur \"Abandonner\""}, "abandonable": true }, "question-age": { "type": "question-integer", - "title": "Quel est votre âge ?", + "title": {"fr": "Quel est votre âge ?"}, "default": 30, "minimum": 13, "maximum": 150 @@ -24,52 +24,52 @@ "question-usage-steam": { "type": "question-single-choice", - "title": "Utilisez-vous Steam ?", + "title": {"fr": "Utilisez-vous Steam ?"}, "choices": { - "always": "Tout le temps", - "often": "Souvent", - "sometime": "De temps en temps", - "rarely": "Rarement", - "never": "Jamais" + "always": {"fr": "Tout le temps"}, + "often": {"fr": "Souvent"}, + "sometime": {"fr": "De temps en temps"}, + "rarely": {"fr": "Rarement"}, + "never": {"fr": "Jamais"} } }, "question-usage-concurrent": { "type": "question-multiple-choice", - "title": "Quel autre plateforme de jeu en ligne utilisez-vous ?", + "title": {"fr": "Quel autre plateforme de jeu en ligne utilisez-vous ?"}, "choices": { - "epic": "Epic Games Store", - "gog": "GOG", - "origin": "Origin", - "uplay": "Uplay", - "battle": "Battle.net" + "epic": {"fr": "Epic Games Store"}, + "gog": {"fr": "GOG"}, + "origin": {"fr": "Origin"}, + "uplay": {"fr": "Uplay"}, + "battle": {"fr": "Battle.net"} }, "details_choice_enabled": true, "details_choice_id": "other", - "details_choice_text": "Autre" + "details_choice_text": {"fr": "Autre"} }, "question-difficulty-before": { "type": "question-single-choice", - "title": "Avez-vous déjà rencontré des difficultés particulières lors de votre utilisation de Steam ?", + "title": {"fr": "Avez-vous déjà rencontré des difficultés particulières lors de votre utilisation de Steam ?"}, "choices": { - "no": "Non" + "no": {"fr": "Non"} }, "details_choice_enabled": true, "details_choice_id": "yes", - "details_choice_text": "Oui" + "details_choice_text": {"fr": "Oui"} }, "mission-explanation": { "type": "text", - "title": "Explication de l'Evaluation", - "description": "Nous allons vous demander de naviguer dans Steam.\nUn message en haut vous indiquera une tâche à réaliser dans la plateforme.\n\nSi la tâche vous semble trop difficile, un bouton pour passer la tâche\ns'affichera au bout d'un certain temps en bas à droite.\n\nRappel : Votre clavier et votre souris sont enregistré.\nNe tapez aucune information personnel dans l'écran qui va suivre !" + "title": {"fr": "Explication de l'Evaluation"}, + "description": {"fr": "Nous allons vous demander de naviguer dans Steam.\nUn message en haut vous indiquera une tâche à réaliser dans la plateforme.\n\nSi la tâche vous semble trop difficile, un bouton pour passer la tâche\ns'affichera au bout d'un certain temps en bas à droite.\n\nRappel : Votre clavier et votre souris sont enregistré.\nNe tapez aucune information personnel dans l'écran qui va suivre !"} }, "mission-language": { "type": "mission-web", - "title": "Changer la langue en français.", + "title": {"fr": "Changer la langue en français."}, "url": "https://store.steampowered.com/", "check": "document.getElementsByTagName('html')[0].lang == 'fr'", "skip_time": 60 @@ -77,7 +77,7 @@ "mission-price": { "type": "mission-web", - "title": "Afficher tous les jeux coutant moins de 20€.", + "title": {"fr": "Afficher tous les jeux coutant moins de 20€."}, "url": "https://store.steampowered.com/", "check": "document.getElementById('maxprice_input').value == '20'", "skip_time": 90 @@ -85,7 +85,7 @@ "mission-community-hub": { "type": "mission-web", - "title": "Rendez-vous sur le Hub de la Communauté du jeu \"Stray\".", + "title": {"fr": "Rendez-vous sur le Hub de la Communauté du jeu \"Stray\"."}, "url": "https://store.steampowered.com/", "check": "document.documentURI == 'https://steamcommunity.com/app/1332010'", "skip_time": 90 @@ -93,7 +93,7 @@ "mission-game-page": { "type": "mission-web", - "title": "Rendez-vous sur la page du jeu \"Outer Wilds\".", + "title": {"fr": "Rendez-vous sur la page du jeu \"Outer Wilds\"."}, "url": "https://store.steampowered.com/", "check": "document.documentURI == 'https://store.steampowered.com/app/753640/Outer_Wilds/'", "skip_time": 90 @@ -101,7 +101,7 @@ "mission-game-dlc": { "type": "mission-web", - "title": "Rendez-vous sur la page du contenu additionnel \"Echoes of the Eye\" du jeu \"Outer Wilds\".", + "title": {"fr": "Rendez-vous sur la page du contenu additionnel \"Echoes of the Eye\" du jeu \"Outer Wilds\"."}, "url": "https://store.steampowered.com/app/753640/Outer_Wilds/", "check": "document.documentURI == 'https://store.steampowered.com/app/1622100/Outer_Wilds__Echoes_of_the_Eye/'", "skip_time": 90 @@ -109,7 +109,7 @@ "mission-actuality-new": { "type": "mission-web", - "title": "Aller sur la page des Actualités \"À la une\".", + "title": {"fr": "Aller sur la page des Actualités \"À la une\"."}, "url": "https://store.steampowered.com/", "check": "document.documentURI == 'https://store.steampowered.com/news/collection/featured/'", "skip_time": 120 @@ -117,7 +117,7 @@ "mission-profile": { "type": "mission-web", - "title": "Trouver la page de profil de \"Faraphel\".", + "title": {"fr": "Trouver la page de profil de \"Faraphel\"."}, "url": "https://store.steampowered.com/", "check": "document.documentURI == 'https://steamcommunity.com/id/Faraphel'", "skip_time": 240 @@ -125,7 +125,7 @@ "mission-game-discussion": { "type": "mission-web", - "title": "Trouver la page de discussions du jeu \"Sid Meier's Civilization V\".", + "title": {"fr": "Trouver la page de discussions du jeu \"Sid Meier's Civilization V\"."}, "url": "https://store.steampowered.com/", "check": "document.documentURI == 'https://steamcommunity.com/app/8930/discussions/'", "skip_time": 180 @@ -133,7 +133,7 @@ "mission-gift-card": { "type": "mission-web", - "title": "Trouver la page des cartes-cadeaux.", + "title": {"fr": "Trouver la page des cartes-cadeaux."}, "url": "https://store.steampowered.com/", "check": "document.documentURI == 'https://store.steampowered.com/digitalgiftcards'", "skip_time": 240 @@ -141,7 +141,7 @@ "mission-workshop": { "type": "mission-web", - "title": "Trouver la page de communauté de la modification \"Animated Hair\" du jeu \"Terraria\" sur le Workshop.", + "title": {"fr": "Trouver la page de communauté de la modification \"Animated Hair\" du jeu \"Terraria\" sur le Workshop."}, "url": "https://store.steampowered.com/", "check": "publishedfileid == '2871109853'", "skip_time": 240 @@ -150,46 +150,45 @@ "question-hardest-mission": { "type": "question-single-choice", - "title": "Parmi les tâches, laquelle avez-vous trouvée la plus difficile ?", + "title": {"fr": "Parmi les tâches, laquelle avez-vous trouvée la plus difficile ?"}, "choices": { - "mission-language": "Changer la langue", - "mission-price": "Filtrer les jeux par leur prix", - "mission-community-hub": "Se rendre sur le hub de la communauté", - "mission-game-page": "Se rendre sur la page d'un jeu", - "mission-game-dlc": "Se rendre sur la page du contenu additionnel (DLC) d'un jeu", - "mission-actuality-new": "Se rendre sur la page des actualités", - "mission-profile": "Se rendre sur le profil d'un utilisateur", - "mission-game-discussion": "Se rendre sur la page de discussion d'un jeu", - "mission-gift-card": "Se rendre sur la page des cartes cadeaux", - "mission-workshop": "Se rendre sur la page de la modification d'un jeu" + "mission-language": {"fr": "Changer la langue"}, + "mission-price": {"fr": "Filtrer les jeux par leur prix"}, + "mission-community-hub": {"fr": "Se rendre sur le hub de la communauté"}, + "mission-game-page": {"fr": "Se rendre sur la page d'un jeu"}, + "mission-game-dlc": {"fr": "Se rendre sur la page du contenu additionnel (DLC) d'un jeu"}, + "mission-actuality-new": {"fr": "Se rendre sur la page des actualités"}, + "mission-profile": {"fr": "Se rendre sur le profil d'un utilisateur"}, + "mission-game-discussion": {"fr": "Se rendre sur la page de discussion d'un jeu"}, + "mission-gift-card": {"fr": "Se rendre sur la page des cartes cadeaux"}, + "mission-workshop": {"fr": "Se rendre sur la page de la modification d'un jeu"} } }, "question-experience": { "type": "question-single-choice", - "title": "Avez-vous trouvé l'interface de Steam ergonomique ?", + "title": {"fr": "Avez-vous trouvé l'interface de Steam ergonomique ?"}, "choices": { - "yes": "Oui", - "mixed": "Mitigé", - "no": "Non" + "yes": {"fr": "Oui"}, + "mixed": {"fr": "Mitigé"}, + "no": {"fr": "Non"} } }, "question-experience-details": { "type": "question-text", - "title": "Qu'avez-vous pensé de l'ergonomie de Steam ?" + "title": {"fr": "Qu'avez-vous pensé de l'ergonomie de Steam ?"} }, "question-comment": { "type": "question-text", - "title": "Vous pouvez laisser un commentaire sur votre ressenti général (optionnel)" + "title": {"fr": "Vous pouvez laisser un commentaire sur votre ressenti général (optionnel)"} }, "text-thanking": { "type": "text", - "title": "Remerciement", - "description": "Nous vous remercions grandement pour votre contribution à notre questionnaire et pour votre temps.\n\nVos données collectées sont situés dans le dossier \"result\".", - "abandonable": true + "title": {"fr": "Remerciement"}, + "description": {"fr": "Nous vous remercions grandement pour votre contribution à notre questionnaire et pour votre temps.\n\nVos données collectées sont situés dans le dossier \"result\"."} } }