From e37f00a196f300cc21c79d7541031c4289d709dc Mon Sep 17 00:00:00 2001
From: Pierre-Jean CHANCELLIER <paidge_cs@hotmail.com>
Date: Sat, 8 Jan 2022 19:58:01 +0100
Subject: [PATCH] i18n

---
 components/badge/CertifStatus.vue             |   2 +-
 components/badge/Status.vue                   |  13 +-
 components/certif/List.vue                    |   2 +-
 components/member/Card.vue                    |  22 +--
 components/member/List.vue                    |   6 +-
 components/navigation/Bar.vue                 |  16 +-
 components/navigation/Breadcrumb.vue          |   9 +-
 components/navigation/Language.vue            |  24 +++
 components/navigation/Loader.vue              |   2 +-
 components/navigation/menu/Group.vue          |   6 +-
 components/navigation/menu/Sidebar.vue        |   2 +-
 graphql/index.js                              |   4 +-
 i18n/index.js                                 |  10 ++
 i18n/locales/dateTimeFormats.js               |  47 ++++++
 i18n/locales/en.json                          |  57 +++++++
 i18n/locales/es.json                          |  57 +++++++
 i18n/locales/fr.json                          |  57 +++++++
 layouts/default.vue                           |  40 +++--
 nuxt.config.js                                |  28 +++-
 package-lock.json                             | 146 +++++++++++++++++-
 package.json                                  |   5 +-
 pages/chartjs.vue                             |   9 +-
 pages/index.vue                               |  14 +-
 pages/membres/_hash.vue                       |  19 ++-
 pages/membres/index.vue                       |  19 ++-
 pages/previsions/_hash.vue                    |  13 +-
 .../{newcomers.vue => futurs_membres.vue}     |  29 ++--
 pages/previsions/index.vue                    |  11 +-
 28 files changed, 581 insertions(+), 88 deletions(-)
 create mode 100644 components/navigation/Language.vue
 create mode 100644 i18n/index.js
 create mode 100644 i18n/locales/dateTimeFormats.js
 create mode 100644 i18n/locales/en.json
 create mode 100644 i18n/locales/es.json
 create mode 100644 i18n/locales/fr.json
 rename pages/previsions/{newcomers.vue => futurs_membres.vue} (85%)

diff --git a/components/badge/CertifStatus.vue b/components/badge/CertifStatus.vue
index cee160b..b3ceae8 100644
--- a/components/badge/CertifStatus.vue
+++ b/components/badge/CertifStatus.vue
@@ -21,7 +21,7 @@ export default {
             }
         },
         textWarning: function() {
-            return (this.$options.filters.dateStatus(this.limitDate) == 'danger') ? 'En manque de certifications' : 'Bientôt en manque de certifications'
+            return (this.$options.filters.dateStatus(this.limitDate) == 'danger') ? this.$i18n.t('statut.manquecertif') : this.$i18n.t('statut.bientotmanquecertif')
         }
     }
 }
diff --git a/components/badge/Status.vue b/components/badge/Status.vue
index 0301cf7..ed5e794 100644
--- a/components/badge/Status.vue
+++ b/components/badge/Status.vue
@@ -1,7 +1,6 @@
 <template>
     <small>
-        <span class="badge"
-          :class="this.displayStatus(membre).class">
+        <span class="badge" :class="this.displayStatus(membre).class">
             {{ this.displayStatus(membre).str }}
         </span>
     </small>
@@ -19,17 +18,17 @@ export default {
         displayStatus: function(member){
             switch (member.status) {
                 case 'NEWCOMER':
-                    return {str: 'Futur membre',class: 'badge-info'}
+                    return {str: this.$i18n.t('statut.newcomer'),class: 'badge-info'}
                 case 'MISSING':
-                    return {str: 'Adhésion perdue',class: 'badge-danger'}
+                    return {str: this.$i18n.t('statut.missing'),class: 'badge-danger'}
                 case 'MEMBER':
                     if (this.$options.filters.dateStatus(member.limitDate) == 'warning') {
-                        return {str: 'Adhésion à renouveler',class: 'badge-warning'}
+                        return {str: this.$i18n.t('statut.renew'),class: 'badge-warning'}
                     } else {
-                        return {str: 'Membre',class: 'badge-success'}
+                        return {str: this.$i18n.t('statut.member'),class: 'badge-success'}
                     }
                 case 'REVOKED':
-                    return {str: 'Membre revoqué',class: 'badge-secondary'}
+                    return {str: this.$i18n.t('statut.revoked'),class: 'badge-secondary'}
                 default:
                     return 'N/A'
             }
diff --git a/components/certif/List.vue b/components/certif/List.vue
index 659a542..378a7d0 100644
--- a/components/certif/List.vue
+++ b/components/certif/List.vue
@@ -3,7 +3,7 @@
         <table class="table table-striped table-hover">
             <tbody>
                 <tr v-for="certif in certifsTriees" :key="getNeighbor(certif).uid"
-                    @click="$router.push({ path: '/membres/' + getNeighbor(certif).hash })">
+                    @click="$router.push(localePath({name:'membres-hash', params: {hash: getNeighbor(certif).hash}}))">
                     <th scope="row">
                         {{ getNeighbor(certif).uid }}
                         <BadgeCertifStatus :limitDate="getNeighbor(certif).received_certifications.limit" :memberStatus="getNeighbor(certif).status" />
diff --git a/components/member/Card.vue b/components/member/Card.vue
index ce3c7cc..876a431 100644
--- a/components/member/Card.vue
+++ b/components/member/Card.vue
@@ -9,40 +9,40 @@
         <table class="table table-sm table-borderless" v-if="hash.status != 'REVOKED'">
         <tbody>
             <tr v-if="hash.status == 'MEMBER'">
-                <th scope="row">Référent :</th>
-                <td :class="{'list-group-item-success': isReferent, 'list-group-item-warning': !isReferent}">{{ isReferent ? 'Oui' : 'Non' }}</td>
+                <th scope="row">{{ $t('membre.referent') }} :</th>
+                <td :class="{'list-group-item-success': isReferent, 'list-group-item-warning': !isReferent}">{{ isReferent ? $t('oui') : $t('non') }}</td>
             </tr>
             <tr v-if="hash.status != 'NEWCOMER'">
-                <th scope="row">Qualité :</th>
+                <th scope="row">{{ $t('membre.qualite') }} :</th>
                 <td :class="{
                     'list-group-item-success': hash.quality.ratio >= 80,
                     'list-group-item-warning': hash.quality.ratio < 80,
                 }">{{ Math.round(hash.quality.ratio*100)/100 }}</td>
             </tr>
             <tr v-if="hash.status != 'NEWCOMER'">
-                <th scope="row">Distance :</th>
+                <th scope="row">{{ $t('membre.distance') }} :</th>
                 <td :class="{
                     'list-group-item-success': hash.distance.dist_ok,
                     'list-group-item-danger': !hash.distance.dist_ok,
                 }">{{ Math.round(hash.distance.value.ratio*100)/100 }}</td>
             </tr>
             <tr>
-                <th scope="row">{{ hash.status != 'MISSING' ? "Date limite d'adhésion" : "Date limite avant révocation"}} :</th>
-                <td :class="hash.status != 'MISSING' ? 'list-group-item-'+ $options.filters.dateStatus(hash.limitDate) : 'list-group-item-danger'">{{ hash.limitDate | formatDate }}</td>
+                <th scope="row">{{ hash.status != 'MISSING' ? $t('membre.datelimadhesion') : $t('membre.datelimrevoc')}} :</th>
+                <td :class="hash.status != 'MISSING' ? 'list-group-item-'+ $options.filters.dateStatus(hash.limitDate) : 'list-group-item-danger'">{{ $d(new Date(hash.limitDate*1000), 'long') }}</td>
             </tr>
             <tr v-if="hash.status == 'MEMBER'">
-                <th scope="row">Date avant de manquer de certifs :</th>
-                <td :class="'list-group-item-'+ $options.filters.dateStatus(hash.received_certifications.limit)">{{ hash.received_certifications.limit | formatDate }}</td>
+                <th scope="row">{{ $t('membre.datemanquecertifs') }} :</th>
+                <td :class="'list-group-item-'+ $options.filters.dateStatus(hash.received_certifications.limit)">{{ $d(new Date(hash.received_certifications.limit*1000), 'long') }}</td>
             </tr>
             <tr v-if="hash.status == 'MEMBER'">
-                <th scope="row">Disponible pour certifier :</th>
+                <th scope="row">{{ $t('membre.dispocertif') }} :</th>
                 <td :class="{
                     'list-group-item-success': hash.minDatePassed,
                     'list-group-item-danger': !hash.minDatePassed,
-                }">{{ hash.minDatePassed ? 'Oui' : 'Non'  }} <small v-if="!hash.minDatePassed">( > {{ hash.minDate | formatDate }} )</small></td>
+                }">{{ hash.minDatePassed ? $t('oui') : $t('non')  }} <small v-if="!hash.minDatePassed">( > {{ hash.minDate | formatDate }} )</small></td>
             </tr>
             <tr v-if="hash.status == 'MEMBER'">
-                <th scope="row">Nbre de certifs disponibles :</th>
+                <th scope="row">{{ $t('membre.nb_certifs') }} :</th>
                 <td :class="{
                     'list-group-item-success': hash.sent_certifications.length<=80,
                     'list-group-item-warning': hash.sent_certifications.length>80,
diff --git a/components/member/List.vue b/components/member/List.vue
index ccb5d49..5541d3d 100644
--- a/components/member/List.vue
+++ b/components/member/List.vue
@@ -9,7 +9,7 @@
           </thead>
           <tbody>
             <tr v-for="member in members" :key="member.uid"
-              @click="redirect('/membres/' + member.hash)">
+              @click="redirect(member.hash)">
               <th scope="row">
                 {{ member.uid }}
                 <BadgeCertifStatus :limitDate="member.received_certifications.limit" :memberStatus="member.status" />
@@ -39,8 +39,8 @@ export default {
       }
   },
   methods: {
-    redirect(path) {
-      this.$router.push({ path: path })
+    redirect(hash) {
+      this.$router.push(this.localePath({name:'membres-hash', params: {hash}}))
     }
   },
 }
diff --git a/components/navigation/Bar.vue b/components/navigation/Bar.vue
index 05dfdd1..3c8f537 100644
--- a/components/navigation/Bar.vue
+++ b/components/navigation/Bar.vue
@@ -2,7 +2,7 @@
   <header class="header position-fixed">
     <div class="position-relative">
       <button class="toggle btn border-secondary position-absolute p-1 m-1 ml-3" @click="toggleMenu"><span></span></button>
-      <NavigationBreadcrumb :breadcrumb="breadcrumb" class="breadcrumb p-1" />
+      <NavigationBreadcrumb :breadcrumb="breadcrumb" />
     </div>
     <NavigationMenuSidebar @toggleMenu="toggleMenu" :menus="menus" />
     <div class="bg_overlay position-fixed" @click="toggleMenu"></div>
@@ -42,14 +42,24 @@ $btn-width: 50px;
   }
 }
 
-nav.breadcrumb {
-  margin: .5rem .5rem .5rem 5rem;
+nav.breadcrumb-wrapper {
+  margin: 8px 15px 8px 80px;
+  display: flex;
+  flex-direction: column;
+  gap: 1rem;
+  background: var(--background-color-secondary);
 
   a {color: var(--text-primary-color)}
 
   .breadcrumb-item.active {
       opacity: .7;
   }
+
+  @media (min-width:768px) {
+    flex-direction: row;
+    justify-content: space-between;
+    align-items: center;
+  }
 }
 
 %hamburger-line {
diff --git a/components/navigation/Breadcrumb.vue b/components/navigation/Breadcrumb.vue
index 6e40c09..2c0f0de 100644
--- a/components/navigation/Breadcrumb.vue
+++ b/components/navigation/Breadcrumb.vue
@@ -1,12 +1,15 @@
 <template>
-    <nav aria-label="Fil d'Ariane" class="justify-content-between align-items-center">
-      <ol class="breadcrumb m-0">
+    <nav aria-label="Fil d'Ariane" class="breadcrumb-wrapper rounded p-3">
+      <ol class="breadcrumb m-0 p-0">
           <li class="breadcrumb-item" :class="{ 'active': item.active }" :aria-current="item.active ? 'page' : null" v-for="item in breadcrumb" :key="item.text">
               <NuxtLink :to="item.to" v-if="item.to">{{ item.text }}</NuxtLink>
               <span v-else>{{ item.text }}</span>
           </li>
       </ol>
-      <BtnTheme />
+      <div class="d-flex justify-content-between align-items-center">
+        <NavigationLanguage class="mr-3" />
+        <BtnTheme />
+      </div>
     </nav>
 </template>
 
diff --git a/components/navigation/Language.vue b/components/navigation/Language.vue
new file mode 100644
index 0000000..6f1cf36
--- /dev/null
+++ b/components/navigation/Language.vue
@@ -0,0 +1,24 @@
+<template>
+<div>
+    <select class="form-control" @change="saveLocale($event)" v-model="lang">
+        <option v-for="lang in $i18n.locales" :key="lang.code" :value="lang.code">{{ lang.name }}</option>
+    </select>
+</div>
+</template>
+
+<script>
+export default {
+    data() {
+        return {
+            lang: this.$i18n.locale
+        }
+    },
+    methods: {
+        saveLocale(e) {
+            this.lang = e.target.value
+            this.$i18n.setLocaleCookie(e.target.value)
+            this.$router.push(this.switchLocalePath(e.target.value))
+        }
+    }
+}
+</script>
\ No newline at end of file
diff --git a/components/navigation/Loader.vue b/components/navigation/Loader.vue
index c12b53a..1e99a26 100644
--- a/components/navigation/Loader.vue
+++ b/components/navigation/Loader.vue
@@ -2,7 +2,7 @@
 <transition name="fade">
   <div class="loader overflow-hidden text-center position-absolute" v-if="isLoading">
     <img src="~@/assets/img/loader.gif">
-    <div class="text-center font-weight-bold my-3">Chargement...</div>
+    <div class="text-center font-weight-bold my-3">{{ $t('chargement') }}...</div>
   </div>
 </transition>
 </template>
diff --git a/components/navigation/menu/Group.vue b/components/navigation/menu/Group.vue
index 01943d3..967668c 100644
--- a/components/navigation/menu/Group.vue
+++ b/components/navigation/menu/Group.vue
@@ -1,8 +1,10 @@
 <template>
     <div class="mb-4">
-        <h2 class="small text-muted text-uppercase ml-4 mb-3 pb-3 border-bottom">{{ menu.title }}</h2>
+        <h2 class="small text-muted text-uppercase ml-4 mb-3 pb-3 border-bottom">{{ $t(menu.title) }}</h2>
         <div class="nav navbar-nav list-group list-group-flush">
-            <NuxtLink class="list-group-item list-group-item-action p-0 pl-3" :to="item.path" v-for="item in menu.items" :key="item.path"><div class="position-relative py-3">{{ item.title }}</div></NuxtLink>
+            <NuxtLink class="list-group-item list-group-item-action p-0 pl-3" :to="localePath(item.path)" v-for="item in menu.items" :key="item.path">
+                <div class="position-relative py-3">{{ $t(item.title) }}</div>
+            </NuxtLink>
         </div>
     </div>
 </template>
diff --git a/components/navigation/menu/Sidebar.vue b/components/navigation/menu/Sidebar.vue
index 3a493c5..4f599e4 100644
--- a/components/navigation/menu/Sidebar.vue
+++ b/components/navigation/menu/Sidebar.vue
@@ -1,7 +1,7 @@
 <template>
     <aside class="menu shadow position-fixed">
         <div class="nav_header pb-3 mb-5">
-            <nuxt-link to="/"><h1 class="h2 d-flex"><img src="@/assets/img/logo.png" alt="Accueil" class="logo">&nbsp;Wotwizard</h1></nuxt-link>
+            <nuxt-link :to="localePath('/')"><h1 class="h2 d-flex"><img src="@/assets/img/logo.png" alt="Accueil" class="logo">&nbsp;Wotwizard</h1></nuxt-link>
             <button type="button" class="close position-absolute d-xl-none" aria-label="Close" @click="toggleMenu">
                 <span aria-hidden="true">&times;</span>
             </button>
diff --git a/graphql/index.js b/graphql/index.js
index c8e3991..178821e 100644
--- a/graphql/index.js
+++ b/graphql/index.js
@@ -21,6 +21,8 @@ export default ctx => {
 
   return {
     link,
-    cache
+    cache,
+    // https://github.com/nuxt-community/apollo-module/issues/306#issuecomment-607225431
+    defaultHttpLink: false
   }
 }
\ No newline at end of file
diff --git a/i18n/index.js b/i18n/index.js
new file mode 100644
index 0000000..e72ec82
--- /dev/null
+++ b/i18n/index.js
@@ -0,0 +1,10 @@
+import en from './locales/en.json'
+import fr from './locales/fr.json'
+import es from './locales/es.json'
+import {dateTimeFormats} from './locales/dateTimeFormats'
+
+export default {
+  fallbackLocale: 'en',
+  dateTimeFormats,
+  messages: { en, fr, es }
+}
\ No newline at end of file
diff --git a/i18n/locales/dateTimeFormats.js b/i18n/locales/dateTimeFormats.js
new file mode 100644
index 0000000..4f6948e
--- /dev/null
+++ b/i18n/locales/dateTimeFormats.js
@@ -0,0 +1,47 @@
+export const dateTimeFormats = {
+    'fr': {
+        short: {
+            day: 'numeric',
+            month: 'long',
+            year: 'numeric'
+        },
+        long: {
+            day: 'numeric',
+            month: 'long',
+            year: 'numeric',
+        },
+        hour: {
+            hour: 'numeric'
+        }
+    },
+    'en': {
+        short: {
+            day: 'numeric',
+            month: 'long',
+            year: 'numeric'
+        },
+        long: {
+            month: 'long',
+            day: 'numeric',
+            year: 'numeric',
+        },
+        hour: {
+            hour: 'numeric'
+        }
+    },
+    'es': {
+        short: {
+            day: 'numeric',
+            month: 'long',
+            year: 'numeric'
+        },
+        long: {
+            day: 'numeric',
+            month: 'long',
+            year: 'numeric',
+        },
+        hour: {
+            hour: 'numeric'
+        }
+    }
+}
\ No newline at end of file
diff --git a/i18n/locales/en.json b/i18n/locales/en.json
new file mode 100644
index 0000000..7bbde18
--- /dev/null
+++ b/i18n/locales/en.json
@@ -0,0 +1,57 @@
+{
+    "accueil": "Home",
+    "aurevoir": "Goodbye to",
+    "bienvenue": "Welcome to",
+    "certifications": {
+        "envoyees" : "Certificates sent",
+        "recues" : "Certificates received "
+    },
+    "chargement" : "Loading",
+    "dev": "In development",
+    "futuremembers": "Future members",
+    "inout": "Entries and exits of the web of trust for the last 2 days",
+    "inpreparation": "In preparation",
+    "membre": {
+        "datelimadhesion": "Membership deadline",
+        "datelimrevoc" : "Deadline before revocation ",
+        "datemanquecertifs": "Date before running out of certs",
+        "dispocertif": "Available to certify",
+        "distance": "Distance",
+        "nb_certifs": "Nb of available certs",
+        "qualite": "Quality",
+        "referent": "Referent"
+    },
+    "membres": "Members",
+    "non": "No",
+    "oui": "Yes",
+    "previsions" : {
+        "certificationsinternes": "No internal certification | 1 internal certification | {n} internal certifications",
+        "dossiersattente": "No pending files | 1 pending file | {n} pending files",
+        "pardate": "Forecasts by dates",
+        "parmembre": "Forecasts by members",
+        "permutations": "No permutation | 1 permutation | {n} permutations",
+        "title": "Forecasts"
+    },
+    "recherche": {
+        "desc" : "Enter the start of a nickname or public key",
+        "title": "Your search"
+    },
+    "revoila": "Here they are again",
+    "statut" : {
+        "bientotmanquecertif": "Needs certifications soon",
+        "manquecertif": "Needs certifications",
+        "member": "Member",
+        "missing": "Membership lost",
+        "newcomer": "Future member",
+        "renew": "Membership to renew",
+        "revoked": "Revoked member"
+    },
+    "time": {
+        "a": "at"
+    },
+    "tri": {
+        "pardate": "Sort by date",
+        "parmembres": "Sort by members"
+    },
+    "wot": "Web of trust"
+}
\ No newline at end of file
diff --git a/i18n/locales/es.json b/i18n/locales/es.json
new file mode 100644
index 0000000..3c36da8
--- /dev/null
+++ b/i18n/locales/es.json
@@ -0,0 +1,57 @@
+{
+    "accueil": "Página principal",
+    "aurevoir": "Adios a",
+    "bienvenue": "Bienvenido a",
+    "certifications": {
+        "envoyees" : "Certificados enviados",
+        "recues" : "Certificaciones recibidas "
+    },
+    "chargement" : "Cargando",
+    "dev": "En desarrollo",
+    "futuremembers": "Futuros miembros",
+    "inout": "Entradas y salidas de la web de confianza de los últimos 2 días",
+    "inpreparation": "En preparación",
+    "membre": {
+        "datelimadhesion": "Fecha límite de membresía",
+        "datelimrevoc" : "Plazo antes de la revocación ",
+        "datemanquecertifs": "Fecha antes de quedarse sin certificados",
+        "dispocertif": "Disponible para certificar",
+        "distance": "Distancia",
+        "nb_certifs": "Núm de certificados disponibles",
+        "qualite": "Calidad",
+        "referent": "Referente"
+    },
+    "membres": "Miembros",
+    "non": "No",
+    "oui": "Sí",
+    "previsions" : {
+        "certificationsinternes": "Sin certificación interna | 1 certificación interna | {n} certificaciones internas",
+        "dossiersattente": "No hay expedientes pendientes | 1 expediente pendiente | {n} expedientes pendientes",
+        "pardate": "Previsiones por fecha",
+        "parmembre": "Previsiones por miembros",
+        "permutations": "Sin permutación | 1 permutación | {n} permutaciones",
+        "title": "Pronósticos"
+    },
+    "recherche": {
+        "desc" : "Ingrese el inicio de un apodo o clave pública",
+        "title": "Tu búsqueda"
+    },
+    "revoila": "Aquí están de nuevo",
+    "statut" : {
+        "bientotmanquecertif": "Pronto necesitará certificaciones",
+        "manquecertif": "A falta de certificaciones",
+        "member": "Miembro",
+        "missing": "Membresía perdida",
+        "newcomer": "Futuro miembro",
+        "renew": "Membresía para renovar",
+        "revoked": "Miembro revocado"
+    },
+    "time": {
+        "a": "a"
+    },
+    "tri": {
+        "pardate": "Ordenar por fecha",
+        "parmembres": "lasificar por miembros"
+    },
+    "wot": "Web de confianza"
+}
\ No newline at end of file
diff --git a/i18n/locales/fr.json b/i18n/locales/fr.json
new file mode 100644
index 0000000..3dbf2b3
--- /dev/null
+++ b/i18n/locales/fr.json
@@ -0,0 +1,57 @@
+{
+    "accueil": "Accueil",
+    "aurevoir": "Au revoir à",
+    "bienvenue": "Bienvenue à",
+    "certifications": {
+        "envoyees" : "Certifications envoyées",
+        "recues" : "Certifications reçues "
+    },
+    "chargement" : "Chargement",
+    "dev": "En Développement",
+    "futuremembers": "Futurs membres",
+    "inout": "Entrées et sorties de la toile de confiance des 2 derniers jours",
+    "inpreparation": "En préparation",
+    "membre": {
+        "datelimadhesion": "Date limite d'adhésion",
+        "datelimrevoc" : "Date limite avant révocation ",
+        "datemanquecertifs": "Date avant de manquer de certifs",
+        "dispocertif": "Disponible pour certifier",
+        "distance": "Distance",
+        "nb_certifs": "Nbre de certifs disponibles",
+        "qualite": "Qualité",
+        "referent": "Référent"
+    },
+    "membres": "Membres",
+    "non": "Non",
+    "oui": "Oui",
+    "previsions" : {
+        "certificationsinternes": "Aucune certification interne | 1 certification interne | {n} certifications internes",
+        "dossiersattente": "Aucun dossier en attente | 1 dossier en attente | {n} dossiers en attente",
+        "pardate": "Prévisions par date",
+        "parmembre": "Prévisions par membres",
+        "permutations": "Aucune permutation | 1 permutation | {n} permutations",
+        "title": "Prévisions"
+    },
+    "recherche": {
+        "desc" : "Saisissez le début d'un pseudo ou d'une clé publique",
+        "title": "Votre recherche"
+    },
+    "revoila": "Les revoilà",
+    "statut" : {
+        "bientotmanquecertif": "Bientôt en manque de certifications",
+        "manquecertif": "En manque de certifications",
+        "member": "Membre",
+        "missing": "Adhésion perdue",
+        "newcomer": "Futur membre",
+        "renew": "Adhésion à renouveler",
+        "revoked": "Membre révoqué"
+    },
+    "time": {
+        "a": "à"
+    },
+    "tri": {
+        "pardate": "Tri par date",
+        "parmembres": "Tri par membres"
+    },
+    "wot": "Toile de confiance"
+}
\ No newline at end of file
diff --git a/layouts/default.vue b/layouts/default.vue
index 382a848..80c65ee 100644
--- a/layouts/default.vue
+++ b/layouts/default.vue
@@ -10,22 +10,26 @@ export default {
   data() {
     return {
       breadcrumb: [],
-      menus : [{
-            title: 'Prévisions',
-            items : [
-              {path: '/previsions',title: 'En préparation'},
-              {path: '/previsions/newcomers',title: 'Futurs membres'}
-          ]},
-          {
-            title: 'Toile de confiance',
-            items : [
-              {path: '/membres',title: 'Membres'}
-          ]},
-          {
-          title: 'En Développement',
+      // Les title correspondent aux chaînes de traduction dans /i18n/locales
+      menus : [
+        {
+          title: 'previsions.title',
           items : [
-              {path: '/chartjs',title: 'ChartJS'},
-          ]},
+            {path: '/previsions/futurs_membres',title: 'futuremembers'}
+          ]
+        },
+        {
+          title: 'wot',
+          items : [
+            {path: '/membres',title: 'membres'}
+          ]
+        },
+        {
+          title: 'dev',
+          items : [
+            {path: '/previsions',title: 'inpreparation'}
+          ]
+        },
       ]
     }
   },
@@ -60,8 +64,12 @@ export default {
     transition: margin .5s ease-in-out;
 }
 main {
-  padding-top: 5rem;
+  padding-top: 8rem;
   position: relative;
+
+  @media (min-width:768px) {
+    padding-top: 5rem;
+  }
 }
 .fade-enter-active, .fade-leave-active {
   transition: opacity .5s;
diff --git a/nuxt.config.js b/nuxt.config.js
index d4c40bf..6ce6da7 100644
--- a/nuxt.config.js
+++ b/nuxt.config.js
@@ -1,3 +1,5 @@
+import i18n from './i18n'
+
 export default {
   // Disable server-side rendering: https://go.nuxtjs.dev/ssr-mode
   ssr: false,
@@ -41,9 +43,33 @@ export default {
   // Modules: https://go.nuxtjs.dev/config-modules
   modules: [
     // https://github.com/nuxt-community/apollo-module
-    '@nuxtjs/apollo'
+    '@nuxtjs/apollo',
+    // https://i18n.nuxtjs.org
+    '@nuxtjs/i18n'
   ],
 
+  i18n : {
+    defaultLocale: 'fr',
+    locales: [
+      {
+        code: 'en',
+        name: 'English'
+      },
+      {
+        code: 'fr',
+        name: 'Français'
+      },
+      {
+        code: 'es',
+        name: 'Español'
+      }
+    ],
+    detectBrowserLanguage: {
+      alwaysRedirect: true
+    },
+    vueI18n: i18n
+  },
+
   // PWA module configuration: https://go.nuxtjs.dev/pwa
   pwa: {
     manifest: {
diff --git a/package-lock.json b/package-lock.json
index 34636c1..07c0c6a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
       "version": "1.0.0",
       "dependencies": {
         "@nuxtjs/apollo": "^4.0.1-rc.5",
+        "@nuxtjs/i18n": "^7.2.0",
         "@nuxtjs/pwa": "^3.3.5",
         "bootstrap": "^4.6.1",
         "chart.js": "^3.6.2",
@@ -3147,6 +3148,38 @@
         "@hapi/hoek": "^8.3.0"
       }
     },
+    "node_modules/@intlify/shared": {
+      "version": "9.1.9",
+      "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.1.9.tgz",
+      "integrity": "sha512-xKGM1d0EAxdDFCWedcYXOm6V5Pfw/TMudd6/qCdEb4tv0hk9EKeg7lwQF1azE0dP2phvx0yXxrt7UQK+IZjNdw==",
+      "engines": {
+        "node": ">= 10"
+      }
+    },
+    "node_modules/@intlify/vue-i18n-extensions": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@intlify/vue-i18n-extensions/-/vue-i18n-extensions-1.0.2.tgz",
+      "integrity": "sha512-rnfA0ScyBXyp9xsSD4EAMGeOh1yv/AE7fhqdAdSOr5X8N39azz257umfRtzNT9sHXAKSSzpCVhIbMAkp5c/gjQ==",
+      "dependencies": {
+        "@babel/parser": "^7.9.6"
+      },
+      "engines": {
+        "node": ">= 10.0"
+      }
+    },
+    "node_modules/@intlify/vue-i18n-loader": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@intlify/vue-i18n-loader/-/vue-i18n-loader-1.1.0.tgz",
+      "integrity": "sha512-9LXiztMtYKTE8t/hRwwGUp+ofrwU0sxLQLzFEOZ38zvn0DonUIQmZUj1cfz5p1Lu8BllxKbCrn6HnsRJ+LYA6g==",
+      "dependencies": {
+        "@intlify/shared": "^9.0.0",
+        "js-yaml": "^3.13.1",
+        "json5": "^2.1.1"
+      },
+      "engines": {
+        "node": ">= 10"
+      }
+    },
     "node_modules/@josephg/resolvable": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/@josephg/resolvable/-/resolvable-1.0.1.tgz",
@@ -3836,6 +3869,33 @@
       "resolved": "https://registry.npmjs.org/webpack-node-externals/-/webpack-node-externals-2.5.2.tgz",
       "integrity": "sha512-aHdl/y2N7PW2Sx7K+r3AxpJO+aDMcYzMQd60Qxefq3+EwhewSbTBqNumOsCE1JsCUNoyfGj5465N0sSf6hc/5w=="
     },
+    "node_modules/@nuxtjs/i18n": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@nuxtjs/i18n/-/i18n-7.2.0.tgz",
+      "integrity": "sha512-fO/QRMxZ0t6LpYJ7ti6mHc0lsog4SMr7Xll3d45zdVSI7+5eBE1YcoG8yXv1sndGQZoq8k0iD1dXuAJA6UL5ww==",
+      "dependencies": {
+        "@babel/parser": "^7.16.2",
+        "@babel/traverse": "^7.16.0",
+        "@intlify/vue-i18n-extensions": "^1.0.2",
+        "@intlify/vue-i18n-loader": "^1.1.0",
+        "cookie": "^0.4.1",
+        "devalue": "^2.0.1",
+        "is-https": "^4.0.0",
+        "js-cookie": "^3.0.1",
+        "klona": "^2.0.5",
+        "lodash.merge": "^4.6.2",
+        "ufo": "^0.7.9",
+        "vue-i18n": "^8.26.7"
+      }
+    },
+    "node_modules/@nuxtjs/i18n/node_modules/cookie": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
+      "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
     "node_modules/@nuxtjs/pwa": {
       "version": "3.3.5",
       "resolved": "https://registry.npmjs.org/@nuxtjs/pwa/-/pwa-3.3.5.tgz",
@@ -12667,6 +12727,11 @@
         "node": ">=0.10.0"
       }
     },
+    "node_modules/is-https": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/is-https/-/is-https-4.0.0.tgz",
+      "integrity": "sha512-FeMLiqf8E5g6SdiVJsPcNZX8k4h2fBs1wp5Bb6uaNxn58ufK1axBqQZdmAQsqh0t9BuwFObybrdVJh6MKyPlyg=="
+    },
     "node_modules/is-installed-globally": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
@@ -12988,6 +13053,14 @@
       "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==",
       "peer": true
     },
+    "node_modules/js-cookie": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz",
+      "integrity": "sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw==",
+      "engines": {
+        "node": ">=12"
+      }
+    },
     "node_modules/js-message": {
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz",
@@ -13135,7 +13208,6 @@
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz",
       "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==",
-      "dev": true,
       "engines": {
         "node": ">= 8"
       }
@@ -20433,6 +20505,11 @@
       "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz",
       "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog=="
     },
+    "node_modules/vue-i18n": {
+      "version": "8.26.8",
+      "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.26.8.tgz",
+      "integrity": "sha512-BN2OXolO15AKS95yNF8oOtARibaO6RxyKkAYNV4XpOmL7S4eVZYMIDtyvDv+XGZaiUmBJSH9mdNqzexvGMnK2A=="
+    },
     "node_modules/vue-loader": {
       "version": "15.9.8",
       "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.9.8.tgz",
@@ -23841,6 +23918,29 @@
         "@hapi/hoek": "^8.3.0"
       }
     },
+    "@intlify/shared": {
+      "version": "9.1.9",
+      "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.1.9.tgz",
+      "integrity": "sha512-xKGM1d0EAxdDFCWedcYXOm6V5Pfw/TMudd6/qCdEb4tv0hk9EKeg7lwQF1azE0dP2phvx0yXxrt7UQK+IZjNdw=="
+    },
+    "@intlify/vue-i18n-extensions": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@intlify/vue-i18n-extensions/-/vue-i18n-extensions-1.0.2.tgz",
+      "integrity": "sha512-rnfA0ScyBXyp9xsSD4EAMGeOh1yv/AE7fhqdAdSOr5X8N39azz257umfRtzNT9sHXAKSSzpCVhIbMAkp5c/gjQ==",
+      "requires": {
+        "@babel/parser": "^7.9.6"
+      }
+    },
+    "@intlify/vue-i18n-loader": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/@intlify/vue-i18n-loader/-/vue-i18n-loader-1.1.0.tgz",
+      "integrity": "sha512-9LXiztMtYKTE8t/hRwwGUp+ofrwU0sxLQLzFEOZ38zvn0DonUIQmZUj1cfz5p1Lu8BllxKbCrn6HnsRJ+LYA6g==",
+      "requires": {
+        "@intlify/shared": "^9.0.0",
+        "js-yaml": "^3.13.1",
+        "json5": "^2.1.1"
+      }
+    },
     "@josephg/resolvable": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/@josephg/resolvable/-/resolvable-1.0.1.tgz",
@@ -24449,6 +24549,32 @@
         }
       }
     },
+    "@nuxtjs/i18n": {
+      "version": "7.2.0",
+      "resolved": "https://registry.npmjs.org/@nuxtjs/i18n/-/i18n-7.2.0.tgz",
+      "integrity": "sha512-fO/QRMxZ0t6LpYJ7ti6mHc0lsog4SMr7Xll3d45zdVSI7+5eBE1YcoG8yXv1sndGQZoq8k0iD1dXuAJA6UL5ww==",
+      "requires": {
+        "@babel/parser": "^7.16.2",
+        "@babel/traverse": "^7.16.0",
+        "@intlify/vue-i18n-extensions": "^1.0.2",
+        "@intlify/vue-i18n-loader": "^1.1.0",
+        "cookie": "^0.4.1",
+        "devalue": "^2.0.1",
+        "is-https": "^4.0.0",
+        "js-cookie": "^3.0.1",
+        "klona": "^2.0.5",
+        "lodash.merge": "^4.6.2",
+        "ufo": "^0.7.9",
+        "vue-i18n": "^8.26.7"
+      },
+      "dependencies": {
+        "cookie": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz",
+          "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA=="
+        }
+      }
+    },
     "@nuxtjs/pwa": {
       "version": "3.3.5",
       "resolved": "https://registry.npmjs.org/@nuxtjs/pwa/-/pwa-3.3.5.tgz",
@@ -31431,6 +31557,11 @@
         "is-extglob": "^2.1.1"
       }
     },
+    "is-https": {
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/is-https/-/is-https-4.0.0.tgz",
+      "integrity": "sha512-FeMLiqf8E5g6SdiVJsPcNZX8k4h2fBs1wp5Bb6uaNxn58ufK1axBqQZdmAQsqh0t9BuwFObybrdVJh6MKyPlyg=="
+    },
     "is-installed-globally": {
       "version": "0.4.0",
       "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz",
@@ -31656,6 +31787,11 @@
       "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==",
       "peer": true
     },
+    "js-cookie": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz",
+      "integrity": "sha512-+0rgsUXZu4ncpPxRL+lNEptWMOWl9etvPHc/koSRp6MPwpRYAhmk0dUG00J4bxVV3r9uUzfo24wW0knS07SKSw=="
+    },
     "js-message": {
       "version": "1.0.7",
       "resolved": "https://registry.npmjs.org/js-message/-/js-message-1.0.7.tgz",
@@ -31773,8 +31909,7 @@
     "klona": {
       "version": "2.0.5",
       "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.5.tgz",
-      "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ==",
-      "dev": true
+      "integrity": "sha512-pJiBpiXMbt7dkzXe8Ghj/u4FfXOOa98fPW+bihOJ4SjnoijweJrNThJfd3ifXpXhREjpoF2mZVH1GfS9LV3kHQ=="
     },
     "last-call-webpack-plugin": {
       "version": "3.0.0",
@@ -37497,6 +37632,11 @@
       "resolved": "https://registry.npmjs.org/vue-hot-reload-api/-/vue-hot-reload-api-2.3.4.tgz",
       "integrity": "sha512-BXq3jwIagosjgNVae6tkHzzIk6a8MHFtzAdwhnV5VlvPTFxDCvIttgSiHWjdGoTJvXtmRu5HacExfdarRcFhog=="
     },
+    "vue-i18n": {
+      "version": "8.26.8",
+      "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-8.26.8.tgz",
+      "integrity": "sha512-BN2OXolO15AKS95yNF8oOtARibaO6RxyKkAYNV4XpOmL7S4eVZYMIDtyvDv+XGZaiUmBJSH9mdNqzexvGMnK2A=="
+    },
     "vue-loader": {
       "version": "15.9.8",
       "resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-15.9.8.tgz",
diff --git a/package.json b/package.json
index dfcddec..7c6fdf6 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
   },
   "dependencies": {
     "@nuxtjs/apollo": "^4.0.1-rc.5",
+    "@nuxtjs/i18n": "^7.2.0",
     "@nuxtjs/pwa": "^3.3.5",
     "bootstrap": "^4.6.1",
     "chart.js": "^3.6.2",
@@ -25,8 +26,8 @@
     "vue": "^2.6.14"
   },
   "devDependencies": {
+    "npm-run-all": "^4.1.5",
     "sass": "^1.45.0",
-    "sass-loader": "^10.2.0",
-    "npm-run-all": "^4.1.5"
+    "sass-loader": "^10.2.0"
   }
 }
diff --git a/pages/chartjs.vue b/pages/chartjs.vue
index 70a37de..868f022 100644
--- a/pages/chartjs.vue
+++ b/pages/chartjs.vue
@@ -32,7 +32,7 @@ export default {
         data: {},
         breadcrumb: [
           {
-            text: 'Accueil',
+            text: this.$t('accueil'),
             to: "/"
           },
           {
@@ -77,6 +77,13 @@ export default {
         return Math.floor(Math.random() * (max - 5 + 1)) + 5
       }
     },
+    nuxtI18n: {
+      paths: {
+        fr: '/graphiques',
+        en: '/graphics',
+        es: '/graficos'
+      }
+    },
     mounted () {
       // Mise à jour du fil d'ariane au chargement
       $nuxt.$emit('changeRoute',this.breadcrumb)
diff --git a/pages/index.vue b/pages/index.vue
index 39b3873..ec0a5b4 100644
--- a/pages/index.vue
+++ b/pages/index.vue
@@ -1,21 +1,23 @@
 <template>
 <main class="container">
-  <h2 class="text-center mb-5 font-weight-light">Entrées et sorties de la toile de confiance des 2 derniers jours</h2>
+  <h2 class="text-center mb-5 font-weight-light">{{ $t('inout') }}</h2>
   <NavigationLoader :isLoading="$apollo.queries.newMembers.loading" />
   <transition name="fade">
     <div class="alert alert-danger" v-if="error">{{ error }}</div>
+  </transition>
+  <transition name="fade">
     <div class="result" v-if="newMembers">
       <div class="row text-center">
         <div class="col-lg-4">
-          <h2 class="h4 text-success">Bienvenue à</h2>
+          <h2 class="h4 text-success">{{ $t('bienvenue') }}</h2>
           <MemberList :members="newMembers['entrees']" :displayPubkey="false" :displayHead="false" />
         </div>
         <div class="col-lg-4">
-          <h2 class="h4 text-danger">Au revoir à</h2>
+          <h2 class="h4 text-danger">{{ $t('aurevoir') }}</h2>
           <MemberList :members="newMembers['sorties']" :displayPubkey="false" :displayHead="false" />
         </div>
         <div class="col-lg-4">
-          <h2 class="h4 text-info">Les revoilà</h2>
+          <h2 class="h4 text-info">{{ $t('revoila') }}</h2>
           <MemberList :members="newMembers['renew']" :displayPubkey="false" :displayHead="false" />
         </div>
       </div>
@@ -34,7 +36,7 @@ export default {
     return {
       breadcrumb: [
         {
-          text: 'Accueil',
+          text: this.$t('accueil'),
           to: '/'
         }
       ],
@@ -61,8 +63,6 @@ export default {
             let member = data.membersCount[i].idList[j]
             member.member.inOut = member.inOut
 
-            console.log(member.member.uid + ' : ' + member.member.history.length)
-
             if (member.member.history.length==1) {
               if (result['entrees'].filter(function(e) { return e.uid === member.member.uid }).length == 0) {
                 result['entrees'].push(member.member)
diff --git a/pages/membres/_hash.vue b/pages/membres/_hash.vue
index f2a79ba..4ba30d5 100644
--- a/pages/membres/_hash.vue
+++ b/pages/membres/_hash.vue
@@ -3,6 +3,8 @@
     <NavigationLoader :isLoading="$apollo.queries.idFromHash.loading" />
     <transition name="fade">
       <div class="alert alert-danger" v-if="error">{{ error }}</div>
+    </transition>
+    <transition name="fade">
       <div v-if="idFromHash">
         <div class="row">
           <div class="col-md-10 col-lg-8 col-xl-6 mx-auto mt-3">
@@ -11,13 +13,13 @@
         </div>
         <div class="row mt-3" v-if="idFromHash.status != 'REVOKED'">
           <div class="col-12 col-md-6 mb-3">
-            <h3>Certifications reçues 
+            <h3>{{ $t('certifications.recues') }} 
               <BadgeCertifStatus :limitDate="idFromHash.received_certifications.limit" :memberStatus="idFromHash.status" />
             </h3>
             <CertifList :certifs="idFromHash.received_certifications.certifications" type="recieved" />
           </div>
           <div class="col-12 col-md-6">
-            <h3>Certifications envoyées</h3>
+            <h3>{{ $t('certifications.envoyees') }}</h3>
             <CertifList :certifs="idFromHash.sent_certifications" type="sent" />
           </div>
         </div>
@@ -35,11 +37,11 @@ export default {
     return {
       breadcrumb: [
         {
-          text: 'Accueil',
+          text: this.$t('accueil'),
           to: '/'
         },
         {
-          text: 'Membres',
+          text: this.$t('membres'),
           to: '/membres'
         },
         {
@@ -57,7 +59,14 @@ export default {
       query: SEARCH_MEMBER,
       variables() {return { hash: this.$route.params.hash }},
       error (err) {this.error = err.message}
-    },
+    }
+  },
+  nuxtI18n: {
+    paths: {
+      fr: '/membres/:hash',
+      en: '/members/:hash',
+      es: '/miembros/:hash'
+    }
   },
   computed: {
     classWarning: function() {
diff --git a/pages/membres/index.vue b/pages/membres/index.vue
index a5ac74c..d086e38 100644
--- a/pages/membres/index.vue
+++ b/pages/membres/index.vue
@@ -1,16 +1,18 @@
 <template>
 <main class="container">
-  <h2 class="text-center mb-5 font-weight-light">Membres</h2>
+  <h2 class="text-center mb-5 font-weight-light">{{ $t('membres') }}</h2>
   <div class="row mb-4">
     <div class="col-6 m-auto text-center">
-      <label for="rech" class="form-label">Votre recherche</label>
+      <label for="rech" class="form-label">{{ $t('recherche.title') }}</label>
       <input type="text" class="form-control" id="rech" aria-describedby="rechHelp" v-model="param" autocomplete="off" @keyup="save">
-      <small id="rechHelp" class="form-text text-muted">Saisissez le début d'un pseudo ou d'une clé publique</small>
+      <small id="rechHelp" class="form-text text-muted">{{ $t('recherche.desc') }}</small>
     </div>
   </div>
   <NavigationLoader :isLoading="$apollo.queries.idSearch.loading" />
   <transition name="fade">
     <div class="alert alert-danger" v-if="error">{{ error }}</div>
+  </transition>
+  <transition name="fade">
     <div class="row" v-if="idSearch && param.length>2 && !$apollo.queries.idSearch.loading">
       <div class="col-8 m-auto">
         <MemberList :members="idSearch.ids"/>
@@ -28,11 +30,11 @@ export default {
     return {
       breadcrumb: [
         {
-          text: 'Accueil',
+          text: this.$t('accueil'),
           to: '/'
         },
         {
-          text: 'Membres',
+          text: this.$t('membres'),
           active: true
         }
       ],
@@ -54,6 +56,13 @@ export default {
       error (err) {this.error = err.message}
     } 
   },
+  nuxtI18n: {
+    paths: {
+      fr: '/membres',
+      en: '/members',
+      es: '/miembros'
+    }
+  },
   mounted () {
     // Mise à jour du fil d'ariane au chargement
     $nuxt.$emit('changeRoute',this.breadcrumb)
diff --git a/pages/previsions/_hash.vue b/pages/previsions/_hash.vue
index 4cfc380..66a8d7c 100644
--- a/pages/previsions/_hash.vue
+++ b/pages/previsions/_hash.vue
@@ -3,6 +3,8 @@
     <NavigationLoader :isLoading="$apollo.queries.idFromHash.loading" />
     <transition name="fade">
       <div class="alert alert-danger" v-if="error">{{ error }}</div>
+    </transition>
+    <transition name="fade">
       <div v-if="idFromHash">
         <div class="row">
           <div class="col-md-10 col-lg-8 col-xl-6 mx-auto mt-3">
@@ -24,11 +26,11 @@ export default {
     return {
       breadcrumb: [
         {
-          text: 'Accueil',
+          text: this.$t('accueil'),
           to: '/'
         },
         {
-          text: 'Prévisions',
+          text: this.$t('previsions.title'),
           to: '/previsions'
         },
         {
@@ -48,6 +50,13 @@ export default {
       error (err) {this.error = err.message}
     },
   },
+  nuxtI18n: {
+    paths: {
+      fr: '/previsions/:hash',
+      en: '/forecasts/:hash',
+      es: '/pronosticos/:hash'
+    }
+  },
   computed: {
     classWarning: function() {
       return {
diff --git a/pages/previsions/newcomers.vue b/pages/previsions/futurs_membres.vue
similarity index 85%
rename from pages/previsions/newcomers.vue
rename to pages/previsions/futurs_membres.vue
index 57df5cc..9a90ad8 100644
--- a/pages/previsions/newcomers.vue
+++ b/pages/previsions/futurs_membres.vue
@@ -3,12 +3,14 @@
   <NavigationLoader :isLoading="$apollo.queries.wwResult.loading" />
   <transition name="fade">
     <div class="alert alert-danger" v-if="error">{{ error }}</div>
+  </transition>
+  <transition name="fade">
     <div v-if="wwResult">
-      <h2 class="text-center mb-5 font-weight-light">Prévisions <small><span class="badge badge-secondary">{{ wwResult.dossiers_nb }} dossiers en attente</span></small></h2>
+      <h2 class="text-center mb-5 font-weight-light">{{ $t('previsions.title') }} <small><span class="badge badge-secondary">{{ $tc('previsions.dossiersattente', wwResult.dossiers_nb) }}</span></small></h2>
       <div class="alert alert-info" role="alert">
         <ul class="list-unstyled m-0">
-          <li>{{ wwResult.certifs_nb }} certifications internes</li>
-          <li>{{ wwResult.permutations_nb }} permutations</li>
+          <li>{{ $tc('previsions.certificationsinternes', wwResult.certifs_nb) }}</li>
+          <li>{{ $tc('previsions.permutations', wwResult.permutations_nb) }}</li>
         </ul>
       </div>
       <div class="row">
@@ -16,19 +18,19 @@
           <div class="form-check form-check-inline">
             <input class="form-check-input" type="radio" id="forecastsByNames" value="forecastsByNames" checked v-model="display" @change="save">
             <label class="form-check-label" for="forecastsByNames">
-              Tri par membres
+              {{ $t('tri.parmembres') }}
             </label>
           </div>
           <div class="form-check form-check-inline">
             <input class="form-check-input" type="radio" id="forecastsByDates" value="forecastsByDates" v-model="display" @change="save">
             <label class="form-check-label" for="forecastsByDates">
-              Tri par dates
+              {{ $t('tri.pardate') }}
             </label>
           </div>
         </div>
         <div class="col-lg-8 m-auto">
           <div v-if="display=='forecastsByNames'">
-            <h3>Prévisions par membres</h3>
+            <h3>{{ $t('previsions.parmembre') }}</h3>
             <div class="table-responsive">
               <table class="table table-striped table-hover">
                 <tbody>
@@ -39,7 +41,7 @@
                     </th>
                     <td class="p-0">
                       <div class="d-flex justify-content-between p-3" v-for="date in forecast.forecasts" :key="date.date">
-                        <div>{{ date.date | formatDateHeure }}</div>
+                        <div>{{ $d(new Date(date.date*1000), 'long') }} {{ $t('time.a') }} {{ $d(new Date(date.date*1000), 'hour') }}</div>
                         <div>{{ date.proba * 100 }} %</div>
                       </div>
                     </td>
@@ -49,7 +51,7 @@
             </div>
           </div>
           <div v-if="display=='forecastsByDates'">
-            <h3>Prévisions par dates</h3>
+            <h3>{{ $t('previsions.pardate') }}</h3>
             <div class="table-responsive">
               <table class="table table-striped">
                 <tbody>
@@ -83,11 +85,11 @@ export default {
     return {
       breadcrumb: [
         {
-          text: 'Accueil',
+          text: this.$t('accueil'),
           to: '/'
         },
         {
-          text: 'Prévisions',
+          text: this.$t('previsions.title'),
           to: '/previsions'
         },
         {
@@ -165,6 +167,13 @@ export default {
       error (err) {this.error = err.message}
     }
   },
+  nuxtI18n: {
+    paths: {
+      fr: '/previsions/futurs_membres',
+      en: '/forecasts/future_members',
+      es: '/pronosticos/futuros_miembros'
+    }
+  },
   mounted () {
     // Mise à jour du fil d'ariane au chargement
     $nuxt.$emit('changeRoute',this.breadcrumb)
diff --git a/pages/previsions/index.vue b/pages/previsions/index.vue
index 1481388..ba77d5d 100644
--- a/pages/previsions/index.vue
+++ b/pages/previsions/index.vue
@@ -22,11 +22,11 @@ export default {
     return {
       breadcrumb: [
         {
-          text: 'Accueil',
+          text: this.$t('accueil'),
           to: '/'
         },
         {
-          text: 'Prévisions',
+          text: this.$t('previsions.title'),
           active: true
         }
       ],
@@ -48,6 +48,13 @@ export default {
       error (err) {this.error = err.message}
     }
   },
+  nuxtI18n: {
+    paths: {
+      fr: '/previsions',
+      en: '/forecasts',
+      es: '/pronosticos'
+    }
+  },
   mounted () {
     // Mise à jour du fil d'ariane au chargement
     $nuxt.$emit('changeRoute',this.breadcrumb)
-- 
GitLab