diff --git a/.gitignore b/.gitignore index f30aa49..8499d7c 100644 --- a/.gitignore +++ b/.gitignore @@ -23,3 +23,6 @@ save.json # Don't commit docs output docs/_build + +# Don't commit compiled messages +*.mo diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8613258..ff5c142 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -7,6 +7,7 @@ py37: stage: test image: python:3.7-alpine before_script: + - apk add --no-cache gettext - pip install tox script: tox -e py3 @@ -14,6 +15,7 @@ py38: stage: test image: python:3.8-alpine before_script: + - apk add --no-cache gettext - pip install tox script: tox -e py3 @@ -22,6 +24,7 @@ py39: stage: test image: python:3.9-alpine before_script: + - apk add --no-cache gettext - pip install tox script: tox -e py3 @@ -37,7 +40,7 @@ build-deb: image: debian:buster-slim stage: build before_script: - - apt-get update && apt-get -y --no-install-recommends install build-essential debmake dh-python debhelper python3-all python3-setuptools + - apt-get update && apt-get -y --no-install-recommends install build-essential debmake dh-python debhelper gettext python3-all python3-setuptools script: - dpkg-buildpackage - mkdir build && cp ../*.deb build/ diff --git a/debian/changelog b/debian/changelog index 887634f..2399e41 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,4 +1,4 @@ -python3-squirrel-battle (3.14) beta; urgency=low +python3-squirrel-battle (3.14.1) beta; urgency=low * Some graphical improvements. diff --git a/debian/control b/debian/control index fc52e38..b59997d 100644 --- a/debian/control +++ b/debian/control @@ -2,7 +2,7 @@ Source: python3-squirrel-battle Section: devel Priority: optional Maintainer: ynerant -Build-Depends: debhelper (>=10~), dh-python, python3-all, python3-setuptools +Build-Depends: debhelper (>=10~), dh-python, gettext, python3-all, python3-setuptools Depends: fonts-noto-color-emoji Standards-Version: 4.1.4 Homepage: https://gitlab.crans.org/ynerant/squirrel-battle diff --git a/docs/deployment.rst b/docs/deployment.rst index a9f58ee..9477a10 100644 --- a/docs/deployment.rst +++ b/docs/deployment.rst @@ -34,6 +34,16 @@ paquet ainsi que des détails à fournir à PyPI : with open("README.md", "r") as f: long_description = f.read() + # Compile messages + for language in ["de", "en", "fr"]: + args = ["msgfmt", "--check-format", + "-o", f"squirrelbattle/locale/{language}/LC_MESSAGES" + "/squirrelbattle.mo", + f"squirrelbattle/locale/{language}/LC_MESSAGES" + "/squirrelbattle.po"] + print(f"Compiling {language} messages...") + subprocess.Popen(args) + setup( name="squirrel-battle", version="3.14.1", @@ -60,7 +70,7 @@ paquet ainsi que des détails à fournir à PyPI : ], python_requires='>=3.6', include_package_data=True, - package_data={"squirrelbattle": ["assets/*"]}, + package_data={"squirrelbattle": ["assets/*", "locale/*/*/*.mo"]}, entry_points={ "console_scripts": [ "squirrel-battle = squirrelbattle.bootstrap:Bootstrap.run_game", @@ -72,6 +82,8 @@ Ce fichier contient le nom du paquet, sa version, l'auteur et son contact, sa description en une ligne et sa description longue, le lien d'accueil du projet, sa licence, ses classificateurs et son exécutable. +Il commence tout d'abord par compiler les fichiers de `traduction `_. + Le paramètre ``entry_points`` définit un exécutable nommé ``squirrel-battle``, qui permet de lancer le jeu. @@ -167,7 +179,7 @@ du dépôt Git. Le fichier ``PKGBUILD`` dispose de cette structure : url="https://gitlab.crans.org/ynerant/squirrel-battle" license=('GPLv3') depends=('python') - makedepends=('python-setuptools') + makedepends=('gettext' 'python-setuptools') depends=('noto-fonts-emoji') checkdepends=('python-tox') ssource=("git+https://gitlab.crans.org/ynerant/squirrel-battle.git") @@ -217,7 +229,7 @@ les releases, est plus ou moins similaire : url="https://gitlab.crans.org/ynerant/squirrel-battle" license=('GPLv3') depends=('python') - makedepends=('python-setuptools') + makedepends=('gettext' 'python-setuptools') depends=('noto-fonts-emoji') checkdepends=('python-tox') source=("https://gitlab.crans.org/ynerant/squirrel-battle/-/archive/v3.14.1/$pkgbase-v$pkgver.tar.gz") @@ -296,7 +308,7 @@ D'abord on installe les paquets nécessaires : .. code:: apt update - apt --no-install-recommends install build-essential debmake dh-python debhelper python3-all python3-setuptools + apt --no-install-recommends install build-essential debmake dh-python debhelper gettext python3-all python3-setuptools On peut ensuite construire le paquet : diff --git a/docs/index.rst b/docs/index.rst index ff6bcf3..1cb7d83 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -37,6 +37,7 @@ Bienvenue dans la documentation de Squirrel Battle ! install-dev tests display/index + translation deployment documentation diff --git a/docs/install-dev.rst b/docs/install-dev.rst index db611e0..973c0e0 100644 --- a/docs/install-dev.rst +++ b/docs/install-dev.rst @@ -1,16 +1,19 @@ Installation d'un environnement de développement ================================================ -Il est toujours préférable de travailler dans un environnement Python isolé du reste de son instalation. +Il est toujours préférable de travailler dans un environnement Python isolé du +reste de son instalation. 1. **Installation des dépendances de la distribution.** - Vous devez déjà installer Python et le module qui permet de créer des environnements virtuels. - On donne ci-dessous l'exemple pour une distribution basée sur Debian, mais vous pouvez facilement adapter pour ArchLinux ou autre. + Vous devez déjà installer Python et le module qui permet de créer des + environnements virtuels. + On donne ci-dessous l'exemple pour une distribution basée sur Debian, + mais vous pouvez facilement adapter pour ArchLinux ou autre. .. code:: bash $ sudo apt update - $ sudo apt install --no-install-recommends -y python3-setuptools python3-venv python3-dev git + $ sudo apt install --no-install-recommends -y python3-setuptools python3-venv python3-dev gettext git 2. **Clonage du dépot** là où vous voulez : @@ -25,7 +28,13 @@ Il est toujours préférable de travailler dans un environnement Python isolé d $ python3 -m venv env $ source env/bin/activate # entrer dans l'environnement - (env)$ pip3 install -r requirements.txt - (env)$ deactivate # sortir de l'environnement + (env) $ pip3 install -r requirements.txt + (env) $ deactivate # sortir de l'environnement + +4. **Compilation des messages de traduction.** + +.. code:: bash + + (env) $ python3 main.py --compilemessages Le lancement du jeu se fait en lançant la commande ``python3 main.py``. \ No newline at end of file diff --git a/docs/translation.rst b/docs/translation.rst new file mode 100644 index 0000000..f3d2584 --- /dev/null +++ b/docs/translation.rst @@ -0,0 +1,120 @@ +Traduction +========== + +Le jeu Squirrel Battle est entièrement traduit en anglais, en français et en allement. +La langue se choisit dans les `paramètres `_. + + +Utitisation +----------- + +Les traductions sont gérées grâce au module natif ``gettext``. Le module +``squirrelbattle.translations`` s'occupe d'installer les traductions, et de +donner les chaînes traduites. + +Pour choisir la langue, il faut appeler ``Translator.setlocale(language: str)``, +où ``language`` correspond au code à 2 lettres de la langue. + +Enfin, le module expose une fonction ``gettext(str) -> str`` qui permet de +traduire les chaînes. + +Il est courant et recommandé d'importer cette fonction sous l'alias ``_``, +afin de limiter la verbositer et de permettre de rendre facilement une chaîne +traduisible. + +.. code:: python + + from squirrelbattle.translations import gettext as _, Translator + + Translator.setlocale("fr") + print(_("I am a translatable string")) + print("I am not translatable") + +Si les traductions sont bien faites (voir ci-dessous), cela donnera : + +.. code:: + + Je suis une chaîne traduisible + I am not translatable + +À noter que si la chaîne n'est pas traduite, alors par défaut on renvoie la +chaîne elle-même. + + +Extraction des chaînes à traduire +--------------------------------- + +L'appel à ``gettext`` ne fait pas que traduire les chaînes : il est possible +également d'extraire toutes les chaînes à traduire. + +Il est nécessaire d'installer le paquet Linux ``gettext`` pour cela. + +L'utilitaire ``xgettext`` s'occupe de cette extraction. Il s'utilise de la façon +suivante : + +.. code:: bash + + xgettext --from-code utf-8 -o output_file.po source_1.py ... source_n.py + +Afin de ne pas avoir à sélectionner manuellement chaque fichier, il est possible +d'appeler directement ``python3 main.py --makemessages``. Cela a pour effet +d'exécuter pour chaque langue ```` : + +.. code:: bash + + find squirrelbattle -iname '*.py' | xargs xgettext --from-code utf-8 + --add-comments + --package-name=squirrelbattle + --package-version=3.14.1 + "--copyright-holder=ÿnérant, eichhornchen, nicomarg, charlse" + --msgid-bugs-address=squirrel-battle@crans.org + -o squirrelbattle/locale//LC_MESSAGES/squirrelbattle.po + +Les fichiers de traductions se trouvent alors dans +``squirrelbattle/locale//LC_MESSAGES/squirrelbattle.po``. + + +Traduire les chaînes +-------------------- + +Après extraction des chaînes, les chaînes à traduire se trouvent dans +``squirrelbattle/locale//LC_MESSAGES/squirrelbattle.po``, comme indiqué +ci-dessus. + +Ce fichier peut-être édité avec un utilitaire tel que ``poedit``, sur +l'interface Web sur ``_, +mais surtout manuellement avec un éditeur de texte. + +Dans ce fichier, on obtient pour chaque chaîne à traduire un paragraphe de la +forme : + +.. code:: po + + #: main.py:4 + msgid "I am a translatable string" + msgstr "Je suis une chaîne traduisible" + +Il sufift de remplir les champs ``msgstr``. + + +Compilation des chaînes +----------------------- + +Pour gagner en efficacité, les chaînes sont compilées dans un fichier avec +l'extension ``.mo``. Ce sont ces fichiers qui sont lus par le module de traduction. + +Pour compiler les traductions, c'est l'utilitaire ``msgfmt`` fourni toujours par +le paquet Linux ``gettext`` que nous utilisons. Il s'utilise assez simplement : + +.. code:: bash + + msgfmt po_file.po -o mo_file.mo + +À nouveau, il est possible de compiler automatiquement les messages en exécutant +``python3 main.py --compilemessages``. + +.. warning:: + + On ne partagera pas dans le dépôt Git les fichiers compilé. En développement, + on compilera soi-même les messages, et en production, la construction des + paquets se charge de compiler automatiquement les traductions. diff --git a/main.py b/main.py index e8c333e..fbbbb35 100755 --- a/main.py +++ b/main.py @@ -2,8 +2,24 @@ # Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse # SPDX-License-Identifier: GPL-3.0-or-later +import argparse +import sys from squirrelbattle.bootstrap import Bootstrap +from squirrelbattle.translations import Translator if __name__ == "__main__": - Bootstrap.run_game() + parser = argparse.ArgumentParser() + + parser.add_argument("--makemessages", "-mm", action="store_true", + help="Extract translatable strings") + parser.add_argument("--compilemessages", "-cm", action="store_true", + help="Compile translatable strings") + + args = parser.parse_args(sys.argv[1:]) + if args.makemessages: + Translator.makemessages() + elif args.compilemessages: + Translator.compilemessages() + else: + Bootstrap.run_game() diff --git a/setup.py b/setup.py index 6287f7d..f051bbb 100644 --- a/setup.py +++ b/setup.py @@ -3,13 +3,23 @@ # Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse # SPDX-License-Identifier: GPL-3.0-or-later -import os +import subprocess from setuptools import find_packages, setup with open("README.md", "r") as f: long_description = f.read() +# Compile messages +for language in ["de", "en", "fr"]: + args = ["msgfmt", "--check-format", + "-o", f"squirrelbattle/locale/{language}/LC_MESSAGES" + "/squirrelbattle.mo", + f"squirrelbattle/locale/{language}/LC_MESSAGES" + "/squirrelbattle.po"] + print(f"Compiling {language} messages...") + subprocess.Popen(args) + setup( name="squirrel-battle", version="3.14.1", @@ -36,7 +46,7 @@ setup( ], python_requires='>=3.6', include_package_data=True, - package_data={"squirrelbattle": ["assets/*"]}, + package_data={"squirrelbattle": ["assets/*", "locale/*/*/*.mo"]}, entry_points={ "console_scripts": [ "squirrel-battle = squirrelbattle.bootstrap:Bootstrap.run_game", diff --git a/squirrelbattle/display/menudisplay.py b/squirrelbattle/display/menudisplay.py index 731ecee..b3036a0 100644 --- a/squirrelbattle/display/menudisplay.py +++ b/squirrelbattle/display/menudisplay.py @@ -6,6 +6,7 @@ from typing import List from squirrelbattle.menus import Menu, MainMenu from .display import Display, Box from ..resources import ResourceManager +from ..translations import gettext as _ class MenuDisplay(Display): @@ -17,8 +18,6 @@ class MenuDisplay(Display): def update_menu(self, menu: Menu) -> None: self.menu = menu - self.trueheight = len(self.values) - self.truewidth = max([len(a) for a in self.values]) # Menu values are printed in pad self.pad = self.newpad(self.trueheight, self.truewidth + 2) @@ -44,6 +43,14 @@ class MenuDisplay(Display): self.height - 2 + self.y, self.width - 2 + self.x) + @property + def truewidth(self) -> int: + return max([len(str(a)) for a in self.values]) + + @property + def trueheight(self) -> int: + return len(self.values) + @property def preferred_width(self) -> int: return self.truewidth + 6 @@ -60,9 +67,10 @@ class MenuDisplay(Display): class SettingsMenuDisplay(MenuDisplay): @property def values(self) -> List[str]: - return [a[1][1] + (" : " + return [_(a[1][1]) + (" : " + ("?" if self.menu.waiting_for_key - and a == self.menu.validate() else a[1][0]) + and a == self.menu.validate() else a[1][0] + .replace("\n", "\\n")) if a[1][0] else "") for a in self.menu.values] diff --git a/squirrelbattle/display/statsdisplay.py b/squirrelbattle/display/statsdisplay.py index b65e716..da9213f 100644 --- a/squirrelbattle/display/statsdisplay.py +++ b/squirrelbattle/display/statsdisplay.py @@ -3,10 +3,10 @@ import curses +from ..entities.player import Player +from ..translations import gettext as _ from .display import Display -from squirrelbattle.entities.player import Player - class StatsDisplay(Display): player: Player @@ -31,12 +31,12 @@ class StatsDisplay(Display): self.player.dexterity, self.player.constitution) self.addstr(self.pad, 3, 0, string3) - inventory_str = "Inventaire : " + "".join( + inventory_str = _("Inventory:") + " " + "".join( self.pack[item.name.upper()] for item in self.player.inventory) self.addstr(self.pad, 8, 0, inventory_str) if self.player.dead: - self.addstr(self.pad, 10, 0, "VOUS ÊTES MORT", + self.addstr(self.pad, 10, 0, _("YOU ARE DEAD"), curses.A_BOLD | curses.A_BLINK | curses.A_STANDOUT | self.color_pair(3)) diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index a64d82e..44ad349 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -1,5 +1,6 @@ # Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse # SPDX-License-Identifier: GPL-3.0-or-later + from json import JSONDecodeError from random import randint from typing import Any, Optional @@ -13,6 +14,7 @@ from .interfaces import Map, Logs from .resources import ResourceManager from .settings import Settings from . import menus +from .translations import gettext as _, Translator from typing import Callable @@ -30,11 +32,12 @@ class Game: Init the game. """ self.state = GameMode.MAINMENU - self.main_menu = menus.MainMenu() - self.settings_menu = menus.SettingsMenu() self.settings = Settings() self.settings.load_settings() self.settings.write_settings() + Translator.setlocale(self.settings.LOCALE) + self.main_menu = menus.MainMenu() + self.settings_menu = menus.SettingsMenu() self.settings_menu.update_values(self.settings) self.logs = Logs() self.message = None @@ -142,16 +145,16 @@ class Game: try: self.map.load_state(d) except KeyError: - self.message = "Some keys are missing in your save file.\n" \ - "Your save seems to be corrupt. It got deleted." + self.message = _("Some keys are missing in your save file.\n" + "Your save seems to be corrupt. It got deleted.") os.unlink(ResourceManager.get_config_path("save.json")) self.display_actions(DisplayActions.UPDATE) return players = self.map.find_entities(Player) if not players: - self.message = "No player was found on this map!\n" \ - "Maybe you died?" + self.message = _("No player was found on this map!\n" + "Maybe you died?") self.player.health = 0 self.display_actions(DisplayActions.UPDATE) return @@ -170,8 +173,9 @@ class Game: state = json.loads(f.read()) self.load_state(state) except JSONDecodeError: - self.message = "The JSON file is not correct.\n" \ - "Your save seems corrupted. It got deleted." + self.message = _("The JSON file is not correct.\n" + "Your save seems corrupted. " + "It got deleted.") os.unlink(file_path) self.display_actions(DisplayActions.UPDATE) diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index 8958e7b..90e5d69 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -6,7 +6,8 @@ from math import sqrt from random import choice, randint from typing import List, Optional -from squirrelbattle.display.texturepack import TexturePack +from .display.texturepack import TexturePack +from .translations import gettext as _ class Logs: @@ -128,7 +129,7 @@ class Map: """ Put randomly {count} hedgehogs on the map, where it is available. """ - for _ in range(count): + for ignored in range(count): y, x = 0, 0 while True: y, x = randint(0, self.height - 1), randint(0, self.width - 1) @@ -314,6 +315,10 @@ class Entity: from squirrelbattle.entities.items import Item return isinstance(self, Item) + @property + def translated_name(self) -> str: + return _(self.name.replace("_", " ")) + @staticmethod def get_all_entity_classes(): """ @@ -390,8 +395,10 @@ class FightingEntity(Entity): """ Deals damage to the opponent, based on the stats """ - return f"{self.name} hits {opponent.name}. "\ - + opponent.take_damage(self, self.strength) + return _("{name} hits {opponent}.")\ + .format(name=_(self.translated_name.capitalize()), + opponent=_(opponent.translated_name)) + " " + \ + opponent.take_damage(self, self.strength) def take_damage(self, attacker: "Entity", amount: int) -> str: """ @@ -400,8 +407,11 @@ class FightingEntity(Entity): self.health -= amount if self.health <= 0: self.die() - return f"{self.name} takes {amount} damage."\ - + (f" {self.name} dies." if self.health <= 0 else "") + return _("{name} takes {amount} damage.")\ + .format(name=self.translated_name.capitalize(), amount=str(amount))\ + + (" " + _("{name} dies.") + .format(name=self.translated_name.capitalize()) + if self.health <= 0 else "") def die(self) -> None: """ diff --git a/squirrelbattle/locale/de/LC_MESSAGES/squirrelbattle.po b/squirrelbattle/locale/de/LC_MESSAGES/squirrelbattle.po new file mode 100644 index 0000000..dfd3365 --- /dev/null +++ b/squirrelbattle/locale/de/LC_MESSAGES/squirrelbattle.po @@ -0,0 +1,166 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse +# This file is distributed under the same license as the squirrelbattle package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: squirrelbattle 3.14.1\n" +"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n" +"POT-Creation-Date: 2020-11-28 16:03+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: squirrelbattle/tests/game_test.py:284 squirrelbattle/tests/game_test.py:287 +#: squirrelbattle/tests/translations_test.py:16 +msgid "New game" +msgstr "Neu Spiel" + +#: squirrelbattle/tests/translations_test.py:17 +msgid "Resume" +msgstr "Weitergehen" + +#: squirrelbattle/tests/translations_test.py:18 +msgid "Load" +msgstr "Laden" + +#: squirrelbattle/tests/translations_test.py:19 +msgid "Save" +msgstr "Speichern" + +#: squirrelbattle/tests/translations_test.py:20 +msgid "Settings" +msgstr "Einstellungen" + +#: squirrelbattle/tests/translations_test.py:21 +msgid "Exit" +msgstr "Verlassen" + +#: squirrelbattle/tests/translations_test.py:27 +msgid "Main key to move up" +msgstr "Haupttaste zum Obengehen" + +#: squirrelbattle/tests/translations_test.py:29 +msgid "Secondary key to move up" +msgstr "Sekundärtaste zum Obengehen" + +#: squirrelbattle/tests/translations_test.py:31 +msgid "Main key to move down" +msgstr "Haupttaste zum Untergehen" + +#: squirrelbattle/tests/translations_test.py:33 +msgid "Secondary key to move down" +msgstr "Sekundärtaste zum Untergehen" + +#: squirrelbattle/tests/translations_test.py:35 +msgid "Main key to move left" +msgstr "Haupttaste zum Linksgehen" + +#: squirrelbattle/tests/translations_test.py:37 +msgid "Secondary key to move left" +msgstr "Sekundärtaste zum Linksgehen" + +#: squirrelbattle/tests/translations_test.py:39 +msgid "Main key to move right" +msgstr "Haupttaste zum Rechtsgehen" + +#: squirrelbattle/tests/translations_test.py:41 +msgid "Secondary key to move right" +msgstr "Sekundärtaste zum Rechtsgehen" + +#: squirrelbattle/tests/translations_test.py:43 +msgid "Key to validate a menu" +msgstr "Menütaste" + +#: squirrelbattle/tests/translations_test.py:45 +msgid "Texture pack" +msgstr "Textur-Packung" + +#: squirrelbattle/tests/translations_test.py:46 +msgid "Language" +msgstr "Sprache" + +#: squirrelbattle/tests/translations_test.py:49 +msgid "player" +msgstr "Spieler" + +#: squirrelbattle/tests/translations_test.py:51 +msgid "tiger" +msgstr "Tiger" + +#: squirrelbattle/tests/translations_test.py:52 +msgid "hedgehog" +msgstr "Igel" + +#: squirrelbattle/tests/translations_test.py:53 +msgid "rabbit" +msgstr "Kanninchen" + +#: squirrelbattle/tests/translations_test.py:54 +msgid "teddy bear" +msgstr "Teddybär" + +#: squirrelbattle/tests/translations_test.py:56 +msgid "bomb" +msgstr "Bombe" + +#: squirrelbattle/tests/translations_test.py:57 +msgid "heart" +msgstr "Herz" + +#: squirrelbattle/display/statsdisplay.py:34 +msgid "Inventory:" +msgstr "Bestand:" + +#: squirrelbattle/display/statsdisplay.py:39 +msgid "YOU ARE DEAD" +msgstr "SIE WURDEN GESTORBEN" + +#: squirrelbattle/interfaces.py:398 +#, python-brace-format +msgid "{name} hits {opponent}." +msgstr "{name} schlägt {opponent}." + +#: squirrelbattle/interfaces.py:410 +#, python-brace-format +msgid "{name} takes {amount} damage." +msgstr "{name} nimmt {amount} Schadenspunkte." + +#: squirrelbattle/interfaces.py:412 +#, python-brace-format +msgid "{name} dies." +msgstr "{name} stirbt." + +#: squirrelbattle/menus.py:71 +msgid "Back" +msgstr "Zurück" + +#: squirrelbattle/game.py:148 +msgid "" +"Some keys are missing in your save file.\n" +"Your save seems to be corrupt. It got deleted." +msgstr "" +"In Ihrer Speicherdatei fehlen einige Schlüssel.\n" +"Ihre Speicherung scheint korrupt zu sein. Es wird gelöscht." + +#: squirrelbattle/game.py:156 +msgid "" +"No player was found on this map!\n" +"Maybe you died?" +msgstr "" +"Auf dieser Karte wurde kein Spieler gefunden!\n" +"Vielleicht sind Sie gestorben?" + +#: squirrelbattle/game.py:176 +msgid "" +"The JSON file is not correct.\n" +"Your save seems corrupted. It got deleted." +msgstr "" +"Die JSON-Datei ist nicht korrekt.\n" +"Ihre Speicherung scheint korrumpiert. Sie wurde gelöscht." diff --git a/squirrelbattle/locale/en/LC_MESSAGES/squirrelbattle.po b/squirrelbattle/locale/en/LC_MESSAGES/squirrelbattle.po new file mode 100644 index 0000000..3f563fa --- /dev/null +++ b/squirrelbattle/locale/en/LC_MESSAGES/squirrelbattle.po @@ -0,0 +1,195 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse +# This file is distributed under the same license as the squirrelbattle package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: squirrelbattle 3.14.1\n" +"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n" +"POT-Creation-Date: 2020-11-28 16:03+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: squirrelbattle/display/statsdisplay.py:34 +msgid "Inventory:" +msgstr "" + +#: squirrelbattle/display/statsdisplay.py:39 +msgid "YOU ARE DEAD" +msgstr "" + +#: squirrelbattle/interfaces.py:394 squirrelbattle/interfaces.py:398 +#, python-brace-format +msgid "{name} hits {opponent}." +msgstr "" + +#: squirrelbattle/interfaces.py:405 squirrelbattle/interfaces.py:410 +#, python-brace-format +msgid "{name} takes {amount} damage." +msgstr "" + +#: squirrelbattle/menus.py:45 squirrelbattle/tests/translations_test.py:14 +#: squirrelbattle/tests/game_test.py:284 squirrelbattle/tests/game_test.py:287 +#: squirrelbattle/tests/translations_test.py:16 +msgid "New game" +msgstr "" + +#: squirrelbattle/menus.py:46 squirrelbattle/tests/translations_test.py:15 +#: squirrelbattle/tests/translations_test.py:17 +msgid "Resume" +msgstr "" + +#: squirrelbattle/menus.py:47 squirrelbattle/tests/translations_test.py:17 +#: squirrelbattle/tests/translations_test.py:19 +msgid "Save" +msgstr "" + +#: squirrelbattle/menus.py:48 squirrelbattle/tests/translations_test.py:16 +#: squirrelbattle/tests/translations_test.py:18 +msgid "Load" +msgstr "" + +#: squirrelbattle/menus.py:49 squirrelbattle/tests/translations_test.py:18 +#: squirrelbattle/tests/translations_test.py:20 +msgid "Settings" +msgstr "" + +#: squirrelbattle/menus.py:50 squirrelbattle/tests/translations_test.py:19 +#: squirrelbattle/tests/translations_test.py:21 +msgid "Exit" +msgstr "" + +#: squirrelbattle/menus.py:71 +msgid "Back" +msgstr "" + +#: squirrelbattle/game.py:147 squirrelbattle/game.py:148 +msgid "" +"Some keys are missing in your save file.\n" +"Your save seems to be corrupt. It got deleted." +msgstr "" + +#: squirrelbattle/game.py:155 squirrelbattle/game.py:156 +msgid "" +"No player was found on this map!\n" +"Maybe you died?" +msgstr "" + +#: squirrelbattle/game.py:175 squirrelbattle/game.py:176 +msgid "" +"The JSON file is not correct.\n" +"Your save seems corrupted. It got deleted." +msgstr "" + +#: squirrelbattle/settings.py:21 squirrelbattle/tests/translations_test.py:21 +#: squirrelbattle/tests/translations_test.py:25 +#: squirrelbattle/tests/translations_test.py:27 +msgid "Main key to move up" +msgstr "" + +#: squirrelbattle/settings.py:22 squirrelbattle/tests/translations_test.py:23 +#: squirrelbattle/tests/translations_test.py:27 +#: squirrelbattle/tests/translations_test.py:29 +msgid "Secondary key to move up" +msgstr "" + +#: squirrelbattle/settings.py:23 squirrelbattle/tests/translations_test.py:25 +#: squirrelbattle/tests/translations_test.py:29 +#: squirrelbattle/tests/translations_test.py:31 +msgid "Main key to move down" +msgstr "" + +#: squirrelbattle/settings.py:24 squirrelbattle/tests/translations_test.py:27 +#: squirrelbattle/tests/translations_test.py:31 +#: squirrelbattle/tests/translations_test.py:33 +msgid "Secondary key to move down" +msgstr "" + +#: squirrelbattle/settings.py:25 squirrelbattle/tests/translations_test.py:29 +#: squirrelbattle/tests/translations_test.py:33 +#: squirrelbattle/tests/translations_test.py:35 +msgid "Main key to move left" +msgstr "" + +#: squirrelbattle/settings.py:26 squirrelbattle/tests/translations_test.py:31 +#: squirrelbattle/tests/translations_test.py:35 +#: squirrelbattle/tests/translations_test.py:37 +msgid "Secondary key to move left" +msgstr "" + +#: squirrelbattle/settings.py:27 squirrelbattle/tests/translations_test.py:33 +#: squirrelbattle/tests/translations_test.py:37 +#: squirrelbattle/tests/translations_test.py:39 +msgid "Main key to move right" +msgstr "" + +#: squirrelbattle/settings.py:29 squirrelbattle/tests/translations_test.py:35 +#: squirrelbattle/tests/translations_test.py:39 +#: squirrelbattle/tests/translations_test.py:41 +msgid "Secondary key to move right" +msgstr "" + +#: squirrelbattle/settings.py:30 squirrelbattle/tests/translations_test.py:37 +#: squirrelbattle/tests/translations_test.py:41 +#: squirrelbattle/tests/translations_test.py:43 +msgid "Key to validate a menu" +msgstr "" + +#: squirrelbattle/settings.py:31 squirrelbattle/tests/translations_test.py:39 +#: squirrelbattle/tests/translations_test.py:43 +#: squirrelbattle/tests/translations_test.py:45 +msgid "Texture pack" +msgstr "" + +#: squirrelbattle/settings.py:32 squirrelbattle/tests/translations_test.py:40 +#: squirrelbattle/tests/translations_test.py:44 +#: squirrelbattle/tests/translations_test.py:46 +msgid "Language" +msgstr "" + +#: squirrelbattle/interfaces.py:407 squirrelbattle/interfaces.py:412 +#, python-brace-format +msgid "{name} dies." +msgstr "" + +#: squirrelbattle/tests/translations_test.py:47 +#: squirrelbattle/tests/translations_test.py:49 +msgid "player" +msgstr "" + +#: squirrelbattle/tests/translations_test.py:49 +#: squirrelbattle/tests/translations_test.py:51 +msgid "tiger" +msgstr "" + +#: squirrelbattle/tests/translations_test.py:50 +#: squirrelbattle/tests/translations_test.py:52 +msgid "hedgehog" +msgstr "" + +#: squirrelbattle/tests/translations_test.py:51 +#: squirrelbattle/tests/translations_test.py:53 +msgid "rabbit" +msgstr "" + +#: squirrelbattle/tests/translations_test.py:52 +#: squirrelbattle/tests/translations_test.py:54 +msgid "teddy bear" +msgstr "" + +#: squirrelbattle/tests/translations_test.py:54 +#: squirrelbattle/tests/translations_test.py:56 +msgid "bomb" +msgstr "" + +#: squirrelbattle/tests/translations_test.py:55 +#: squirrelbattle/tests/translations_test.py:57 +msgid "heart" +msgstr "" diff --git a/squirrelbattle/locale/fr/LC_MESSAGES/squirrelbattle.po b/squirrelbattle/locale/fr/LC_MESSAGES/squirrelbattle.po new file mode 100644 index 0000000..d46cee6 --- /dev/null +++ b/squirrelbattle/locale/fr/LC_MESSAGES/squirrelbattle.po @@ -0,0 +1,201 @@ +# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR ÿnérant, eichhornchen, nicomarg, charlse +# This file is distributed under the same license as the squirrelbattle package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: squirrelbattle 3.14.1\n" +"Report-Msgid-Bugs-To: squirrel-battle@crans.org\n" +"POT-Creation-Date: 2020-11-28 16:03+0100\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME \n" +"Language-Team: LANGUAGE \n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: squirrelbattle/display/statsdisplay.py:34 +msgid "Inventory:" +msgstr "Inventaire :" + +#: squirrelbattle/display/statsdisplay.py:39 +msgid "YOU ARE DEAD" +msgstr "VOUS ÊTES MORT" + +#: squirrelbattle/interfaces.py:394 squirrelbattle/interfaces.py:398 +#, python-brace-format +msgid "{name} hits {opponent}." +msgstr "{name} frappe {opponent}." + +#: squirrelbattle/interfaces.py:405 squirrelbattle/interfaces.py:410 +#, python-brace-format +msgid "{name} takes {amount} damage." +msgstr "{name} prend {amount} points de dégât." + +#: squirrelbattle/menus.py:45 squirrelbattle/tests/translations_test.py:14 +#: squirrelbattle/tests/game_test.py:284 squirrelbattle/tests/game_test.py:287 +#: squirrelbattle/tests/translations_test.py:16 +msgid "New game" +msgstr "Nouvelle partie" + +#: squirrelbattle/menus.py:46 squirrelbattle/tests/translations_test.py:15 +#: squirrelbattle/tests/translations_test.py:17 +msgid "Resume" +msgstr "Continuer" + +#: squirrelbattle/menus.py:47 squirrelbattle/tests/translations_test.py:17 +#: squirrelbattle/tests/translations_test.py:19 +msgid "Save" +msgstr "Sauvegarder" + +#: squirrelbattle/menus.py:48 squirrelbattle/tests/translations_test.py:16 +#: squirrelbattle/tests/translations_test.py:18 +msgid "Load" +msgstr "Charger" + +#: squirrelbattle/menus.py:49 squirrelbattle/tests/translations_test.py:18 +#: squirrelbattle/tests/translations_test.py:20 +msgid "Settings" +msgstr "Paramètres" + +#: squirrelbattle/menus.py:50 squirrelbattle/tests/translations_test.py:19 +#: squirrelbattle/tests/translations_test.py:21 +msgid "Exit" +msgstr "Quitter" + +#: squirrelbattle/menus.py:71 +msgid "Back" +msgstr "Retour" + +#: squirrelbattle/game.py:147 squirrelbattle/game.py:148 +msgid "" +"Some keys are missing in your save file.\n" +"Your save seems to be corrupt. It got deleted." +msgstr "" +"Certaines clés de votre ficher de sauvegarde sont manquantes.\n" +"Votre sauvegarde semble corrompue. Elle a été supprimée." + +#: squirrelbattle/game.py:155 squirrelbattle/game.py:156 +msgid "" +"No player was found on this map!\n" +"Maybe you died?" +msgstr "" +"Aucun joueur n'a été trouvé sur la carte !\n" +"Peut-être êtes-vous mort ?" + +#: squirrelbattle/game.py:175 squirrelbattle/game.py:176 +msgid "" +"The JSON file is not correct.\n" +"Your save seems corrupted. It got deleted." +msgstr "" +"Le fichier JSON de sauvegarde est incorrect.\n" +"Votre sauvegarde semble corrompue. Elle a été supprimée." + +#: squirrelbattle/settings.py:21 squirrelbattle/tests/translations_test.py:21 +#: squirrelbattle/tests/translations_test.py:25 +#: squirrelbattle/tests/translations_test.py:27 +msgid "Main key to move up" +msgstr "Touche principale pour aller vers le haut" + +#: squirrelbattle/settings.py:22 squirrelbattle/tests/translations_test.py:23 +#: squirrelbattle/tests/translations_test.py:27 +#: squirrelbattle/tests/translations_test.py:29 +msgid "Secondary key to move up" +msgstr "Touche secondaire pour aller vers le haut" + +#: squirrelbattle/settings.py:23 squirrelbattle/tests/translations_test.py:25 +#: squirrelbattle/tests/translations_test.py:29 +#: squirrelbattle/tests/translations_test.py:31 +msgid "Main key to move down" +msgstr "Touche principale pour aller vers le bas" + +#: squirrelbattle/settings.py:24 squirrelbattle/tests/translations_test.py:27 +#: squirrelbattle/tests/translations_test.py:31 +#: squirrelbattle/tests/translations_test.py:33 +msgid "Secondary key to move down" +msgstr "Touche secondaire pour aller vers le bas" + +#: squirrelbattle/settings.py:25 squirrelbattle/tests/translations_test.py:29 +#: squirrelbattle/tests/translations_test.py:33 +#: squirrelbattle/tests/translations_test.py:35 +msgid "Main key to move left" +msgstr "Touche principale pour aller vers la gauche" + +#: squirrelbattle/settings.py:26 squirrelbattle/tests/translations_test.py:31 +#: squirrelbattle/tests/translations_test.py:35 +#: squirrelbattle/tests/translations_test.py:37 +msgid "Secondary key to move left" +msgstr "Touche secondaire pour aller vers la gauche" + +#: squirrelbattle/settings.py:27 squirrelbattle/tests/translations_test.py:33 +#: squirrelbattle/tests/translations_test.py:37 +#: squirrelbattle/tests/translations_test.py:39 +msgid "Main key to move right" +msgstr "Touche principale pour aller vers la droite" + +#: squirrelbattle/settings.py:29 squirrelbattle/tests/translations_test.py:35 +#: squirrelbattle/tests/translations_test.py:39 +#: squirrelbattle/tests/translations_test.py:41 +msgid "Secondary key to move right" +msgstr "Touche secondaire pour aller vers la droite" + +#: squirrelbattle/settings.py:30 squirrelbattle/tests/translations_test.py:37 +#: squirrelbattle/tests/translations_test.py:41 +#: squirrelbattle/tests/translations_test.py:43 +msgid "Key to validate a menu" +msgstr "Touche pour valider un menu" + +#: squirrelbattle/settings.py:31 squirrelbattle/tests/translations_test.py:39 +#: squirrelbattle/tests/translations_test.py:43 +#: squirrelbattle/tests/translations_test.py:45 +msgid "Texture pack" +msgstr "Pack de textures" + +#: squirrelbattle/settings.py:32 squirrelbattle/tests/translations_test.py:40 +#: squirrelbattle/tests/translations_test.py:44 +#: squirrelbattle/tests/translations_test.py:46 +msgid "Language" +msgstr "Langue" + +#: squirrelbattle/interfaces.py:407 squirrelbattle/interfaces.py:412 +#, python-brace-format +msgid "{name} dies." +msgstr "{name} meurt." + +#: squirrelbattle/tests/translations_test.py:47 +#: squirrelbattle/tests/translations_test.py:49 +msgid "player" +msgstr "joueur" + +#: squirrelbattle/tests/translations_test.py:49 +#: squirrelbattle/tests/translations_test.py:51 +msgid "tiger" +msgstr "tigre" + +#: squirrelbattle/tests/translations_test.py:50 +#: squirrelbattle/tests/translations_test.py:52 +msgid "hedgehog" +msgstr "hérisson" + +#: squirrelbattle/tests/translations_test.py:51 +#: squirrelbattle/tests/translations_test.py:53 +msgid "rabbit" +msgstr "lapin" + +#: squirrelbattle/tests/translations_test.py:52 +#: squirrelbattle/tests/translations_test.py:54 +msgid "teddy bear" +msgstr "nounours" + +#: squirrelbattle/tests/translations_test.py:54 +#: squirrelbattle/tests/translations_test.py:56 +msgid "bomb" +msgstr "bombe" + +#: squirrelbattle/tests/translations_test.py:55 +#: squirrelbattle/tests/translations_test.py:57 +msgid "heart" +msgstr "cœur" diff --git a/squirrelbattle/menus.py b/squirrelbattle/menus.py index 31c50ea..4fcfabe 100644 --- a/squirrelbattle/menus.py +++ b/squirrelbattle/menus.py @@ -7,6 +7,7 @@ from typing import Any, Optional from .display.texturepack import TexturePack from .enums import GameMode, KeyValues, DisplayActions from .settings import Settings +from .translations import gettext as _, Translator class Menu: @@ -41,15 +42,15 @@ class MainMenuValues(Enum): """ Values of the main menu """ - START = 'Nouvelle partie' - RESUME = 'Continuer' - SAVE = 'Sauvegarder' - LOAD = 'Charger' - SETTINGS = 'Paramètres' - EXIT = 'Quitter' + START = "New game" + RESUME = "Resume" + SAVE = "Save" + LOAD = "Load" + SETTINGS = "Settings" + EXIT = "Exit" def __str__(self): - return self.value + return _(self.value) class MainMenu(Menu): @@ -67,7 +68,7 @@ class SettingsMenu(Menu): def update_values(self, settings: Settings) -> None: self.values = list(settings.__dict__.items()) - self.values.append(("RETURN", ["", "Retour"])) + self.values.append(("RETURN", ["", _("Back")])) def handle_key_pressed(self, key: Optional[KeyValues], raw_key: str, game: Any) -> None: @@ -95,6 +96,12 @@ class SettingsMenu(Menu): game.settings.TEXTURE_PACK) game.settings.write_settings() self.update_values(game.settings) + elif option == "LOCALE": + game.settings.LOCALE = 'fr' if game.settings.LOCALE == 'en'\ + else 'de' if game.settings.LOCALE == 'fr' else 'en' + Translator.setlocale(game.settings.LOCALE) + game.settings.write_settings() + self.update_values(game.settings) else: self.waiting_for_key = True self.update_values(game.settings) diff --git a/squirrelbattle/settings.py b/squirrelbattle/settings.py index 9601457..3090679 100644 --- a/squirrelbattle/settings.py +++ b/squirrelbattle/settings.py @@ -2,10 +2,12 @@ # SPDX-License-Identifier: GPL-3.0-or-later import json +import locale import os from typing import Any, Generator from .resources import ResourceManager +from .translations import gettext as _ class Settings: @@ -16,25 +18,17 @@ class Settings: We can define the setting by simply use settings.TEXTURE_PACK = 'new_key' """ def __init__(self): - self.KEY_UP_PRIMARY = \ - ['z', 'Touche principale pour aller vers le haut'] - self.KEY_UP_SECONDARY = \ - ['KEY_UP', 'Touche secondaire pour aller vers le haut'] - self.KEY_DOWN_PRIMARY = \ - ['s', 'Touche principale pour aller vers le bas'] - self.KEY_DOWN_SECONDARY = \ - ['KEY_DOWN', 'Touche secondaire pour aller vers le bas'] - self.KEY_LEFT_PRIMARY = \ - ['q', 'Touche principale pour aller vers la gauche'] - self.KEY_LEFT_SECONDARY = \ - ['KEY_LEFT', 'Touche secondaire pour aller vers la gauche'] - self.KEY_RIGHT_PRIMARY = \ - ['d', 'Touche principale pour aller vers la droite'] - self.KEY_RIGHT_SECONDARY = \ - ['KEY_RIGHT', 'Touche secondaire pour aller vers la droite'] - self.KEY_ENTER = \ - ['\n', 'Touche pour valider un menu'] - self.TEXTURE_PACK = ['ascii', 'Pack de textures utilisé'] + self.KEY_UP_PRIMARY = ['z', 'Main key to move up'] + self.KEY_UP_SECONDARY = ['KEY_UP', 'Secondary key to move up'] + self.KEY_DOWN_PRIMARY = ['s', 'Main key to move down'] + self.KEY_DOWN_SECONDARY = ['KEY_DOWN', 'Secondary key to move down'] + self.KEY_LEFT_PRIMARY = ['q', 'Main key to move left'] + self.KEY_LEFT_SECONDARY = ['KEY_LEFT', 'Secondary key to move left'] + self.KEY_RIGHT_PRIMARY = ['d', 'Main key to move right'] + self.KEY_RIGHT_SECONDARY = ['KEY_RIGHT', 'Secondary key to move right'] + self.KEY_ENTER = ['\n', 'Key to validate a menu'] + self.TEXTURE_PACK = ['ascii', 'Texture pack'] + self.LOCALE = [locale.getlocale()[0][:2], 'Language'] def __getattribute__(self, item: str) -> Any: superattribute = super().__getattribute__(item) @@ -53,10 +47,10 @@ class Settings: Retrieve the comment of a setting. """ if item in self.settings_keys: - return object.__getattribute__(self, item)[1] + return _(object.__getattribute__(self, item)[1]) for key in self.settings_keys: if getattr(self, key) == item: - return object.__getattribute__(self, key)[1] + return _(object.__getattribute__(self, key)[1]) @property def settings_keys(self) -> Generator[str, Any, None]: diff --git a/squirrelbattle/tests/entities_test.py b/squirrelbattle/tests/entities_test.py index 8f4e0c2..371bfc7 100644 --- a/squirrelbattle/tests/entities_test.py +++ b/squirrelbattle/tests/entities_test.py @@ -46,10 +46,10 @@ class TestEntities(unittest.TestCase): self.assertEqual(entity.strength, 2) for _ in range(9): self.assertEqual(entity.hit(entity), - "tiger hits tiger. tiger takes 2 damage.") + "Tiger hits tiger. Tiger takes 2 damage.") self.assertFalse(entity.dead) - self.assertEqual(entity.hit(entity), "tiger hits tiger. " - + "tiger takes 2 damage. tiger dies.") + self.assertEqual(entity.hit(entity), "Tiger hits tiger. " + + "Tiger takes 2 damage. Tiger dies.") self.assertTrue(entity.dead) entity = Rabbit() @@ -70,8 +70,8 @@ class TestEntities(unittest.TestCase): self.assertTrue(entity.y == 2 and entity.x == 6) self.assertEqual(old_health - entity.strength, self.player.health) self.assertEqual(self.map.logs.messages[-1], - f"{entity.name} hits {self.player.name}. \ -{self.player.name} takes {entity.strength} damage.") + f"{entity.name.capitalize()} hits {self.player.name}. \ +{self.player.name.capitalize()} takes {entity.strength} damage.") # Fight the rabbit old_health = entity.health diff --git a/squirrelbattle/tests/game_test.py b/squirrelbattle/tests/game_test.py index 5081912..a23b6f9 100644 --- a/squirrelbattle/tests/game_test.py +++ b/squirrelbattle/tests/game_test.py @@ -4,17 +4,16 @@ import os import unittest -from squirrelbattle.resources import ResourceManager - -from squirrelbattle.enums import DisplayActions - -from squirrelbattle.bootstrap import Bootstrap -from squirrelbattle.display.display import Display -from squirrelbattle.display.display_manager import DisplayManager -from squirrelbattle.entities.player import Player -from squirrelbattle.game import Game, KeyValues, GameMode -from squirrelbattle.menus import MainMenuValues -from squirrelbattle.settings import Settings +from ..bootstrap import Bootstrap +from ..display.display import Display +from ..display.display_manager import DisplayManager +from ..entities.player import Player +from ..enums import DisplayActions +from ..game import Game, KeyValues, GameMode +from ..menus import MainMenuValues +from ..resources import ResourceManager +from ..settings import Settings +from ..translations import gettext as _, Translator class TestGame(unittest.TestCase): @@ -275,12 +274,23 @@ class TestGame(unittest.TestCase): self.game.handle_key_pressed(KeyValues.ENTER) self.assertEqual(self.game.settings.TEXTURE_PACK, "ascii") + # Change language + Translator.compilemessages() + Translator.refresh_translations() + self.game.settings.LOCALE = "en" + self.game.handle_key_pressed(KeyValues.DOWN) + self.game.handle_key_pressed(KeyValues.ENTER) + self.assertEqual(self.game.settings.LOCALE, "fr") + self.assertEqual(_("New game"), "Nouvelle partie") + self.game.handle_key_pressed(KeyValues.ENTER) + self.assertEqual(self.game.settings.LOCALE, "de") + self.assertEqual(_("New game"), "Neu Spiel") + self.game.handle_key_pressed(KeyValues.ENTER) + self.assertEqual(self.game.settings.LOCALE, "en") + self.assertEqual(_("New game"), "New game") + # Navigate to "back" button self.game.handle_key_pressed(KeyValues.DOWN) - self.game.handle_key_pressed(KeyValues.DOWN) - self.game.handle_key_pressed(KeyValues.DOWN) - self.game.handle_key_pressed(KeyValues.DOWN) - self.game.handle_key_pressed(KeyValues.DOWN) self.game.handle_key_pressed(KeyValues.ENTER) self.assertEqual(self.game.state, GameMode.MAINMENU) diff --git a/squirrelbattle/tests/settings_test.py b/squirrelbattle/tests/settings_test.py index cef60c0..b0d9739 100644 --- a/squirrelbattle/tests/settings_test.py +++ b/squirrelbattle/tests/settings_test.py @@ -24,7 +24,7 @@ class TestSettings(unittest.TestCase): self.assertEqual(settings.get_comment(settings.TEXTURE_PACK), settings.get_comment('TEXTURE_PACK')) self.assertEqual(settings.get_comment(settings.TEXTURE_PACK), - 'Pack de textures utilisé') + 'Texture pack') settings.TEXTURE_PACK = 'squirrel' self.assertEqual(settings.TEXTURE_PACK, 'squirrel') diff --git a/squirrelbattle/tests/translations_test.py b/squirrelbattle/tests/translations_test.py new file mode 100644 index 0000000..6c18840 --- /dev/null +++ b/squirrelbattle/tests/translations_test.py @@ -0,0 +1,57 @@ +import unittest + +from squirrelbattle.translations import gettext as _, Translator + + +class TestTranslations(unittest.TestCase): + def setUp(self) -> None: + Translator.compilemessages() + Translator.refresh_translations() + Translator.setlocale("fr") + + def test_main_menu_translation(self) -> None: + """ + Ensure that the main menu is translated. + """ + self.assertEqual(_("New game"), "Nouvelle partie") + self.assertEqual(_("Resume"), "Continuer") + self.assertEqual(_("Load"), "Charger") + self.assertEqual(_("Save"), "Sauvegarder") + self.assertEqual(_("Settings"), "Paramètres") + self.assertEqual(_("Exit"), "Quitter") + + def test_settings_menu_translation(self) -> None: + """ + Ensure that the settings menu is translated. + """ + self.assertEqual(_("Main key to move up"), + "Touche principale pour aller vers le haut") + self.assertEqual(_("Secondary key to move up"), + "Touche secondaire pour aller vers le haut") + self.assertEqual(_("Main key to move down"), + "Touche principale pour aller vers le bas") + self.assertEqual(_("Secondary key to move down"), + "Touche secondaire pour aller vers le bas") + self.assertEqual(_("Main key to move left"), + "Touche principale pour aller vers la gauche") + self.assertEqual(_("Secondary key to move left"), + "Touche secondaire pour aller vers la gauche") + self.assertEqual(_("Main key to move right"), + "Touche principale pour aller vers la droite") + self.assertEqual(_("Secondary key to move right"), + "Touche secondaire pour aller vers la droite") + self.assertEqual(_("Key to validate a menu"), + "Touche pour valider un menu") + self.assertEqual(_("Texture pack"), "Pack de textures") + self.assertEqual(_("Language"), "Langue") + + def test_entities_translation(self) -> None: + self.assertEqual(_("player"), "joueur") + + self.assertEqual(_("tiger"), "tigre") + self.assertEqual(_("hedgehog"), "hérisson") + self.assertEqual(_("rabbit"), "lapin") + self.assertEqual(_("teddy bear"), "nounours") + + self.assertEqual(_("bomb"), "bombe") + self.assertEqual(_("heart"), "cœur") diff --git a/squirrelbattle/translations.py b/squirrelbattle/translations.py new file mode 100644 index 0000000..f532bb0 --- /dev/null +++ b/squirrelbattle/translations.py @@ -0,0 +1,96 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + +import gettext as gt +import os +import subprocess +from pathlib import Path +from typing import Any, List + + +class Translator: + """ + This module uses gettext to translate strings. + Translator.setlocale defines the language of the strings, + then gettext() translates the message. + """ + SUPPORTED_LOCALES: List[str] = ["de", "en", "fr"] + locale: str = "en" + translators: dict = {} + + @classmethod + def refresh_translations(cls) -> None: + """ + Load compiled translations. + """ + for language in cls.SUPPORTED_LOCALES: + rep = Path(__file__).parent / "locale" / language / "LC_MESSAGES" + rep.mkdir(parents=True) if not rep.is_dir() else None + if os.path.isfile(rep / "squirrelbattle.mo"): + cls.translators[language] = gt.translation( + "squirrelbattle", + localedir=Path(__file__).parent / "locale", + languages=[language], + ) + + @classmethod + def setlocale(cls, lang: str) -> None: + """ + Define the language used to translate the game. + The language must be supported, otherwise nothing is done. + """ + lang = lang[:2] + if lang in cls.SUPPORTED_LOCALES: + cls.locale = lang + + @classmethod + def get_translator(cls) -> Any: + return cls.translators.get(cls.locale, gt.NullTranslations()) + + @classmethod + def makemessages(cls) -> None: # pragma: no cover + """ + Analyse all strings in the project and extract them. + """ + for language in cls.SUPPORTED_LOCALES: + file_name = Path(__file__).parent / "locale" / language \ + / "LC_MESSAGES" / "squirrelbattle.po" + args = ["find", "squirrelbattle", "-iname", "*.py"] + find = subprocess.Popen(args, cwd=Path(__file__).parent.parent, + stdout=subprocess.PIPE) + args = ["xargs", "xgettext", "--from-code", "utf-8", + "--add-comments", + "--package-name=squirrelbattle", + "--package-version=3.14.1", + "--copyright-holder=ÿnérant, eichhornchen, " + "nicomarg, charlse", + "--msgid-bugs-address=squirrel-battle@crans.org", + "-o", file_name] + if file_name.is_file(): + args.append("--join-existing") + print(f"Make {language} messages...") + subprocess.Popen(args, stdin=find.stdout).wait() + + @classmethod + def compilemessages(cls) -> None: + """ + Compile translation messages from source files. + """ + for language in cls.SUPPORTED_LOCALES: + args = ["msgfmt", "--check-format", + "-o", Path(__file__).parent / "locale" / language + / "LC_MESSAGES" / "squirrelbattle.mo", + Path(__file__).parent / "locale" / language + / "LC_MESSAGES" / "squirrelbattle.po"] + print(f"Compiling {language} messages...") + subprocess.Popen(args).wait() + + +def gettext(message: str) -> str: + """ + Translate a message. + """ + return Translator.get_translator().gettext(message) + + +Translator.refresh_translations()