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 6cb844c..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/ @@ -45,3 +48,5 @@ build-deb: paths: - build/*.deb expire_in: 1 week + only: + - master diff --git a/LICENSE b/COPYING similarity index 99% rename from LICENSE rename to COPYING index 7d821ed..1bc08a5 100644 --- a/LICENSE +++ b/COPYING @@ -632,7 +632,7 @@ state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Squirrel Battle - Copyright (C) 2020 ynerant + Copyright (C) 2020 ÿnérant, eichhornchen, nicomarg, charlse This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -652,7 +652,7 @@ Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: - Squirrel Battle Copyright (C) 2020 ynerant + Squirrel Battle Copyright (C) 2020 ÿnérant, eichhornchen, nicomarg, charlse This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. diff --git a/README.md b/README.md index d340eee..6ad3063 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ [![pipeline status](https://gitlab.crans.org/ynerant/squirrel-battle/badges/master/pipeline.svg)](https://gitlab.crans.org/ynerant/squirrel-battle/-/commits/master) [![coverage report](https://gitlab.crans.org/ynerant/squirrel-battle/badges/master/coverage.svg)](https://gitlab.crans.org/ynerant/squirrel-battle/-/commits/master) [![Documentation Status](https://readthedocs.org/projects/squirrel-battle/badge/?version=latest)](https://squirrel-battle.readthedocs.io/fr/latest/?badge=latest) -[![PyPI](https://img.shields.io/pypi/v/dungeon-battle)](https://pypi.org/project/squirrel-battle/) +[![PyPI](https://img.shields.io/pypi/v/squirrel-battle)](https://pypi.org/project/squirrel-battle/) [![PYPI downloads](https://img.shields.io/pypi/dm/squirrel-battle)](https://pypi.org/project/squirrel-battle/) [![AUR version](https://img.shields.io/aur/version/python-squirrel-battle)](https://aur.archlinux.org/packages/python-squirrel-battle/) [![License: GPL v3](https://img.shields.io/badge/License-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0.txt) diff --git a/debian/README.debian b/debian/README.debian index 8404efb..6961b22 100644 --- a/debian/README.debian +++ b/debian/README.debian @@ -2,4 +2,4 @@ Squirrel Battle Watch out for squirrel's knifes! - -- Yohann D'ANELLO Thu, 19 Nov 2020 03:30:42 +0100 + -- Yohann D'ANELLO Thu, 19 Nov 2020 03:30:42 +0100 diff --git a/debian/changelog b/debian/changelog index d1afdad..2399e41 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,5 +1,11 @@ -python3-squirrel-battle (3.14) beta; urgency=low +python3-squirrel-battle (3.14.1) beta; urgency=low + + * Some graphical improvements. + + -- Yohann D'ANELLO Thu, 27 Nov 2020 18:25:42 +0100 + + python3-squirrel-battle (3.14) beta; urgency=low * Initial release. - -- Yohann D'ANELLO Thu, 19 Nov 2020 03:30:42 +0100 + -- Yohann D'ANELLO Thu, 19 Nov 2020 03:30:42 +0100 diff --git a/debian/control b/debian/control index e36e424..b59997d 100644 --- a/debian/control +++ b/debian/control @@ -1,8 +1,8 @@ Source: python3-squirrel-battle Section: devel Priority: optional -Maintainer: ynerant -Build-Depends: debhelper (>=10~), dh-python, python3-all, python3-setuptools +Maintainer: ynerant +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/debian/copyright b/debian/copyright index c616d56..6d35767 100644 --- a/debian/copyright +++ b/debian/copyright @@ -1,11 +1,11 @@ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ -Upstream-Name: Yohann D'ANELLO -Upstream-Contact: Yohann D'ANELLO +Upstream-Name: ÿnérant, eichhornchen, nicomarg, charlse +Upstream-Contact: ÿnérant, eichhornchen, nicomarg, charlse Source: https://gitlab.crans.org/ynerant/squirrel-battle Files: * -Copyright: 2020 Yohann D'ANELLO +Copyright: 2020 ÿnérant, eichhornchen, nicomarg, charlse License: GPL-3+ This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public diff --git a/docs/deployment.rst b/docs/deployment.rst new file mode 100644 index 0000000..9477a10 --- /dev/null +++ b/docs/deployment.rst @@ -0,0 +1,323 @@ +Déploiement du projet +===================== + +.. _PyPI: https://pypi.org/project/squirrel-battle/ +.. _AUR: https://aur.archlinux.org/packages/python-squirrel-battle/ +.. _Debian: https://gitlab.crans.org/ynerant/squirrel-battle/-/jobs/artifacts/master/raw/build/python3-squirrelbattle_3.14.1_all.deb?job=build-deb +.. _installation: install.html + +À chaque nouvelle version du projet, il est compilé et déployé dans PyPI_, dans +l'AUR_ et un paquet Debian_ est créé, voir la page d'installation_. + + +PyPI +---- + +Définition du paquet +~~~~~~~~~~~~~~~~~~~~ + +.. _setup.py: https://gitlab.crans.org/ynerant/squirrel-battle/-/blob/master/setup.py + +La documentation sur le packaging dans PyPI_ est disponible `ici +`_. + +Le fichier `setup.py`_ contient l'ensemble des instructions d'installation du +paquet ainsi que des détails à fournir à PyPI : + +.. code:: python + + #!/usr/bin/env python3 + import os + + 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", + author="ÿnérant, eichhornchen, nicomarg, charlse", + author_email="squirrel-battle@crans.org", + description="Watch out for squirrel's knives!", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://gitlab.crans.org/ynerant/squirrel-battle", + packages=find_packages(), + license='GPLv3', + classifiers=[ + "Development Status :: 4 - Beta", + "Environment :: Console :: Curses", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", + "Natural Language :: French", + "Operating System :: OS Independent", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Topic :: Games/Entertainment", + ], + python_requires='>=3.6', + include_package_data=True, + package_data={"squirrelbattle": ["assets/*", "locale/*/*/*.mo"]}, + entry_points={ + "console_scripts": [ + "squirrel-battle = squirrelbattle.bootstrap:Bootstrap.run_game", + ] + } + ) + +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. + + +Installation locale du paquet +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +L'installation du paquet localement dans son environnement Python (virtuel ou non) +peut se faire en exécutant ``pip install -e .``. + + +Génération des binaires +~~~~~~~~~~~~~~~~~~~~~~~ + +Les paquets ``setuptools`` (``python3-setuptools`` pour APT, ``python-setuptools`` +pour pacman) et ``wheel`` (``python3-wheel`` pour APT, ``python-wheel`` pour pacman) +sont nécessaires. Une fois installés, il faut appeler la commande : + +.. code:: bash + + python3 setup.py sdist bdist_wheel + +Une fois cela fait, le dossier ``dist/`` contiendra les archives à transmettre à PyPI. + + +Publier sur PyPI +~~~~~~~~~~~~~~~~ + +Il faut avant tout avoir un compte sur PyPI. Dans `votre compte PyPI +`_, il faut générer un jeton d'accès API. + +Dans le fichier ``.pypirc`` dans le répertoire principal de l'utilisateur, +il faut ajouter le jeton d'accès : + +.. code:: + + [pypi] + username = __token__ + password = pypi-my-pypi-api-access-token + +Cela permet de s'authentifier directement par ce jeton. + +Ensuite, il faut installer ``twine``, qui permet de publier sur PyPI. + +Il suffit ensuite d'appeler : + +.. code:: bash + + twine upload dist/* + +pour envoyer le paquet sur PyPI. + + +.. note:: + + À des fins de tests, il est possible d'utiliser le dépôt ``_. + Les différences sont au niveau de l'authentification, où il faut l'en-tête + ``[testpypi]`` dans le ``.pypirc``, et il faut envoyer le paquet avec + ``twine upload --repository testpypi dist/``. + + +Publier dans l'AUR +------------------ + +Fonctionnement du packaging +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. _python-squirrel-battle: https://aur.archlinux.org/packages/python-squirrel-battle/ +.. _python-squirrel-battle-git: https://aur.archlinux.org/packages/python-squirrel-battle-git/ + +Deux paquets sont publiés dans l'AUR (Arch User Repository) : + +- python-squirrel-battle_ +- python-squirrel-battle-git_ + +Le packaging dans Arch Linux se fait en commitant un fichier ``PKGBUILD`` dans +le dépôt à l'adresse ``ssh://aur@aur.archlinux.org/packagename.git``, +en remplaçant ``packagename`` par le nom du paquet. + +Le second paquet compile directement le jeu à partir de la branche ``master`` +du dépôt Git. Le fichier ``PKGBUILD`` dispose de cette structure : + +.. code:: + + # Maintainer: Yohann D'ANELLO + + pkgbase=squirrel-battle + pkgname=python-squirrel-battle-git + pkgver=3.14.1 + pkgrel=1 + pkgdesc="Watch out for squirrel's knives!" + arch=('any') + url="https://gitlab.crans.org/ynerant/squirrel-battle" + license=('GPLv3') + depends=('python') + makedepends=('gettext' 'python-setuptools') + depends=('noto-fonts-emoji') + checkdepends=('python-tox') + ssource=("git+https://gitlab.crans.org/ynerant/squirrel-battle.git") + sha256sums=("SKIP") + + pkgver() { + cd pkgbase + git describe --long --tags | sed -r 's/^v//;s/([^-]*-g)/r\1/;s/-/./g' + } + build() { + cd $pkgbase + python setup.py build + } + + check() { + cd $pkgbase + tox -e py3 + tox -e linters + } + + package() { + cd $pkgbase + python setup.py install --skip-build \ + --optimize=1 \ + --root="${pkgdir}" + install -vDm 644 README.md \ + -t "${pkgdir}/usr/share/doc/${pkgname}" + install -vDm 644 LICENSE -t "${pkgdir}/usr/share/licenses/${pkgname}" + } + +Ces instructions permettent de cloner le dépôt, l'installer et exécuter des tests, +en plus de définir les attributs du paquet. + +Le fichier ``PKGBUILD`` du paquet ``python-squirrel-battle``, synchronisé avec +les releases, est plus ou moins similaire : + +.. code:: + + # Maintainer: Yohann D'ANELLO + + pkgbase=squirrel-battle + pkgname=python-squirrel-battle + pkgver=3.14.1 + pkgrel=1 + pkgdesc="Watch out for squirrel's knives!" + arch=('any') + url="https://gitlab.crans.org/ynerant/squirrel-battle" + license=('GPLv3') + depends=('python') + 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") + sha256sums=("6090534d598c0b3a8f5acdb553c12908ba8107d62d08e17747d1dbb397bddef0") + + build() { + cd $pkgbase-v$pkgver + python setup.py build + } + + check() { + cd $pkgbase-v$pkgver + tox -e py3 + tox -e linters + } + + package() { + cd $pkgbase-v$pkgver + python setup.py install --skip-build \ + --optimize=1 \ + --root="${pkgdir}" + install -vDm 644 README.md \ + -t "${pkgdir}/usr/share/doc/${pkgname}" + install -vDm 644 LICENSE -t "${pkgdir}/usr/share/licenses/${pkgname}" + } + +Il se contente ici de télécharger l'archive de la dernière release, et de travailler +dessus. + + +Mettre à jour +~~~~~~~~~~~~~ + +Pour mettre à jour le dépôt, une fois les dépôts +``ssh://aur@aur.archlinux.org/python-squirrel-battle.git`` et +``ssh://aur@aur.archlinux.org/python-squirrel-battle-git.git`` clonés, +il suffit de mettre à jour le paramètre ``pkgver`` pour la bonne version, +de régénérer le fichier ``.SRCINFO`` en faisant +``makepkg --printsrcinfo > .SRCINFO``, puis de committer/pousser. + + +Construction du paquet Debian +----------------------------- + +Structure du paquet +------------------- + +L'ensemble des instructions pour construire le paquet Debian est situé dans le +dossier ``debian/``. + +Le fichier ``changelog`` est à modifier à chaque nouvelle version, le fichier +``compat`` contient la version minimale de Debian requise (``10`` pour Debian +Buster), le fichier ``copyright`` contient la liste des fichiers distribués sous +quelle licence (ici GPLv3), le fichier ``control`` contient les informations du +paquet, le fichier ``install`` les fichiers de configuration à installer +(ici le fix de l'affichage de l'écurueil), et enfin le fichier ``rules`` l'ensemble +des instructions à exécuter pour installer. + +Le paquet ``fonts-noto-color-emoji`` est en dépendance pour le bon affichage +des émojis. + +Mettre à jour le paquet +----------------------- + +Pour changer la version du paquet, il faut ajouter des lignes dans le fichier +``changelog``. + + +Construire le paquet +-------------------- + +Il faut partir d'une installation de Debian. + +D'abord on installe les paquets nécessaires : + +.. code:: + + apt update + apt --no-install-recommends install build-essential debmake dh-python debhelper gettext python3-all python3-setuptools + +On peut ensuite construire le paquet : + +.. code:: bash + + dpkg-buildpackage + mkdir build && cp ../*.deb build/ + +Le paquet sera installé dans ``build/python3-squirrel-battle_3.14.1_all.deb``. + +Le paquet Debian_ est construit par l'intégration continue Gitlab et ajouté +à chaque release. diff --git a/docs/documentation.rst b/docs/documentation.rst new file mode 100644 index 0000000..74965be --- /dev/null +++ b/docs/documentation.rst @@ -0,0 +1,31 @@ +Documentation +============= + +La documentation est gérée grâce à Sphinx. Le thème est le thème officiel de +ReadTheDocs ``sphinx-rtd-theme``. + +Générer localement la documentation +----------------------------------- + +On commence par se rendre au bon endroit et installer les bonnes dépendances : + +.. code:: bash + + cd docs + pip install -r requirements.txt + +La documentation se génère à partir d'appels à ``make``, selon le type de +documentation voulue. + +Par exemple, ``make html`` construit la documentation web, ``make latexpdf`` +construit un livre PDF avec cette documentation. + + +Documentation externe +--------------------- + +À chaque commit, un webhook est envoyé à ``_, qui construit +tout seul la documentation Sphinx, la publiant à l'adresse +``_. + +De plus, les documentations sont sauvegardées à chaque release taguée. diff --git a/docs/entities/monsters.rst b/docs/entities/monsters.rst index b719c1b..b6f287c 100644 --- a/docs/entities/monsters.rst +++ b/docs/entities/monsters.rst @@ -25,14 +25,14 @@ Dans le `pack de textures`_ ASCII, il est représenté par le caractère ``*``. Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🦔``. -Castor ------- +Tigre +----- -Son nom est fixé à `beaver`. Il a par défaut une force à **2** et **20** points de vie. +Son nom est fixé à `tiger`. Il a par défaut une force à **2** et **20** points de vie. -Dans le `pack de textures`_ ASCII, il est représenté par le caractère ``_``. +Dans le `pack de textures`_ ASCII, il est représenté par le caractère ``n``. -Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🦫``. +Dans le `pack de textures`_ écureuil, il est représenté par l'émoji ``🐅``. Lapin diff --git a/docs/index.rst b/docs/index.rst index ce312c3..1cb7d83 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -13,11 +13,11 @@ Bienvenue dans la documentation de Squirrel Battle ! :target: https://squirrel-battle.readthedocs.io/fr/latest/?badge=latest :alt: Documentation Status -.. image:: https://img.shields.io/pypi/v/dungeon-battle +.. image:: https://img.shields.io/pypi/v/squirrel-battle :target: https://pypi.org/project/squirrel-battle/ :alt: PyPI -.. image:: https://img.shields.io/pypi/dm/dungeon-battle +.. image:: https://img.shields.io/pypi/dm/squirrel-battle :target: https://pypi.org/project/squirrel-battle/ :alt: PyPI downloads @@ -37,6 +37,9 @@ Bienvenue dans la documentation de Squirrel Battle ! install-dev tests display/index + translation + deployment + documentation .. toctree:: :maxdepth: 3 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/install.rst b/docs/install.rst index d73b5a9..5cc2351 100644 --- a/docs/install.rst +++ b/docs/install.rst @@ -61,29 +61,19 @@ Le jeu peut être ensuite lancé via la commande ``squirrel-battle``. Sur Ubuntu/Debian ~~~~~~~~~~~~~~~~~ -.. _paquet: https://gitlab.crans.org/ynerant/squirrel-battle/-/jobs/artifacts/master/raw/build/python3-squirrelbattle_3.14_all.deb?job=build-deb +.. _paquet: https://gitlab.crans.org/ynerant/squirrel-battle/-/jobs/artifacts/master/raw/build/python3-squirrelbattle_3.14.1_all.deb?job=build-deb Un paquet_ est généré par l'intégration continue de Gitlab à chaque commit. Ils sont également attachés à chaque nouvelle release. Il dépend du paquet ``fonts-noto-color-emoji``, permettant d'afficher les émojis -dans le terminal. Il peut être installé via APT normalement sur une distribution -récente, toutefois sur les versions les plus vieilles, incluant Debian Buster, -certains émojis n'apparaissent pas. Il est essentiel de maintenir ce paquet à -jour. Pour installer manuellement la dernière version de ce paquet, -il suffit d'exécuter : - -.. code:: bash - - wget http://ftp.fr.debian.org/debian/pool/main/f/fonts-noto-color-emoji/fonts-noto-color-emoji_0~20200916-1_all.deb - dpkg -i fonts-noto-color-emoji_0~20200916-1_all.deb - rm fonts-noto-color-emoji_0~20200916-1_all.deb +dans le terminal. Il peut être installé via APT. Pour installer ce paquet, il suffit de le télécharger et d'appeler ``dpkg`` : .. code:: bash - dpkg -i python3-squirrelbattle_3.14_all.deb + dpkg -i python3-squirrelbattle_3.14.1_all.deb Ce paquet inclut un patch pour afficher les émojis écureuil correctement. diff --git a/docs/texture-pack.rst b/docs/texture-pack.rst index 3bb5b4d..377a3cf 100644 --- a/docs/texture-pack.rst +++ b/docs/texture-pack.rst @@ -12,7 +12,7 @@ Pack de textures .. _Cœur: entities/items.html#coeur .. _Bombe: entities/items.html#bombe .. _Lapin: entities/monsters.html#lapin -.. _Castor: entities/monsters.html#castor +.. _Tigre: entities/monsters.html#tigre .. _Nounours: entities/monsters.html#nounours Chaque entité_ et chaque tuile_ de la carte_ est associé à un caractère pour @@ -42,7 +42,7 @@ Chaque tuile fait un caractère de large. * Cœur_ : ``❤`` * Bombe_ : ``o`` * Lapin_ : ``Y`` - * Castor_ : ``_`` + * Tigre_ : ``n`` * Nounours_ : ``8`` @@ -61,5 +61,5 @@ Chaque tuile fait 2 caractères de large pour afficher les émojis proprement. * Cœur_ : ``💜`` * Bombe_ : ``💣`` * Lapin_ : ``🐇`` - * Castor_ : ``🦫`` + * Tigre_ : ``🐅`` * Nounours_ : ``🧸`` 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/docs/troubleshooting.rst b/docs/troubleshooting.rst index 8a26c19..6e400f6 100644 --- a/docs/troubleshooting.rst +++ b/docs/troubleshooting.rst @@ -24,21 +24,12 @@ Sous Ubuntu/Debian ^^^^^^^^^^^^^^^^^^ À nouveau, le terminal `xfce4-terminal` est recommandé. Le paquet -`fonts-noto-color-emoji`. Toutefois, le rythme de mise à jour de Debian étant -lent, le paquet le plus récent ne contient pas tous les émojis. Sur Debian, -il faudra donc installer le paquet le plus récent, ce qui fonctionne sans -dépendance supplémentaire : +`fonts-noto-color-emoji`. -.. code:: bash - - wget http://ftp.fr.debian.org/debian/pool/main/f/fonts-noto-color-emoji/fonts-noto-color-emoji_0~20200916-1_all.deb - dpkg -i fonts-noto-color-emoji_0~20200916-1_all.deb - rm fonts-noto-color-emoji_0~20200916-1_all.deb - -Il reste le problème de l'écureuil. Sous Ubuntu et Debian, le caractère écureuil -existe déjà, mais ne s'affiche pas proprement. On peut appliquer un patch qui -permet d'afficher les émojis correctement dans son terminal. Pour cela, il -suffit de faire : +Toutefois, un problème reste avec l'écureuil. Sous Ubuntu et Debian, le +caractère écureuil existe déjà, mais ne s'affiche pas proprement. On peut +appliquer un patch qui permet d'afficher les émojis correctement dans son +terminal. Pour cela, il suffit de faire : .. code:: bash diff --git a/main.py b/main.py index 64972f1..fbbbb35 100755 --- a/main.py +++ b/main.py @@ -1,5 +1,25 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 + +# 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 dfb15ef..f051bbb 100644 --- a/setup.py +++ b/setup.py @@ -1,17 +1,31 @@ #!/usr/bin/env python3 -import os + +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + +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", - author="ynerant", - author_email="ynerant@crans.org", - description="Watch out for squirrel's knifes!", + version="3.14.1", + author="ÿnérant, eichhornchen, nicomarg, charlse", + author_email="squirrel-battle@crans.org", + description="Watch out for squirrel's knives!", long_description=long_description, long_description_content_type="text/markdown", url="https://gitlab.crans.org/ynerant/squirrel-battle", @@ -32,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/__init__.py b/squirrelbattle/__init__.py index e69de29..1cc6688 100644 --- a/squirrelbattle/__init__.py +++ b/squirrelbattle/__init__.py @@ -0,0 +1,2 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/squirrelbattle/bootstrap.py b/squirrelbattle/bootstrap.py index 45c2ad1..f041aef 100644 --- a/squirrelbattle/bootstrap.py +++ b/squirrelbattle/bootstrap.py @@ -1,3 +1,6 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + from squirrelbattle.game import Game from squirrelbattle.display.display_manager import DisplayManager from squirrelbattle.term_manager import TermManager diff --git a/squirrelbattle/display/__init__.py b/squirrelbattle/display/__init__.py index e69de29..1cc6688 100644 --- a/squirrelbattle/display/__init__.py +++ b/squirrelbattle/display/__init__.py @@ -0,0 +1,2 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/squirrelbattle/display/display.py b/squirrelbattle/display/display.py index 1e47189..9cc1456 100644 --- a/squirrelbattle/display/display.py +++ b/squirrelbattle/display/display.py @@ -1,3 +1,6 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + import curses from typing import Any, Optional, Union @@ -19,6 +22,24 @@ class Display: def newpad(self, height: int, width: int) -> Union[FakePad, Any]: return curses.newpad(height, width) if self.screen else FakePad() + def truncate(self, msg: str, height: int, width: int) -> str: + height = max(0, height) + width = max(0, width) + lines = msg.split("\n") + lines = lines[:height] + lines = [line[:width] for line in lines] + return "\n".join(lines) + + def addstr(self, pad: Any, y: int, x: int, msg: str, *options) -> None: + """ + Display a message onto the pad. + If the message is too large, it is truncated vertically and horizontally + """ + height, width = pad.getmaxyx() + msg = self.truncate(msg, height - y, width - x - 1) + if msg.replace("\n", "") and x >= 0 and y >= 0: + return pad.addstr(y, x, msg, *options) + def init_pair(self, number: int, foreground: int, background: int) -> None: return curses.init_pair(number, foreground, background) \ if self.screen else None @@ -32,14 +53,36 @@ class Display: self.y = y self.width = width self.height = height - if hasattr(self, "pad") and resize_pad: - self.pad.resize(self.height, self.width) + if hasattr(self, "pad") and resize_pad and \ + self.height >= 0 and self.width >= 0: + self.pad.resize(self.height + 1, self.width + 1) def refresh(self, *args, resize_pad: bool = True) -> None: if len(args) == 4: self.resize(*args, resize_pad) self.display() + def refresh_pad(self, pad: Any, top_y: int, top_x: int, + window_y: int, window_x: int, + last_y: int, last_x: int) -> None: + """ + Refresh a pad on a part of the window. + The refresh starts at coordinates (top_y, top_x) from the pad, + and is drawn from (window_y, window_x) to (last_y, last_x). + If coordinates are invalid (negative indexes/length..., then nothing + is drawn and no error is raised. + """ + top_y, top_x = max(0, top_y), max(0, top_x) + window_y, window_x = max(0, window_y), max(0, window_x) + screen_max_y, screen_max_x = self.screen.getmaxyx() if self.screen \ + else (42, 42) + last_y, last_x = min(screen_max_y - 1, last_y), \ + min(screen_max_x - 1, last_x) + + if last_y >= window_y and last_x >= window_x: + # Refresh the pad only if coordinates are valid + pad.refresh(top_y, top_x, window_y, window_x, last_y, last_x) + def display(self) -> None: raise NotImplementedError @@ -50,3 +93,68 @@ class Display: @property def cols(self) -> int: return curses.COLS if self.screen else 42 + + +class VerticalSplit(Display): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.pad = self.newpad(self.rows, 1) + + @property + def width(self) -> int: + return 1 + + @width.setter + def width(self, val: Any) -> None: + pass + + def display(self) -> None: + for i in range(self.height): + self.addstr(self.pad, i, 0, "┃") + self.refresh_pad(self.pad, 0, 0, self.y, self.x, + self.y + self.height - 1, self.x) + + +class HorizontalSplit(Display): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.pad = self.newpad(1, self.cols) + + @property + def height(self) -> int: + return 1 + + @height.setter + def height(self, val: Any) -> None: + pass + + def display(self) -> None: + for i in range(self.width): + self.addstr(self.pad, 0, i, "━") + self.refresh_pad(self.pad, 0, 0, self.y, self.x, self.y, + self.x + self.width - 1) + + +class Box(Display): + + def __init__(self, *args, fg_border_color: Optional[int] = None, **kwargs): + super().__init__(*args, **kwargs) + self.pad = self.newpad(self.rows, self.cols) + self.fg_border_color = fg_border_color or curses.COLOR_WHITE + + pair_number = 4 + self.fg_border_color + self.init_pair(pair_number, self.fg_border_color, curses.COLOR_BLACK) + self.pair = self.color_pair(pair_number) + + def display(self) -> None: + self.addstr(self.pad, 0, 0, "┏" + "━" * (self.width - 2) + "┓", + self.pair) + for i in range(1, self.height - 1): + self.addstr(self.pad, i, 0, "┃", self.pair) + self.addstr(self.pad, i, self.width - 1, "┃", self.pair) + self.addstr(self.pad, self.height - 1, 0, + "┗" + "━" * (self.width - 2) + "┛", self.pair) + self.refresh_pad(self.pad, 0, 0, self.y, self.x, + self.y + self.height - 1, self.x + self.width - 1) diff --git a/squirrelbattle/display/display_manager.py b/squirrelbattle/display/display_manager.py index a4636eb..f7a0882 100644 --- a/squirrelbattle/display/display_manager.py +++ b/squirrelbattle/display/display_manager.py @@ -1,5 +1,10 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + import curses +from squirrelbattle.display.display import VerticalSplit, HorizontalSplit from squirrelbattle.display.mapdisplay import MapDisplay +from squirrelbattle.display.messagedisplay import MessageDisplay from squirrelbattle.display.statsdisplay import StatsDisplay from squirrelbattle.display.menudisplay import SettingsMenuDisplay, \ MainMenuDisplay @@ -22,9 +27,12 @@ class DisplayManager: screen, pack) self.settingsmenudisplay = SettingsMenuDisplay(screen, pack) self.logsdisplay = LogsDisplay(screen, pack) + self.messagedisplay = MessageDisplay(screen=screen, pack=None) + self.hbar = HorizontalSplit(screen, pack) + self.vbar = VerticalSplit(screen, pack) self.displays = [self.statsdisplay, self.mapdisplay, self.mainmenudisplay, self.settingsmenudisplay, - self.logsdisplay] + self.logsdisplay, self.messagedisplay] self.update_game_components() def handle_display_action(self, action: DisplayActions) -> None: @@ -40,20 +48,35 @@ class DisplayManager: self.statsdisplay.update_player(self.game.player) self.settingsmenudisplay.update_menu(self.game.settings_menu) self.logsdisplay.update_logs(self.game.logs) + self.messagedisplay.update_message(self.game.message) def refresh(self) -> None: if self.game.state == GameMode.PLAY: # The map pad has already the good size - self.mapdisplay.refresh(0, 0, self.rows * 4 // 5, self.cols, + self.mapdisplay.refresh(0, 0, self.rows * 4 // 5, + self.mapdisplay.pack.tile_width + * (self.cols * 4 // 5 + // self.mapdisplay.pack.tile_width), resize_pad=False) - self.statsdisplay.refresh(self.rows * 4 // 5, 0, - self.rows // 10, self.cols) - self.logsdisplay.refresh(self.rows * 9 // 10, 0, - self.rows // 10, self.cols) + self.statsdisplay.refresh(0, self.cols * 4 // 5 + 1, + self.rows, self.cols // 5 - 1) + self.logsdisplay.refresh(self.rows * 4 // 5 + 1, 0, + self.rows // 5 - 1, self.cols * 4 // 5) + self.hbar.refresh(self.rows * 4 // 5, 0, 1, self.cols * 4 // 5) + self.vbar.refresh(0, self.cols * 4 // 5, self.rows, 1) if self.game.state == GameMode.MAINMENU: self.mainmenudisplay.refresh(0, 0, self.rows, self.cols) if self.game.state == GameMode.SETTINGS: self.settingsmenudisplay.refresh(0, 0, self.rows, self.cols - 1) + + if self.game.message: + height, width = 0, 0 + for line in self.game.message.split("\n"): + height += 1 + width = max(width, len(line)) + y, x = (self.rows - height) // 2, (self.cols - width) // 2 + self.messagedisplay.refresh(y, x, height, width) + self.resize_window() def resize_window(self) -> bool: diff --git a/squirrelbattle/display/logsdisplay.py b/squirrelbattle/display/logsdisplay.py index 0d95915..b768a0e 100644 --- a/squirrelbattle/display/logsdisplay.py +++ b/squirrelbattle/display/logsdisplay.py @@ -1,3 +1,6 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + from squirrelbattle.display.display import Display from squirrelbattle.interfaces import Logs @@ -12,12 +15,11 @@ class LogsDisplay(Display): self.logs = logs def display(self) -> None: - print(type(self.logs.messages), flush=True) messages = self.logs.messages[-self.height:] messages = messages[::-1] - self.pad.clear() + self.pad.erase() for i in range(min(self.height, len(messages))): - self.pad.addstr(self.height - i - 1, self.x, - messages[i][:self.width]) - self.pad.refresh(0, 0, self.y, self.x, self.y + self.height, - self.x + self.width) + self.addstr(self.pad, self.height - i - 1, self.x, + messages[i][:self.width]) + self.refresh_pad(self.pad, 0, 0, self.y, self.x, + self.y + self.height - 1, self.x + self.width - 1) diff --git a/squirrelbattle/display/mapdisplay.py b/squirrelbattle/display/mapdisplay.py index e6cc278..445f736 100644 --- a/squirrelbattle/display/mapdisplay.py +++ b/squirrelbattle/display/mapdisplay.py @@ -1,4 +1,6 @@ -#!/usr/bin/env python +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + from squirrelbattle.interfaces import Map from .display import Display @@ -15,11 +17,11 @@ class MapDisplay(Display): def update_pad(self) -> None: self.init_pair(1, self.pack.tile_fg_color, self.pack.tile_bg_color) self.init_pair(2, self.pack.entity_fg_color, self.pack.entity_bg_color) - self.pad.addstr(0, 0, self.map.draw_string(self.pack), - self.color_pair(1)) + self.addstr(self.pad, 0, 0, self.map.draw_string(self.pack), + self.color_pair(1)) for e in self.map.entities: - self.pad.addstr(e.y, self.pack.tile_width * e.x, - self.pack[e.name.upper()], self.color_pair(2)) + self.addstr(self.pad, e.y, self.pack.tile_width * e.x, + self.pack[e.name.upper()], self.color_pair(2)) def display(self) -> None: y, x = self.map.currenty, self.pack.tile_width * self.map.currentx @@ -31,9 +33,18 @@ class MapDisplay(Display): smaxrow = min(smaxrow, self.height - 1) smaxcol = self.pack.tile_width * self.map.width - \ (x + deltax) + self.width - 1 + + # Wrap perfectly the map according to the width of the tiles + pmincol = self.pack.tile_width * (pmincol // self.pack.tile_width) + smincol = self.pack.tile_width * (smincol // self.pack.tile_width) + smaxcol = self.pack.tile_width \ + * (smaxcol // self.pack.tile_width + 1) - 1 + smaxcol = min(smaxcol, self.width - 1) pminrow = max(0, min(self.map.height, pminrow)) pmincol = max(0, min(self.pack.tile_width * self.map.width, pmincol)) - self.pad.clear() + + self.pad.erase() self.update_pad() - self.pad.refresh(pminrow, pmincol, sminrow, smincol, smaxrow, smaxcol) + self.refresh_pad(self.pad, pminrow, pmincol, sminrow, smincol, smaxrow, + smaxcol) diff --git a/squirrelbattle/display/menudisplay.py b/squirrelbattle/display/menudisplay.py index 6a0c45f..9cc3ad5 100644 --- a/squirrelbattle/display/menudisplay.py +++ b/squirrelbattle/display/menudisplay.py @@ -1,8 +1,12 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + from typing import List from squirrelbattle.menus import Menu, MainMenu -from .display import Display +from .display import Display, Box from ..resources import ResourceManager +from ..translations import gettext as _ class MenuDisplay(Display): @@ -11,25 +15,23 @@ class MenuDisplay(Display): """ position: int - def __init__(self, *args): - super().__init__(*args) - self.menubox = self.newpad(self.rows, self.cols) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.menubox = Box(*args, **kwargs) 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) for i in range(self.trueheight): - self.pad.addstr(i, 0, " " + self.values[i]) + self.addstr(self.pad, i, 0, " " + self.values[i]) def update_pad(self) -> None: for i in range(self.trueheight): - self.pad.addstr(i, 0, " " + self.values[i]) + self.addstr(self.pad, i, 0, " " + self.values[i]) # set a marker on the selected line - self.pad.addstr(self.menu.position, 0, ">") + self.addstr(self.pad, self.menu.position, 0, ">") def display(self) -> None: cornery = 0 if self.height - 2 >= self.menu.position - 1 \ @@ -37,20 +39,21 @@ class MenuDisplay(Display): if self.height - 2 >= self.trueheight - self.menu.position else 0 # Menu box - self.menubox.addstr(0, 0, "┏" + "━" * (self.width - 2) + "┓") - for i in range(1, self.height - 1): - self.menubox.addstr(i, 0, "┃" + " " * (self.width - 2) + "┃") - self.menubox.addstr(self.height - 1, 0, - "┗" + "━" * (self.width - 2) + "┛") - - self.menubox.refresh(0, 0, self.y, self.x, - self.height + self.y, - self.width + self.x) + self.menubox.refresh(self.y, self.x, self.height, self.width) + self.pad.erase() self.update_pad() - self.pad.refresh(cornery, 0, self.y + 1, self.x + 2, + self.refresh_pad(self.pad, cornery, 0, self.y + 1, self.x + 2, 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 @@ -70,9 +73,10 @@ 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] @@ -95,9 +99,11 @@ class MainMenuDisplay(Display): def display(self) -> None: for i in range(len(self.title)): - self.pad.addstr(4 + i, max(self.width // 2 - - len(self.title[0]) // 2 - 1, 0), self.title[i]) - self.pad.refresh(0, 0, self.y, self.x, self.height, self.width) + self.addstr(self.pad, 4 + i, max(self.width // 2 + - len(self.title[0]) // 2 - 1, 0), self.title[i]) + self.refresh_pad(self.pad, 0, 0, self.y, self.x, + self.height + self.y - 1, + self.width + self.x - 1) menuwidth = min(self.menudisplay.preferred_width, self.width) menuy, menux = len(self.title) + 8, self.width // 2 - menuwidth // 2 - 1 self.menudisplay.refresh( diff --git a/squirrelbattle/display/messagedisplay.py b/squirrelbattle/display/messagedisplay.py new file mode 100644 index 0000000..bcc2539 --- /dev/null +++ b/squirrelbattle/display/messagedisplay.py @@ -0,0 +1,31 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later +import curses + +from squirrelbattle.display.display import Box, Display + + +class MessageDisplay(Display): + """ + Display a message in a popup. + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.box = Box(fg_border_color=curses.COLOR_RED, *args, **kwargs) + self.message = "" + self.pad = self.newpad(1, 1) + + def update_message(self, msg: str) -> None: + self.message = msg + + def display(self) -> None: + self.box.refresh(self.y - 1, self.x - 2, + self.height + 2, self.width + 4) + self.box.display() + self.pad.erase() + self.addstr(self.pad, 0, 0, self.message, curses.A_BOLD) + self.refresh_pad(self.pad, 0, 0, self.y, self.x, + self.height + self.y - 1, + self.width + self.x - 1) diff --git a/squirrelbattle/display/statsdisplay.py b/squirrelbattle/display/statsdisplay.py index 77ddb30..da9213f 100644 --- a/squirrelbattle/display/statsdisplay.py +++ b/squirrelbattle/display/statsdisplay.py @@ -1,9 +1,12 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + 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 @@ -17,36 +20,28 @@ class StatsDisplay(Display): self.player = p def update_pad(self) -> None: - string = "" - for _ in range(self.width - 1): - string = string + "-" - self.pad.addstr(0, 0, string) - string2 = "Player -- LVL {} EXP {}/{} HP {}/{}"\ + string2 = "Player -- LVL {}\nEXP {}/{}\nHP {}/{}"\ .format(self.player.level, self.player.current_xp, self.player.max_xp, self.player.health, self.player.maxhealth) - for _ in range(self.width - len(string2) - 1): - string2 = string2 + " " - self.pad.addstr(1, 0, string2) - string3 = "Stats : STR {} INT {} CHR {} DEX {} CON {}"\ + self.addstr(self.pad, 0, 0, string2) + string3 = "STR {}\nINT {}\nCHR {}\nDEX {}\nCON {}"\ .format(self.player.strength, self.player.intelligence, self.player.charisma, self.player.dexterity, self.player.constitution) - for _ in range(self.width - len(string3) - 1): - string3 = string3 + " " - self.pad.addstr(2, 0, string3) + 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.pad.addstr(3, 0, inventory_str) + self.addstr(self.pad, 8, 0, inventory_str) if self.player.dead: - self.pad.addstr(4, 0, "VOUS ÊTES MORT", - curses.A_BOLD | curses.A_BLINK | curses.A_STANDOUT - | self.color_pair(3)) + self.addstr(self.pad, 10, 0, _("YOU ARE DEAD"), + curses.A_BOLD | curses.A_BLINK | curses.A_STANDOUT + | self.color_pair(3)) def display(self) -> None: - self.pad.clear() + self.pad.erase() self.update_pad() - self.pad.refresh(0, 0, self.y, self.x, - 4 + self.y, self.width + self.x) + self.refresh_pad(self.pad, 0, 0, self.y, self.x, + self.y + self.height - 1, self.width + self.x - 1) diff --git a/squirrelbattle/display/texturepack.py b/squirrelbattle/display/texturepack.py index 71b07a6..665dd6f 100644 --- a/squirrelbattle/display/texturepack.py +++ b/squirrelbattle/display/texturepack.py @@ -1,3 +1,6 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + import curses from typing import Any @@ -51,7 +54,7 @@ TexturePack.ASCII_PACK = TexturePack( HEART='❤', BOMB='o', RABBIT='Y', - BEAVER='_', + TIGER='n', TEDDY_BEAR='8', MERCHANT='M', SUNFLOWER='I', @@ -67,12 +70,12 @@ TexturePack.SQUIRREL_PACK = TexturePack( EMPTY=' ', WALL='🧱', FLOOR='██', - PLAYER='🐿️️', + PLAYER='🐿️ ️', HEDGEHOG='🦔', HEART='💜', BOMB='💣', RABBIT='🐇', - BEAVER='🦫', + TIGER='🐅', TEDDY_BEAR='🧸', MERCHANT='🦜', SUNFLOWER='🌻', diff --git a/squirrelbattle/entities/__init__.py b/squirrelbattle/entities/__init__.py index e69de29..1cc6688 100644 --- a/squirrelbattle/entities/__init__.py +++ b/squirrelbattle/entities/__init__.py @@ -0,0 +1,2 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/squirrelbattle/entities/items.py b/squirrelbattle/entities/items.py index f5da235..e1326b9 100644 --- a/squirrelbattle/entities/items.py +++ b/squirrelbattle/entities/items.py @@ -1,3 +1,6 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + from typing import Optional from .player import Player diff --git a/squirrelbattle/entities/monsters.py b/squirrelbattle/entities/monsters.py index 8192b8e..624f8a3 100644 --- a/squirrelbattle/entities/monsters.py +++ b/squirrelbattle/entities/monsters.py @@ -1,3 +1,6 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + from random import choice from .player import Player @@ -52,13 +55,13 @@ class Monster(FightingEntity): break -class Beaver(Monster): +class Tiger(Monster): """ - A beaver monster + A tiger monster """ def __init__(self, strength: int = 2, maxhealth: int = 20, *args, **kwargs) -> None: - super().__init__(name="beaver", strength=strength, + super().__init__(name="tiger", strength=strength, maxhealth=maxhealth, *args, **kwargs) diff --git a/squirrelbattle/entities/player.py b/squirrelbattle/entities/player.py index c5cd9e3..171f019 100644 --- a/squirrelbattle/entities/player.py +++ b/squirrelbattle/entities/player.py @@ -1,3 +1,6 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + from random import randint from typing import Dict, Tuple diff --git a/squirrelbattle/enums.py b/squirrelbattle/enums.py index 350c196..024f167 100644 --- a/squirrelbattle/enums.py +++ b/squirrelbattle/enums.py @@ -1,3 +1,6 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + from enum import Enum, auto from typing import Optional diff --git a/squirrelbattle/game.py b/squirrelbattle/game.py index 0bb3024..44ad349 100644 --- a/squirrelbattle/game.py +++ b/squirrelbattle/game.py @@ -1,3 +1,7 @@ +# 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 import json @@ -10,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 @@ -27,13 +32,15 @@ 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 def new_game(self) -> None: """ @@ -55,7 +62,7 @@ class Game: when the given key gets pressed. """ while True: # pragma no cover - screen.clear() + screen.erase() screen.refresh() self.display_actions(DisplayActions.REFRESH) key = screen.getkey() @@ -68,6 +75,11 @@ class Game: Indicates what should be done when the given key is pressed, according to the current game state. """ + if self.message: + self.message = None + self.display_actions(DisplayActions.REFRESH) + return + if self.state == GameMode.PLAY: self.handle_key_pressed_play(key) elif self.state == GameMode.MAINMENU: @@ -130,9 +142,24 @@ class Game: """ Loads the game from a dictionary """ - self.map.load_state(d) - # noinspection PyTypeChecker - self.player = self.map.find_entities(Player)[0] + 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.") + 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.player.health = 0 + self.display_actions(DisplayActions.UPDATE) + return + + self.player = players[0] self.display_actions(DisplayActions.UPDATE) def load_game(self) -> None: @@ -142,7 +169,15 @@ class Game: file_path = ResourceManager.get_config_path("save.json") if os.path.isfile(file_path): with open(file_path, "r") as f: - self.load_state(json.loads(f.read())) + try: + 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.") + os.unlink(file_path) + self.display_actions(DisplayActions.UPDATE) def save_game(self) -> None: """ diff --git a/squirrelbattle/interfaces.py b/squirrelbattle/interfaces.py index 0db0fc2..5c89172 100644 --- a/squirrelbattle/interfaces.py +++ b/squirrelbattle/interfaces.py @@ -1,11 +1,13 @@ -#!/usr/bin/env python -from random import randint +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + from enum import Enum, auto from math import sqrt from random import choice, randint from typing import List, Optional, Any -from squirrelbattle.display.texturepack import TexturePack +from .display.texturepack import TexturePack +from .translations import gettext as _ class Logs: @@ -127,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) @@ -319,16 +321,20 @@ class Entity: """ return isinstance(self, FriendlyEntity) + @property + def translated_name(self) -> str: + return _(self.name.replace("_", " ")) + @staticmethod def get_all_entity_classes(): """ Returns all entities subclasses """ from squirrelbattle.entities.items import Heart, Bomb - from squirrelbattle.entities.monsters import Beaver, Hedgehog, \ + from squirrelbattle.entities.monsters import Tiger, Hedgehog, \ Rabbit, TeddyBear from squirrelbattle.entities.friendly import Merchant,Sunflower - return [Beaver, Bomb, Heart, Hedgehog, Rabbit, TeddyBear,Sunflower] + return [Tiger, Bomb, Heart, Hedgehog, Rabbit, TeddyBear,Sunflower] @staticmethod def get_all_entity_classes_in_a_dict() -> dict: @@ -336,12 +342,12 @@ class Entity: Returns all entities subclasses in a dictionary """ from squirrelbattle.entities.player import Player - from squirrelbattle.entities.monsters import Beaver, Hedgehog, Rabbit, \ + from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, \ TeddyBear from squirrelbattle.entities.items import Bomb, Heart from squirrelbattle.entities.friendly import Merchant,Sunflower return { - "Beaver": Beaver, + "Tiger": Tiger, "Bomb": Bomb, "Heart": Heart, "Hedgehog": Hedgehog, @@ -399,8 +405,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: """ @@ -409,8 +417,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: """ @@ -451,5 +462,5 @@ class FriendlyEntity(Entity): def talk_to(self, player : Any) -> str: a = randint(0,len(self.dialogue_option)-1) return "The sunflower said : "+self.dialogue_option[a] - - + + 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 cd7604a..4fcfabe 100644 --- a/squirrelbattle/menus.py +++ b/squirrelbattle/menus.py @@ -1,9 +1,13 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + from enum import Enum 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: @@ -38,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): @@ -64,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: @@ -92,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/resources.py b/squirrelbattle/resources.py index fc6f708..b3421db 100644 --- a/squirrelbattle/resources.py +++ b/squirrelbattle/resources.py @@ -1,3 +1,6 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + from pathlib import Path diff --git a/squirrelbattle/settings.py b/squirrelbattle/settings.py index 6c2e31c..3090679 100644 --- a/squirrelbattle/settings.py +++ b/squirrelbattle/settings.py @@ -1,8 +1,13 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# 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: @@ -13,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) @@ -50,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/term_manager.py b/squirrelbattle/term_manager.py index b1f10b1..6284173 100644 --- a/squirrelbattle/term_manager.py +++ b/squirrelbattle/term_manager.py @@ -1,3 +1,6 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + import curses from types import TracebackType diff --git a/squirrelbattle/tests/__init__.py b/squirrelbattle/tests/__init__.py index e69de29..1cc6688 100644 --- a/squirrelbattle/tests/__init__.py +++ b/squirrelbattle/tests/__init__.py @@ -0,0 +1,2 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later diff --git a/squirrelbattle/tests/entities_test.py b/squirrelbattle/tests/entities_test.py index 0c8ee3c..371bfc7 100644 --- a/squirrelbattle/tests/entities_test.py +++ b/squirrelbattle/tests/entities_test.py @@ -1,7 +1,10 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + import unittest from squirrelbattle.entities.items import Bomb, Heart, Item -from squirrelbattle.entities.monsters import Beaver, Hedgehog, Rabbit, TeddyBear +from squirrelbattle.entities.monsters import Tiger, Hedgehog, Rabbit, TeddyBear from squirrelbattle.entities.player import Player from squirrelbattle.interfaces import Entity, Map from squirrelbattle.resources import ResourceManager @@ -36,17 +39,17 @@ class TestEntities(unittest.TestCase): """ Test some random stuff with fighting entities. """ - entity = Beaver() + entity = Tiger() self.map.add_entity(entity) self.assertEqual(entity.maxhealth, 20) self.assertEqual(entity.maxhealth, entity.health) self.assertEqual(entity.strength, 2) for _ in range(9): self.assertEqual(entity.hit(entity), - "beaver hits beaver. beaver takes 2 damage.") + "Tiger hits tiger. Tiger takes 2 damage.") self.assertFalse(entity.dead) - self.assertEqual(entity.hit(entity), "beaver hits beaver. " - + "beaver takes 2 damage. beaver dies.") + self.assertEqual(entity.hit(entity), "Tiger hits tiger. " + + "Tiger takes 2 damage. Tiger dies.") self.assertTrue(entity.dead) entity = Rabbit() @@ -67,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 61c6c8e..a23b6f9 100644 --- a/squirrelbattle/tests/game_test.py +++ b/squirrelbattle/tests/game_test.py @@ -1,13 +1,19 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + import os import unittest -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): @@ -38,6 +44,27 @@ class TestGame(unittest.TestCase): new_state = self.game.save_state() self.assertEqual(old_state, new_state) + # Error on loading save + with open(ResourceManager.get_config_path("save.json"), "w") as f: + f.write("I am not a JSON file") + self.assertIsNone(self.game.message) + self.game.load_game() + self.assertIsNotNone(self.game.message) + self.game.message = None + + with open(ResourceManager.get_config_path("save.json"), "w") as f: + f.write("{}") + self.assertIsNone(self.game.message) + self.game.load_game() + self.assertIsNotNone(self.game.message) + self.game.message = None + + # Load game with a dead player + self.game.map.remove_entity(self.game.player) + self.game.save_game() + self.game.load_game() + self.assertIsNotNone(self.game.message) + def test_bootstrap_fail(self) -> None: """ Ensure that the test can't play the game, @@ -247,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) @@ -289,3 +327,13 @@ class TestGame(unittest.TestCase): Check that some functions are not implemented, only for coverage. """ self.assertRaises(NotImplementedError, Display.display, None) + + def test_messages(self) -> None: + """ + Display error messages. + """ + self.game.message = "I am an error" + self.game.display_actions(DisplayActions.UPDATE) + self.game.display_actions(DisplayActions.REFRESH) + self.game.handle_key_pressed(None, "random key") + self.assertIsNone(self.game.message) diff --git a/squirrelbattle/tests/interfaces_test.py b/squirrelbattle/tests/interfaces_test.py index 62f2092..c9f7253 100644 --- a/squirrelbattle/tests/interfaces_test.py +++ b/squirrelbattle/tests/interfaces_test.py @@ -1,3 +1,6 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + import unittest from squirrelbattle.display.texturepack import TexturePack diff --git a/squirrelbattle/tests/screen.py b/squirrelbattle/tests/screen.py index 6eb2cd0..9a8afe6 100644 --- a/squirrelbattle/tests/screen.py +++ b/squirrelbattle/tests/screen.py @@ -1,3 +1,9 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + +from typing import Tuple + + class FakePad: """ In order to run tests, we simulate a fake curses pad that accepts functions @@ -10,8 +16,11 @@ class FakePad: smincol: int, smaxrow: int, smaxcol: int) -> None: pass - def clear(self) -> None: + def erase(self) -> None: pass def resize(self, height: int, width: int) -> None: pass + + def getmaxyx(self) -> Tuple[int, int]: + return 42, 42 diff --git a/squirrelbattle/tests/settings_test.py b/squirrelbattle/tests/settings_test.py index 76bcaba..b0d9739 100644 --- a/squirrelbattle/tests/settings_test.py +++ b/squirrelbattle/tests/settings_test.py @@ -1,3 +1,6 @@ +# Copyright (C) 2020 by ÿnérant, eichhornchen, nicomarg, charlse +# SPDX-License-Identifier: GPL-3.0-or-later + import unittest from squirrelbattle.settings import Settings @@ -21,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()