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