From 614232b12a56a48c06936fdafb61a336fb0e098b Mon Sep 17 00:00:00 2001 From: Alexandre Iooss <erdnaxe@crans.org> Date: Tue, 14 Apr 2020 01:20:55 +0200 Subject: [PATCH] Add locale support --- .gitignore | 1 + cpasswords/client.py | 98 ++++++----- cpasswords/locale/fr/LC_MESSAGES/messages.po | 164 +++++++++++++++++++ setup.py | 20 ++- 4 files changed, 237 insertions(+), 46 deletions(-) create mode 100644 cpasswords/locale/fr/LC_MESSAGES/messages.po diff --git a/.gitignore b/.gitignore index baee8a9..323ab5b 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ __pycache__ *.swp *.egg-info _build +*.mo # Virtualenvs .tox diff --git a/cpasswords/client.py b/cpasswords/client.py index 83a9cdf..625a67e 100755 --- a/cpasswords/client.py +++ b/cpasswords/client.py @@ -20,16 +20,23 @@ import argparse import re import copy import logging +import gettext from configparser import ConfigParser from secrets import token_urlsafe -# Import a SSH client +# Import setuptool and SSH client +from pkg_resources import resource_filename from paramiko.client import SSHClient from paramiko.ssh_exception import SSHException # Import modules from .gpg import decrypt, encrypt, receive_keys, list_keys, GPG_TRUSTLEVELS +# Load locale +gettext.bindtextdomain('messages', resource_filename("cpasswords", "locale")) +gettext.textdomain('messages') +_ = gettext.gettext + # Configuration loading # On n'a pas encore accès à la config donc on devine le nom bootstrap_cmd_name = os.path.split(sys.argv[0])[1] @@ -43,9 +50,9 @@ if not config.read(config_path + "/clientconfig.ini"): not any([opt in sys.argv for opt in ["-q", "--quiet"]]) if ducktape_display_error: # Do not use logger as it has not been initialized yet - print("%s/clientconfig.ini could not be found or read.\n" - "Please copy `docs/clientconfig.example.ini` from the source " - "repository and customize." % config_path) + print(_("%s/clientconfig.ini could not be found or read.\n" + "Please copy `docs/clientconfig.example.ini` from the source " + "repository and customize.") % config_path) exit(1) # Local logger @@ -123,7 +130,7 @@ def remote_command(options, command, arg=None, stdin_contents=None): # Write if stdin_contents is not None: - log.info("Writing to stdin: %s" % stdin_contents) + log.info(_("Writing to stdin: %s") % stdin_contents) stdin.write(json.dumps(stdin_contents)) stdin.flush() @@ -133,14 +140,14 @@ def remote_command(options, command, arg=None, stdin_contents=None): err = "" if stderr.channel.recv_stderr_ready(): err = stderr.read() - log.error("Wrong server return code %s, error is %s" % (ret, err)) + log.error(_("Wrong server return code %s, error is %s") % (ret, err)) exit(ret) # Decode directly read buffer try: answer = json.load(stdout) except ValueError: - log.error("Error while parsing JSON") + log.error(_("Error while parsing JSON")) exit(42) log.debug("Server returned %s" % answer) @@ -400,21 +407,21 @@ def editor(texte, annotations=""): def show_files(options): """Affiche la liste des fichiers disponibles sur le serveur distant""" - my_roles, _ = get_my_roles(options) + my_roles, my_roles_w = get_my_roles(options) files = all_files(options) keys = list(files.keys()) keys.sort() - print("Liste des fichiers disponibles :") + print(_("Available files:")) for fname in keys: froles = files[fname] access = set(my_roles).intersection(froles) != set([]) print((" %s %s (%s)" % ((access and '+' or '-'), fname, ", ".join(froles)))) - print(("""--Mes roles: %s""" % (", ".join(my_roles),))) + print((_("""--Mes roles: %s""") % ", ".join(my_roles))) def restore_files(options): """Restore les fichiers corrompues sur le serveur distant""" - print("Fichier corrompus :") + print(_("Fichier corrompus :")) files = restore_all_files(options) keys = files.keys() keys.sort() @@ -424,7 +431,7 @@ def restore_files(options): def show_roles(options): """Affiche la liste des roles existants""" - print("Liste des roles disponibles") + print(_("Liste des roles disponibles")) allroles = all_roles(options) for (role, usernames) in allroles.iteritems(): if role == "whoami": @@ -435,7 +442,7 @@ def show_roles(options): def show_servers(options): """Affiche la liste des serveurs disponibles""" - print("Liste des serveurs disponibles") + print(_("Liste des serveurs disponibles")) for server in config.keys(): print(" * " + server) @@ -450,7 +457,7 @@ def saveclipboard(restore=False, old_clipboard=None): if not restore: old_clipboard = proc.stdout.read() else: - input("Appuyez sur Entrée pour récupérer le contenu précédent du presse papier.") + input(_("Appuyez sur Entrée pour récupérer le contenu précédent du presse papier.")) proc.stdin.write(old_clipboard) proc.stdin.close() proc.stdout.close() @@ -511,7 +518,7 @@ def show_file(options): filtered += line + '\n' if is_key: - filtered = "La clé a été mise dans l'agent ssh" + filtered = _("La clé a été mise dans l'agent ssh") shown = "Fichier %s:\n\n%s-----\nVisible par: %s\n" % ( fname, filtered, ','.join(passfile['roles'])) @@ -665,7 +672,7 @@ def remove_file(options): def my_check_keys(options): """Vérifie les clés et affiche un message en fonction du résultat""" - print("Vérification que les clés sont valides (uid correspondant au login) et de confiance.") + print(_("Vérification que les clés sont valides (uid correspondant au login) et de confiance.")) print(check_keys(options) and "Base de clés ok" or "Erreurs dans la base") @@ -711,7 +718,8 @@ def recrypt_files(options, strict=False): # On informe l'utilisateur et on demande confirmation avant de rechiffrer # Si il a précisé --force, on ne lui demandera rien. filenames = ", ".join(askfiles) - message = "Vous vous apprêtez à rechiffrer les fichiers suivants :\n%s" % filenames + message = _( + "Vous vous apprêtez à rechiffrer les fichiers suivants :\n%s") % filenames if not confirm(options, message + "\nConfirmer"): exit(2) # On rechiffre @@ -729,8 +737,7 @@ def recrypt_files(options, strict=False): for i in range(len(results)): print("%s : %s" % (to_put[i]['filename'], results[i][1])) else: - if not options.quiet: - print("Aucun fichier n'a besoin d'être rechiffré") + log.warn(_("Aucun fichier n'a besoin d'être rechiffré")) def parse_roles(options, cast=False): @@ -771,7 +778,7 @@ def parse_roles(options, cast=False): def insult_on_nofilename(options, parser): """Insulte (si non quiet) et quitte si aucun nom de fichier n'a été fourni en commandline.""" if options.fname is None: - log.warn("You need to provide a filename with this command") + log.warn(_("You need to provide a filename with this command")) if not options.quiet: parser.print_help() exit(1) @@ -780,49 +787,50 @@ def insult_on_nofilename(options, parser): def main(): # Gestion des arguments parser = argparse.ArgumentParser( - description="Gestion de mots de passe partagés grâce à GPG.", + description=_("Gestion de mots de passe partagés grâce à GPG."), ) parser.add_argument( '-v', '--verbose', action='count', default=1, - help="verbose mode, multiple -v options increase verbosity", + help=_("verbose mode, multiple -v options increase verbosity"), ) parser.add_argument( '-q', '--quiet', action='store_true', default=False, - help="silent mode: hide errors, overrides verbosity" + help=_("silent mode: hide errors, overrides verbosity"), ) parser.add_argument( '-s', '--server', default='DEFAULT', - help="Utilisation d'un serveur alternatif (test, backup, etc)" + help=_("Utilisation d'un serveur alternatif (test, backup, etc)"), ) parser.add_argument( '--drop-invalid', action='store_true', default=False, dest='drop_invalid', - help="Combiné avec --force, droppe les clés en lesquelles on n'a pas confiance sans demander confirmation." + help=_("Combiné avec --force, droppe les clés en lesquelles on n'a pas confiance sans demander confirmation."), ) parser.add_argument( '-c', '--clipboard', action='store_true', default=None, - help="Stocker le mot de passe dans le presse papier") + help=_("Stocker le mot de passe dans le presse papier"), + ) parser.add_argument( '--no-clip', '--noclip', '--noclipboard', action='store_false', default=None, dest='clipboard', - help="Ne PAS stocker le mot de passe dans le presse papier" + help=_("Ne PAS stocker le mot de passe dans le presse papier"), ) parser.add_argument( '-f', '--force', action='store_true', default=False, - help="Ne pas demander confirmation" + help=_("Ne pas demander confirmation"), ) # Actions possibles @@ -833,7 +841,7 @@ def main(): dest='action', default=show_file, const=edit_file, - help="Editer (ou créer)" + help=_("Editer (ou créer)"), ) action_grp.add_argument( '--view', @@ -841,7 +849,7 @@ def main(): dest='action', default=show_file, const=show_file, - help="Voir le fichier" + help=_("Voir le fichier"), ) action_grp.add_argument( '--remove', @@ -849,7 +857,7 @@ def main(): dest='action', default=show_file, const=remove_file, - help="Effacer le fichier" + help=_("Effacer le fichier"), ) action_grp.add_argument( '-l', '--list', @@ -857,30 +865,29 @@ def main(): dest='action', default=show_file, const=show_files, - help="Lister les fichiers" + help=("Lister les fichiers"), ) action_grp.add_argument( '-r', '--restore', action='store_const', dest='action', default=show_file, const=restore_files, - help="Restorer les fichiers corrompues" + help=("Restorer les fichiers corrompues"), ) action_grp.add_argument( '--check-keys', - action='store_const', dest='action', default=show_file, const=my_check_keys, - help="Vérifier les clés" + help=("Vérifier les clés"), ) action_grp.add_argument( '--update-keys', action='store_const', dest='action', default=show_file, const=my_update_keys, - help="Mettre à jour les clés" + help=("Mettre à jour les clés"), ) action_grp.add_argument( '--list-roles', @@ -888,7 +895,7 @@ def main(): dest='action', default=show_file, const=show_roles, - help="Lister les rôles existants" + help=("Lister les rôles existants"), ) action_grp.add_argument( '--list-servers', @@ -896,16 +903,17 @@ def main(): dest='action', default=show_file, const=show_servers, - help="Lister les serveurs") + help=("Lister les serveurs"), + ) action_grp.add_argument( '--recrypt-files', action='store_const', dest='action', default=show_file, const=recrypt_files, - help="""Rechiffrer les mots de passe. - (Avec les mêmes rôles que ceux qu'ils avant. - Cela sert à mettre à jour les recipients pour qui un password est chiffré)""" + help=_("""Rechiffrer les mots de passe. + (Avec les mêmes rôles que ceux qu'ils avant. + Cela sert à mettre à jour les recipients pour qui un password est chiffré)"""), ) action_grp.add_argument( '--strict-recrypt-files', @@ -913,14 +921,14 @@ def main(): dest='action', default=show_file, const=lambda x: recrypt_files( x, strict=True), - help="""Rechiffrer les mots de passe (mode strict, voir --roles)""" + help=_("Rechiffrer les mots de passe (mode strict, voir --roles)"), ) parser.add_argument( '--roles', nargs='?', default=None, - help="""Liste de roles (séparés par des virgules). Par défaut, tous les + help=_("""Liste de roles (séparés par des virgules). Par défaut, tous les rôles en écriture (sauf pour l'édition, d'un fichier existant). Avec --edit: le fichier sera chiffré pour exactement ces roles Avec --(strict-)recrypt-files : @@ -928,12 +936,12 @@ def main(): * non-strict: tout fichier possédant un des rôles listé * strict: tout fichier dont *tous* les rôles sont dans la liste - """) + """)) parser.add_argument( 'fname', nargs='?', default=None, - help="Nom du fichier à afficher" + help=_("Nom du fichier à afficher") ) # On parse les options fournies en commandline diff --git a/cpasswords/locale/fr/LC_MESSAGES/messages.po b/cpasswords/locale/fr/LC_MESSAGES/messages.po new file mode 100644 index 0000000..659f5ae --- /dev/null +++ b/cpasswords/locale/fr/LC_MESSAGES/messages.po @@ -0,0 +1,164 @@ +#, fuzzy +msgid "" +msgstr "" +"Project-Id-Version: PACKAGE VERSION\n" +"Report-Msgid-Bugs-To: \n" +"POT-Creation-Date: 2020-04-14 01:11+0200\n" +"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" +"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" +"Language-Team: LANGUAGE <LL@li.org>\n" +"Language: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +#: cpasswords/client.py:53 +#, python-format +msgid "" +"%s/clientconfig.ini could not be found or read.\n" +"Please copy `docs/clientconfig.example.ini` from the source repository and " +"customize." +msgstr "" + +#: cpasswords/client.py:133 +#, python-format +msgid "Writing to stdin: %s" +msgstr "" + +#: cpasswords/client.py:143 +#, python-format +msgid "Wrong server return code %s, error is %s" +msgstr "" + +#: cpasswords/client.py:150 +msgid "Error while parsing JSON" +msgstr "" + +#: cpasswords/client.py:414 +msgid "Available files:" +msgstr "Liste des fichiers disponibles :" + +#: cpasswords/client.py:419 +#, python-format +msgid "--Mes roles: %s" +msgstr "" + +#: cpasswords/client.py:424 +msgid "Fichier corrompus :" +msgstr "" + +#: cpasswords/client.py:434 +msgid "Liste des roles disponibles" +msgstr "" + +#: cpasswords/client.py:445 +msgid "Liste des serveurs disponibles" +msgstr "" + +#: cpasswords/client.py:460 +msgid "" +"Appuyez sur Entrée pour récupérer le contenu précédent du presse papier." +msgstr "" + +#: cpasswords/client.py:521 +msgid "La clé a été mise dans l'agent ssh" +msgstr "" + +#: cpasswords/client.py:675 +msgid "" +"Vérification que les clés sont valides (uid correspondant au login) et de " +"confiance." +msgstr "" + +#: cpasswords/client.py:722 +#, python-format +msgid "" +"Vous vous apprêtez à rechiffrer les fichiers suivants :\n" +"%s" +msgstr "" + +#: cpasswords/client.py:740 +msgid "Aucun fichier n'a besoin d'être rechiffré" +msgstr "" + +#: cpasswords/client.py:781 +msgid "You need to provide a filename with this command" +msgstr "" + +#: cpasswords/client.py:790 +msgid "Gestion de mots de passe partagés grâce à GPG." +msgstr "" + +#: cpasswords/client.py:796 +msgid "verbose mode, multiple -v options increase verbosity" +msgstr "" + +#: cpasswords/client.py:802 +msgid "silent mode: hide errors, overrides verbosity" +msgstr "" + +#: cpasswords/client.py:807 +msgid "Utilisation d'un serveur alternatif (test, backup, etc)" +msgstr "" + +#: cpasswords/client.py:814 +msgid "" +"Combiné avec --force, droppe les clés en lesquelles on n'a pas confiance " +"sans demander confirmation." +msgstr "" + +#: cpasswords/client.py:820 +msgid "Stocker le mot de passe dans le presse papier" +msgstr "" + +#: cpasswords/client.py:827 +msgid "Ne PAS stocker le mot de passe dans le presse papier" +msgstr "" + +#: cpasswords/client.py:833 +msgid "Ne pas demander confirmation" +msgstr "" + +#: cpasswords/client.py:844 +msgid "Editer (ou créer)" +msgstr "" + +#: cpasswords/client.py:852 +msgid "Voir le fichier" +msgstr "" + +#: cpasswords/client.py:860 +msgid "Effacer le fichier" +msgstr "" + +#: cpasswords/client.py:914 +msgid "" +"Rechiffrer les mots de passe.\n" +" (Avec les mêmes rôles que ceux qu'ils avant.\n" +" Cela sert à mettre à jour les recipients pour qui un " +"password est chiffré)" +msgstr "" + +#: cpasswords/client.py:924 +msgid "Rechiffrer les mots de passe (mode strict, voir --roles)" +msgstr "" + +#: cpasswords/client.py:931 +msgid "" +"Liste de roles (séparés par des virgules). Par défaut, tous les\n" +" rôles en écriture (sauf pour l'édition, d'un fichier " +"existant).\n" +" Avec --edit: le fichier sera chiffré pour exactement ces " +"roles\n" +" Avec --(strict-)recrypt-files :\n" +" sert à sélectionnenr les fichiers à rechiffrer\n" +" * non-strict: tout fichier possédant un des rôles listé\n" +" * strict: tout fichier dont *tous* les rôles sont dans " +"la\n" +" liste\n" +" " +msgstr "" + +#: cpasswords/client.py:944 +msgid "Nom du fichier à afficher" +msgstr "" diff --git a/setup.py b/setup.py index 8125c66..96c8bfb 100644 --- a/setup.py +++ b/setup.py @@ -1,10 +1,27 @@ from setuptools import setup -from os import getenv +from os import getenv, path +from subprocess import call # Enable the user to install cpasswords # with another command and config path command_name = getenv("COMMAND_NAME", "cranspasswords") + +def compile_messages(): + """ + Compile gettext translations + For now, only compile french + """ + mo_files = [] + locales = ['cpasswords/locale/fr/LC_MESSAGES/messages.po'] + for po_file in locales: + filename, _ = path.splitext(po_file) + mo_file = filename + '.mo' + call("msgfmt %s -o %s" % (po_file, mo_file), shell=True) + mo_files.append(mo_file) + return [('locale', mo_files), ] + + setup( name="cpasswords", version="0.2.0", @@ -26,6 +43,7 @@ setup( 'Topic :: Utilities', ], packages=['cpasswords'], + data_files=compile_messages(), include_package_data=True, install_requires=[ 'paramiko>=2.2', -- GitLab