From 0cfba3785fd0747a8b3d25b3e1b507acf5dda997 Mon Sep 17 00:00:00 2001
From: VIAUD Nathan <nathan.viaud@inria.fr>
Date: Tue, 18 Mar 2025 12:09:48 +0100
Subject: [PATCH 01/18] WIP: translate(landing page)

---
 package-lock.json         | 65 +++++++++++++++++++++++++++++++++++++++
 package.json              |  1 +
 src/i18n/fr.ts            | 13 ++++++++
 src/main.ts               | 11 +++++++
 src/views/LandingPage.vue | 12 ++++----
 5 files changed, 96 insertions(+), 6 deletions(-)
 create mode 100644 src/i18n/fr.ts

diff --git a/package-lock.json b/package-lock.json
index 8d2dcd74..7179623f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -27,6 +27,7 @@
                 "serve-static": "^1.15.0",
                 "update-electron-app": "^2.0.1",
                 "vue": "3.3",
+                "vue-i18n": "^10.0.6",
                 "vue-router": "^4.1.6",
                 "vue-tippy": "^6.0.0",
                 "vuedraggable": "^4.1.0"
@@ -1298,6 +1299,50 @@
             "dev": true,
             "license": "BSD-3-Clause"
         },
+        "node_modules/@intlify/core-base": {
+            "version": "10.0.6",
+            "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-10.0.6.tgz",
+            "integrity": "sha512-/NINGvy7t8qSCyyuqMIPmHS6CBQjqPIPVOps0Rb7xWrwwkwHJKtahiFnW1HC4iQVhzoYwEW6Js0923zTScLDiA==",
+            "license": "MIT",
+            "dependencies": {
+                "@intlify/message-compiler": "10.0.6",
+                "@intlify/shared": "10.0.6"
+            },
+            "engines": {
+                "node": ">= 16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/kazupon"
+            }
+        },
+        "node_modules/@intlify/message-compiler": {
+            "version": "10.0.6",
+            "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-10.0.6.tgz",
+            "integrity": "sha512-QcUYprK+e4X2lU6eJDxLuf/mUtCuVPj2RFBoFRlJJxK3wskBejzlRvh1Q0lQCi9tDOnD4iUK1ftcGylE3X3idA==",
+            "license": "MIT",
+            "dependencies": {
+                "@intlify/shared": "10.0.6",
+                "source-map-js": "^1.0.2"
+            },
+            "engines": {
+                "node": ">= 16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/kazupon"
+            }
+        },
+        "node_modules/@intlify/shared": {
+            "version": "10.0.6",
+            "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-10.0.6.tgz",
+            "integrity": "sha512-2xqwm05YPpo7TM//+v0bzS0FWiTzsjpSMnWdt7ZXs5/ZfQIedSuBXIrskd8HZ7c/cZzo1G9ALHTksnv/74vk/Q==",
+            "license": "MIT",
+            "engines": {
+                "node": ">= 16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/kazupon"
+            }
+        },
         "node_modules/@isaacs/cliui": {
             "version": "8.0.2",
             "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -19659,6 +19704,26 @@
                 "node": ">=4.0"
             }
         },
+        "node_modules/vue-i18n": {
+            "version": "10.0.6",
+            "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-10.0.6.tgz",
+            "integrity": "sha512-pQPspK5H4srzlu+47+HEY2tmiY3GyYIvSPgSBdQaYVWv7t1zj1t9p1FvHlxBXyJ17t9stG/Vxj+pykrvPWBLeQ==",
+            "license": "MIT",
+            "dependencies": {
+                "@intlify/core-base": "10.0.6",
+                "@intlify/shared": "10.0.6",
+                "@vue/devtools-api": "^6.5.0"
+            },
+            "engines": {
+                "node": ">= 16"
+            },
+            "funding": {
+                "url": "https://github.com/sponsors/kazupon"
+            },
+            "peerDependencies": {
+                "vue": "^3.0.0"
+            }
+        },
         "node_modules/vue-router": {
             "version": "4.4.5",
             "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.4.5.tgz",
diff --git a/package.json b/package.json
index 07917872..87a336f7 100644
--- a/package.json
+++ b/package.json
@@ -44,6 +44,7 @@
         "serve-static": "^1.15.0",
         "update-electron-app": "^2.0.1",
         "vue": "3.3",
+        "vue-i18n": "^10.0.6",
         "vue-router": "^4.1.6",
         "vue-tippy": "^6.0.0",
         "vuedraggable": "^4.1.0"
diff --git a/src/i18n/fr.ts b/src/i18n/fr.ts
new file mode 100644
index 00000000..97f02880
--- /dev/null
+++ b/src/i18n/fr.ts
@@ -0,0 +1,13 @@
+export const frMessages = {
+    global: {
+        open: 'Ouvrir',
+    },
+    landing: {
+        published: "Cet ePoc est une version publiée, vous devez l'importer avant de pouvoir l'éditer ici",
+        loadingPath: 'Chargement de {path}',
+        loadingEpoc: "Chargement de l'ePoc",
+        open: 'Ouvrir un projet existant',
+        create: 'Créer un nouveau projet',
+        recents: 'Fichiers récents',
+    },
+};
diff --git a/src/main.ts b/src/main.ts
index 99d548e1..11d66f3b 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -6,10 +6,21 @@ import { createPinia } from 'pinia';
 import VueTippy from 'vue-tippy';
 import 'tippy.js/dist/tippy.css';
 import draggable from 'vuedraggable';
+import { createI18n } from 'vue-i18n';
+import { frMessages } from './i18n/fr';
+
+const i18n = createI18n({
+    locale: 'fr',
+    fallbackLocale: 'fr',
+    messages: {
+        fr: frMessages,
+    },
+});
 
 const app = createApp(App);
 app.use(router);
 app.use(createPinia());
 app.use(VueTippy);
+app.use(i18n);
 app.component('VueDraggable', draggable);
 app.mount('#app');
diff --git a/src/views/LandingPage.vue b/src/views/LandingPage.vue
index 3d9b0ef3..5577f3a6 100644
--- a/src/views/LandingPage.vue
+++ b/src/views/LandingPage.vue
@@ -44,13 +44,13 @@ function importProject() {
             @accept="importProject"
             @cancel="cancelImport"
         >
-            <h3>Cet ePoc est une version publiée, vous devez l'importer avant de pouvoir l'éditer ici</h3>
+            <h3>{{ $t('landing.published') }}</h3>
         </ChoiceModal>
 
         <div v-if="editorStore.loading" class="loading">
             <div class="spinner"></div>
             <span v-if="editorStore.currentProject.filepath">
-                Chargement de "{{ editorStore.currentProject.filepath }}"
+                {{ $t('landing.loadingPath', { path: editorStore.currentProject.filepath }) }}
             </span>
             <span v-else>Chargement de l'ePoc</span>
         </div>
@@ -59,15 +59,15 @@ function importProject() {
             <div class="buttons">
                 <button class="btn btn-outline btn-large" @click="pickProject">
                     <i class="icon-ouvrir" />
-                    Ouvrir un projet existant
+                    {{ $t('landing.open') }}
                 </button>
                 <button class="btn btn-outline btn-large" @click="createProject">
                     <i class="icon-creer" />
-                    Créer un nouveau projet
+                    {{ $t('landing.create') }}
                 </button>
             </div>
             <div>
-                <h3>Fichiers récents</h3>
+                <h3>{{ $t('landing.recents') }}</h3>
                 <hr class="separator" />
                 <div v-for="epoc of editorStore.recentProjects" :key="epoc.name" class="card-list-item">
                     <div class="card-icon">
@@ -78,7 +78,7 @@ function importProject() {
                     </p>
                     <small>{{ new Date(epoc.modified).toLocaleString() }}</small>
                     <hr class="vertical-separator" />
-                    <div class="btn-open" @click="openProject(epoc)">Ouvrir</div>
+                    <div class="btn-open" @click="openProject(epoc)">{{ $t('global.open') }}</div>
                 </div>
             </div>
         </div>
-- 
GitLab


From 3b37aeb0dd311ccc18d37658974b7c338286fbcb Mon Sep 17 00:00:00 2001
From: VIAUD Nathan <nathan.viaud@inria.fr>
Date: Tue, 18 Mar 2025 14:14:29 +0100
Subject: [PATCH 02/18] WIP: translate components

---
 src/components/ChoiceModal.vue     | 23 ++++++-----------------
 src/components/ValidationModal.vue |  6 +++---
 src/i18n/fr.ts                     | 10 ++++++++++
 src/views/EditorPage.vue           | 13 ++++++++-----
 src/views/LandingPage.vue          |  2 +-
 5 files changed, 28 insertions(+), 26 deletions(-)

diff --git a/src/components/ChoiceModal.vue b/src/components/ChoiceModal.vue
index 0202008a..b5a4dcd9 100644
--- a/src/components/ChoiceModal.vue
+++ b/src/components/ChoiceModal.vue
@@ -1,15 +1,10 @@
 <script setup lang="ts">
 import { ref, onMounted } from 'vue';
 
-interface Props {
+defineProps<{
     acceptLabel: string;
     cancelLabel: string;
-}
-
-withDefaults(defineProps<Props>(), {
-    acceptLabel: 'Accepter',
-    cancelLabel: 'Annuler',
-});
+}>();
 
 const emits = defineEmits<{
     (e: 'accept'): void;
@@ -32,18 +27,12 @@ onMounted(() => {
 </script>
 
 <template>
-    <div
-        ref="modalScreen"
-        class="modal-backdrop"
-        tabindex="0"
-        @keyup.enter="validate"
-        @keyup.esc="cancel"
-    >
+    <div ref="modalScreen" class="modal-backdrop" tabindex="0" @keyup.enter="validate" @keyup.esc="cancel">
         <div class="modal">
             <slot />
             <button class="btn btn-close" @click="cancel"><i class="icon-x"></i></button>
-            <button class="btn-choice accept" @click="validate">{{ acceptLabel }}</button>
-            <button class="btn-choice cancel" @click="cancel">{{ cancelLabel }}</button>
+            <button class="btn-choice accept" @click="validate">{{ acceptLabel ?? $t('global.accept') }}</button>
+            <button class="btn-choice cancel" @click="cancel">{{ cancelLabel ?? $t('global.cancel') }}</button>
         </div>
     </div>
 </template>
@@ -95,4 +84,4 @@ onMounted(() => {
         color: #fff;
     }
 }
-</style>
\ No newline at end of file
+</style>
diff --git a/src/components/ValidationModal.vue b/src/components/ValidationModal.vue
index 11a0db7e..7495f960 100644
--- a/src/components/ValidationModal.vue
+++ b/src/components/ValidationModal.vue
@@ -14,12 +14,12 @@ function confirmDelete() {
 
 <template>
     <ChoiceModal
-        accept-label="OUI, SUPPRIMER"
-        cancel-label="NON, NE PAS SUPPRIMER"
+        :accept-label="$t('validation.yes')"
+        :cancel-label="$t('validation.no')"
         @cancel="editorStore.validationModal = false"
         @accept="confirmDelete"
     >
-        <h3>Souhaitez-vous vraiment supprimer cet élément ?</h3>
+        <h3>{{ $t('validation.confirm') }}</h3>
     </ChoiceModal>
 </template>
 
diff --git a/src/i18n/fr.ts b/src/i18n/fr.ts
index 97f02880..0aed7147 100644
--- a/src/i18n/fr.ts
+++ b/src/i18n/fr.ts
@@ -1,6 +1,13 @@
 export const frMessages = {
     global: {
         open: 'Ouvrir',
+        cancel: 'Annuler',
+        accept: 'Accepter',
+    },
+    validation: {
+        yes: 'OUI, SUPPRIMER',
+        no: 'NON, NE PAS SUPPRIMER',
+        confirm: 'Êtes-vous sûr de vouloir supprimer cet élément ?',
     },
     landing: {
         published: "Cet ePoc est une version publiée, vous devez l'importer avant de pouvoir l'éditer ici",
@@ -10,4 +17,7 @@ export const frMessages = {
         create: 'Créer un nouveau projet',
         recents: 'Fichiers récents',
     },
+    editor: {
+        select: "Cliquer sur l'élément de contenu concerné par la condition pour la sélectionner",
+    },
 };
diff --git a/src/views/EditorPage.vue b/src/views/EditorPage.vue
index 534e3cde..13fe3fb1 100644
--- a/src/views/EditorPage.vue
+++ b/src/views/EditorPage.vue
@@ -32,8 +32,11 @@ function addKeyboardEvent(event: KeyboardEvent) {
     if (metaKey || ctrlKey) {
         if (key === 'v') {
             // Permits to paste in the WYSIWYG link editor modal
-            if((event.target as HTMLElement).className.indexOf('tox-textfield') !== -1
-                || (event.target as HTMLElement).className.indexOf('tox-textarea') !== -1) return;
+            if (
+                (event.target as HTMLElement).className.indexOf('tox-textfield') !== -1 ||
+                (event.target as HTMLElement).className.indexOf('tox-textarea') !== -1
+            )
+                return;
 
             event.preventDefault();
             saveState();
@@ -82,7 +85,7 @@ function onRemoveCursor() {
 }
 
 const editorDisplay = computed(() => (editorStore.selectNodeMode ? 'editor-flex' : 'editor-grid'));
-const sideMenuOpen = computed(() => editorStore.sideMenuOpen ? 'side-menu-open' : '');
+const sideMenuOpen = computed(() => (editorStore.sideMenuOpen ? 'side-menu-open' : ''));
 </script>
 
 <template>
@@ -95,8 +98,8 @@ const sideMenuOpen = computed(() => editorStore.sideMenuOpen ? 'side-menu-open'
         <SideBar v-if="!editorStore.selectNodeMode" class="side-bar" @dragover="onCursorNotAllowed" />
         <TopBar v-if="!editorStore.selectNodeMode" class="top-bar" @dragover="onCursorNotAllowed" />
         <div v-if="editorStore.selectNodeMode" class="flex-information">
-            <h4>Cliquer sur l'élément de contenu concerné par la condition pour la sélectionner</h4>
-            <button class="btn btn-top-bar" @click="exitSelectNodeMode()">Annuler</button>
+            <h4>{{ $t('editor.select') }}</h4>
+            <button class="btn btn-top-bar" @click="exitSelectNodeMode()">{{ $t('global.cancel') }}</button>
         </div>
         <ePocFlow class="editor-content" @dragover="onCursorAllowed" />
         <Transition>
diff --git a/src/views/LandingPage.vue b/src/views/LandingPage.vue
index 5577f3a6..0bd10472 100644
--- a/src/views/LandingPage.vue
+++ b/src/views/LandingPage.vue
@@ -52,7 +52,7 @@ function importProject() {
             <span v-if="editorStore.currentProject.filepath">
                 {{ $t('landing.loadingPath', { path: editorStore.currentProject.filepath }) }}
             </span>
-            <span v-else>Chargement de l'ePoc</span>
+            <span v-else>{{ $t('landing.loadingEpoc') }}</span>
         </div>
 
         <div v-else>
-- 
GitLab


From 2b06c99f120e37003e61c34d35caa69cc7713018 Mon Sep 17 00:00:00 2001
From: VIAUD Nathan <nathan.viaud@inria.fr>
Date: Tue, 18 Mar 2025 14:31:01 +0100
Subject: [PATCH 03/18] WIP: translate(badge)

---
 src/features/badge/components/BadgePreview.vue   |  4 ++--
 .../components/ConditionElementSelector.vue      |  4 ++--
 src/features/badge/components/ConditionItem.vue  |  6 +++---
 src/features/badge/components/ConditionModal.vue | 10 ++++++----
 src/features/badge/components/ConditionValue.vue | 10 +++++-----
 src/features/badge/components/IconModal.vue      |  6 +++---
 src/i18n/fr.ts                                   | 16 ++++++++++++++++
 7 files changed, 37 insertions(+), 19 deletions(-)

diff --git a/src/features/badge/components/BadgePreview.vue b/src/features/badge/components/BadgePreview.vue
index 401aad1f..09352c64 100644
--- a/src/features/badge/components/BadgePreview.vue
+++ b/src/features/badge/components/BadgePreview.vue
@@ -36,11 +36,11 @@ const fileInput = ref(null);
     <div class="new-icon">
         <BadgeItem :icon="blob" :inactive="!url" @click="onClick" />
         <div>
-            <p class="accepted-files">Fichier supporté: SVG</p>
+            <p class="accepted-files">{{ $t('badge.supported') }}</p>
             <div v-if="!url">
                 <button id="file-selector" class="btn btn-form" @click="openFile">
                     <i class="icon-plus"></i>
-                    Sélectionner un fichier
+                    {{ $t('badge.select') }}
                 </button>
             </div>
             <div v-show="url">
diff --git a/src/features/badge/components/ConditionElementSelector.vue b/src/features/badge/components/ConditionElementSelector.vue
index 76108b23..30ebfdc2 100644
--- a/src/features/badge/components/ConditionElementSelector.vue
+++ b/src/features/badge/components/ConditionElementSelector.vue
@@ -14,9 +14,9 @@ function onClick() {
 
 <template>
     <div class="select">
-        Elément
+        {{ $t('global.element') }}
         <div class="select-input" @click="onClick">
-            <p>{{ inputValue || 'Veuillez sélectionner' }}</p>
+            <p>{{ inputValue || $t('global.pleaseSelect') }}</p>
             <i class="icon-cible"></i>
         </div>
     </div>
diff --git a/src/features/badge/components/ConditionItem.vue b/src/features/badge/components/ConditionItem.vue
index f9eca07a..90b6b492 100644
--- a/src/features/badge/components/ConditionItem.vue
+++ b/src/features/badge/components/ConditionItem.vue
@@ -63,7 +63,7 @@ function handleVerbChange(value: string) {
     <article>
         <i class="icon-supprimer delete" @click.stop="removeCondition"></i>
         <div class="logic-condition">
-            <button class="logic-choice active">ET</button>
+            <button class="logic-choice active">{{ $t('global.and').toUpperCase() }}</button>
         </div>
         <!-- Condition switch -->
         <!-- <div v-if="conditionIndex !== 0" class="logic-condition">
@@ -81,7 +81,7 @@ function handleVerbChange(value: string) {
                 class="grid-item"
             />
             <div class="select">
-                Condition
+                {{ $t('global.condition') }}
                 <select
                     id="condition"
                     :disabled="verbDisabled"
@@ -89,7 +89,7 @@ function handleVerbChange(value: string) {
                     :value="currentCondition.verb"
                     @change="handleVerbChange(($event.target as HTMLSelectElement).value)"
                 >
-                    <option value="">Veuillez choisir</option>
+                    <option value="">{{ $t('global.pleaseSelect') }}</option>
                     <option v-for="(description, verb) in getVerbs(elementType)" :key="verb" :value="verb">
                         {{ description.label }}
                     </option>
diff --git a/src/features/badge/components/ConditionModal.vue b/src/features/badge/components/ConditionModal.vue
index 93842c20..2b0ddfef 100644
--- a/src/features/badge/components/ConditionModal.vue
+++ b/src/features/badge/components/ConditionModal.vue
@@ -53,7 +53,7 @@ onMounted(() => {
         <article class="condition-modal">
             <header>
                 <div class="content">
-                    <h2>Conditions d'obtention du badge</h2>
+                    <h2>{{ $t('badge.conditions') }}</h2>
                     <button class="btn btn-close" @click="close"><i class="icon-x"></i></button>
                 </div>
             </header>
@@ -68,14 +68,16 @@ onMounted(() => {
                 />
                 <button class="add btn btn-form" @click="addCondition">
                     <i class="icon-plus"></i>
-                    Ajouter une condition
+                    {{ $t('badge.add') }}
                 </button>
             </div>
 
             <footer>
                 <div class="content">
-                    <button class="btn-choice cancel" @click="close">ANNULER</button>
-                    <button :disabled="!allConditionsValid" class="btn-choice save" @click="save">ENREGISTRER</button>
+                    <button class="btn-choice cancel" @click="close">{{ $t('global.cancel').toUpperCase() }}</button>
+                    <button :disabled="!allConditionsValid" class="btn-choice save" @click="save">
+                        {{ $t('global.save').toUpperCase() }}
+                    </button>
                 </div>
             </footer>
         </article>
diff --git a/src/features/badge/components/ConditionValue.vue b/src/features/badge/components/ConditionValue.vue
index b9f8b790..26bf3c7b 100644
--- a/src/features/badge/components/ConditionValue.vue
+++ b/src/features/badge/components/ConditionValue.vue
@@ -37,14 +37,14 @@ watch(
             :disabled="verb === ''"
             @change="
                 onChange(
-                    ($event.target as HTMLSelectElement).value !== ''
-                        ? ($event.target as HTMLSelectElement).value === 'true'
-                        : '',
+                    ($event.target as HTMLSelectElement).value !== '' ?
+                        ($event.target as HTMLSelectElement).value === 'true'
+                    :   '',
                 )
             "
         >
-            <option value="true">Vrai</option>
-            <option value="false">Faux</option>
+            <option value="true">{{ $t('global.true') }}</option>
+            <option value="false">{{ $t('global.false') }}</option>
         </select>
         <input
             v-else-if="valueType === 'number'"
diff --git a/src/features/badge/components/IconModal.vue b/src/features/badge/components/IconModal.vue
index adfa2fa4..00970309 100644
--- a/src/features/badge/components/IconModal.vue
+++ b/src/features/badge/components/IconModal.vue
@@ -46,12 +46,12 @@ onMounted(() => {
         <article class="condition-modal">
             <header>
                 <div class="content">
-                    <h2>Sélectionner une icône</h2>
+                    <h2>{{ $t('badge.selectIcon') }}</h2>
                     <button class="btn btn-close" @click="close"><i class="icon-x"></i></button>
                 </div>
             </header>
             <div class="content">
-                <h3>Icônes par défaut</h3>
+                <h3>{{ $t('badge.defaultIcons') }}</h3>
                 <hr class="separator" />
                 <div class="badges">
                     <BadgeItem
@@ -64,7 +64,7 @@ onMounted(() => {
             </div>
 
             <div class="content">
-                <h3>Icônes personnalisées</h3>
+                <h3>{{ $t('badge.customIcons') }}</h3>
                 <hr class="separator" />
                 <div class="badges">
                     <BadgeItem
diff --git a/src/i18n/fr.ts b/src/i18n/fr.ts
index 0aed7147..f57c2e2c 100644
--- a/src/i18n/fr.ts
+++ b/src/i18n/fr.ts
@@ -3,6 +3,13 @@ export const frMessages = {
         open: 'Ouvrir',
         cancel: 'Annuler',
         accept: 'Accepter',
+        element: 'Élément',
+        pleaseSelect: 'Veuillez sélectionner',
+        and: 'et',
+        condition: 'Condition',
+        save: 'Sauvegarder',
+        true: 'Vrai',
+        false: 'Faux',
     },
     validation: {
         yes: 'OUI, SUPPRIMER',
@@ -20,4 +27,13 @@ export const frMessages = {
     editor: {
         select: "Cliquer sur l'élément de contenu concerné par la condition pour la sélectionner",
     },
+    badge: {
+        supported: 'Fichier supporté: SVG',
+        select: 'Sélectionner un fichier',
+        conditions: "Conditions d'obtention du badge",
+        add: 'Ajouter une condition',
+        selectIcon: 'Sélectionner une icône',
+        defaultIcons: 'Icônes par défaut',
+        customIcons: 'Icônes personnalisées',
+    },
 };
-- 
GitLab


From 2e5850017bb2b21e7223e0647042b89212ca7d25 Mon Sep 17 00:00:00 2001
From: VIAUD Nathan <nathan.viaud@inria.fr>
Date: Tue, 18 Mar 2025 15:04:59 +0100
Subject: [PATCH 04/18] WIP: translate (inputs)

---
 src/features/forms/components/inputs/HtmlInput.vue  |  5 ++++-
 .../forms/components/inputs/RepeatInput.vue         |  7 +++++--
 .../inputs/badges/components/ConditionInput.vue     |  2 +-
 .../inputs/badges/components/IconPicker.vue         |  2 +-
 .../inputs/card/components/RadioInput.vue           |  4 ++--
 .../inputs/card/components/SelectInput.vue          | 13 ++++++-------
 src/i18n/fr.ts                                      | 11 +++++++++++
 7 files changed, 30 insertions(+), 14 deletions(-)

diff --git a/src/features/forms/components/inputs/HtmlInput.vue b/src/features/forms/components/inputs/HtmlInput.vue
index b999d8be..d3491cf5 100644
--- a/src/features/forms/components/inputs/HtmlInput.vue
+++ b/src/features/forms/components/inputs/HtmlInput.vue
@@ -4,6 +4,9 @@ import { getTinymce } from '@tinymce/tinymce-vue/lib/cjs/main/ts/TinyMCE';
 import { Ref, ref, watch } from 'vue';
 import { graphService } from '@/src/shared/services';
 import { getCurrentState } from '@/src/shared/services/undoRedo.service';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
 
 const props = defineProps<{
     type: 'html' | 'html-text' | 'html-inline';
@@ -66,7 +69,7 @@ const standardOptions = {
     min_height: 150,
     max_height: 800,
     height: 350,
-    templates: [{ title: 'Plier/déplier', content: template, description: 'Plier/déplier avec titre et contenu' }],
+    templates: [{ title: t('inputs.accordion'), content: template, description: t('inputs.accordionDescription') }],
     file_picker_types: 'image',
     file_picker_callback: handleFilePicker,
     link_default_target: '_blank',
diff --git a/src/features/forms/components/inputs/RepeatInput.vue b/src/features/forms/components/inputs/RepeatInput.vue
index 7d118557..0d9b2d10 100644
--- a/src/features/forms/components/inputs/RepeatInput.vue
+++ b/src/features/forms/components/inputs/RepeatInput.vue
@@ -13,6 +13,9 @@ import {
 } from '@/src/shared/interfaces';
 import { ref } from 'vue';
 import { generateContentId } from '@/src/shared/services/graph.service';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
 
 const props = defineProps<{
     id: string;
@@ -136,7 +139,7 @@ function dragOver(event: DragEvent) {
 // Used to get "choice left/right" on swipe choice
 function getLabelIdentifier(index) {
     if (props.addButton === false) {
-        return index === 0 ? 'droite' : 'gauche';
+        return index === 0 ? t('global.right') : t('global.left');
     } else return index + 1;
 }
 </script>
@@ -199,7 +202,7 @@ function getLabelIdentifier(index) {
     <AddCard
         v-if="addButton !== false"
         :data-testid="`${id}-add`"
-        placeholder="Ajouter"
+        :placeholder="t('global.add')"
         class="add-card"
         @click="addCard"
     />
diff --git a/src/features/forms/components/inputs/badges/components/ConditionInput.vue b/src/features/forms/components/inputs/badges/components/ConditionInput.vue
index 89dca04c..30962353 100644
--- a/src/features/forms/components/inputs/badges/components/ConditionInput.vue
+++ b/src/features/forms/components/inputs/badges/components/ConditionInput.vue
@@ -48,7 +48,7 @@ function handleMouseLeave() {
     </ul>
     <button class="btn btn-form" @click="onClick">
         <i class="icon-plus"></i>
-        Configurer les conditions
+        {{ $t('inputs.manageConditions') }}
     </button>
 </template>
 
diff --git a/src/features/forms/components/inputs/badges/components/IconPicker.vue b/src/features/forms/components/inputs/badges/components/IconPicker.vue
index 135fe49d..007fee3b 100644
--- a/src/features/forms/components/inputs/badges/components/IconPicker.vue
+++ b/src/features/forms/components/inputs/badges/components/IconPicker.vue
@@ -19,7 +19,7 @@ function openIconModal() {
         <BadgeItem :icon="inputValue" :view-mode="true" :inactive="!inputValue" />
         <button class="btn btn-form" @click="openIconModal">
             <i class="icon-plus"></i>
-            Modifier l'icône
+            {{ $t('inputs.updateIcon') }}
         </button>
     </div>
 </template>
diff --git a/src/features/forms/components/inputs/card/components/RadioInput.vue b/src/features/forms/components/inputs/card/components/RadioInput.vue
index 626ca022..6b4ce097 100644
--- a/src/features/forms/components/inputs/card/components/RadioInput.vue
+++ b/src/features/forms/components/inputs/card/components/RadioInput.vue
@@ -34,7 +34,7 @@ function onChange(value: string) {
                     :checked="inputValue === '1'"
                     @change="onChange('1')"
                 />
-                <label :for="'left-' + id">Choix gauche</label>
+                <label :for="'left-' + id">{{ $t('inputs.leftChoice') }}</label>
             </div>
             <div class="radio-btn">
                 <input
@@ -45,7 +45,7 @@ function onChange(value: string) {
                     type="radio"
                     @change="onChange('2')"
                 />
-                <label :for="'right-' + id">Choix droite</label>
+                <label :for="'right-' + id">{{ $t('inputs.rightChoice') }}</label>
             </div>
         </div>
     </div>
diff --git a/src/features/forms/components/inputs/card/components/SelectInput.vue b/src/features/forms/components/inputs/card/components/SelectInput.vue
index f7ae8a63..6ec9d6c2 100644
--- a/src/features/forms/components/inputs/card/components/SelectInput.vue
+++ b/src/features/forms/components/inputs/card/components/SelectInput.vue
@@ -22,29 +22,28 @@ const currentNode = editorStore.getCurrentGraphNode;
 const currentContent = currentNode.data.elements.find(({ id }) => id === editorStore.openedElementId);
 
 function getOptions() {
-    if(!props.linkedOptions) return props.options;
+    if (!props.linkedOptions) return props.options;
 
     // In this case we have to change the epoc formValues
     //? refactor this if another case is needed
-    if(props.id === 'template') {
+    if (props.id === 'template') {
         const epocNode = editorStore.getEpocNode;
 
         return walkObjectPath(epocNode.data.formValues, props.linkedOptions);
     } else {
         return currentContent.formValues[props.linkedOptions];
-
     }
 }
 
 function walkObjectPath(object: any, path: string) {
     const currentKey = path.split('.')[0];
 
-    if(!currentKey) {
+    if (!currentKey) {
         return object;
     }
 
-    if(currentKey === '*') {
-        if(!object) return [];
+    if (currentKey === '*') {
+        if (!object) return [];
 
         return object.map((item: any) => walkObjectPath(item, path.slice(2)));
     } else {
@@ -65,7 +64,7 @@ function onChange(event: Event) {
 <template>
     <div class="select">
         <select :id="id" :value="inputValue" class="select-box" @change="onChange">
-            <option value="">Sélectionnez</option>
+            <option value="">{{ $t('global.pleaseSelect') }}</option>
             <option v-for="(option, index) in getOptions()" :key="index" :value="option">{{ option }}</option>
         </select>
     </div>
diff --git a/src/i18n/fr.ts b/src/i18n/fr.ts
index f57c2e2c..1f8440f0 100644
--- a/src/i18n/fr.ts
+++ b/src/i18n/fr.ts
@@ -10,6 +10,9 @@ export const frMessages = {
         save: 'Sauvegarder',
         true: 'Vrai',
         false: 'Faux',
+        right: 'droite',
+        left: 'gauche',
+        add: 'Ajouter',
     },
     validation: {
         yes: 'OUI, SUPPRIMER',
@@ -36,4 +39,12 @@ export const frMessages = {
         defaultIcons: 'Icônes par défaut',
         customIcons: 'Icônes personnalisées',
     },
+    inputs: {
+        manageConditions: 'Configurer les conditions',
+        updateIcon: "Modifier l'icône",
+        leftChoice: 'Choix gauche',
+        rightChoice: 'Choix droit',
+        accordion: 'Plier/déplier',
+        accordionDescription: 'Plier/déplier avec titre et contenu',
+    },
 };
-- 
GitLab


From 27fe972fa09416239bbf89dafdcc16cfdec9866f Mon Sep 17 00:00:00 2001
From: VIAUD Nathan <nathan.viaud@inria.fr>
Date: Tue, 18 Mar 2025 16:35:54 +0100
Subject: [PATCH 05/18] WIP translate (components)

---
 src/features/forms/FormPanel.vue              |  7 +++-
 src/features/settings/SettingsModal.vue       | 30 +++++++--------
 src/features/sideBar/components/ModelMenu.vue |  4 +-
 .../sideBar/components/PageTemplate.vue       |  7 +++-
 src/features/topBar/HamburgerMenu.vue         | 14 +++----
 src/features/topBar/TopActionDropdown.vue     |  2 +-
 src/features/topBar/TopActionsMenu.vue        | 12 +++---
 src/features/topBar/TopBar.vue                | 37 ++++++++++++++-----
 src/i18n/fr.ts                                | 25 +++++++++++++
 9 files changed, 93 insertions(+), 45 deletions(-)

diff --git a/src/features/forms/FormPanel.vue b/src/features/forms/FormPanel.vue
index 84b2a99b..f962328f 100644
--- a/src/features/forms/FormPanel.vue
+++ b/src/features/forms/FormPanel.vue
@@ -14,6 +14,9 @@ import {
     isFormButtonDisabled,
 } from '@/src/shared/services/graph';
 import LinkedBadges from '@/src/features/badge/components/LinkedBadges.vue';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
 
 const editorStore = useEditorStore();
 
@@ -57,8 +60,8 @@ function actionOnForm(action: string) {
 
         case 'save-model':
             if (editorStore.savePageModel(currentNode.data.elements.map((element: NodeElement) => element.action))) {
-                toaster.success('Modèle sauvegardé 👌');
-            } else toaster.error('Le modèle existe déjà 🤔');
+                toaster.success(t('toast.modelSaved'));
+            } else toaster.error(t('toast.modelExists'));
             break;
 
         case 'simple-question':
diff --git a/src/features/settings/SettingsModal.vue b/src/features/settings/SettingsModal.vue
index 528a33a8..1ddf1f1c 100644
--- a/src/features/settings/SettingsModal.vue
+++ b/src/features/settings/SettingsModal.vue
@@ -1,8 +1,6 @@
 <script setup lang="ts">
-import Modal from '@/src/components/LayoutModal.vue'
+import Modal from '@/src/components/LayoutModal.vue';
 import SettingsInput from './SettingsInput.vue';
-import ContentButton from '@/src/components/ContentButton.vue';
-import TopActionButton from '@/src/features/topBar/TopActionButton.vue';
 import { ref, onMounted, watch } from 'vue';
 import { useSettingsStore } from '@/src/shared/stores';
 
@@ -12,7 +10,7 @@ const spellcheck = ref(false);
 
 onMounted(() => {
     settingsStore.init();
-})
+});
 
 function save() {
     settingsStore.setSettings(spellcheck.value);
@@ -23,29 +21,31 @@ function open() {
     modal.value.open();
 }
 
-watch(() => modal.value?.isOpen, (isOpen) => {
-    if (isOpen) spellcheck.value = settingsStore?.settings?.spellcheck;
-})
+watch(
+    () => modal.value?.isOpen,
+    (isOpen) => {
+        if (isOpen) spellcheck.value = settingsStore?.settings?.spellcheck;
+    },
+);
 
 defineExpose({
-    open
-})
-
+    open,
+});
 </script>
 
 <template>
-    <Modal ref="modal" title="Paramètres">
+    <Modal ref="modal" :title="$t('settings.title')">
         <template v-if="modal" #trigger>
             <slot name="trigger" />
         </template>
 
         <div class="settings">
-            <SettingsInput v-model="spellcheck" type="toggle" label="Activer la vérification orthographique" />
+            <SettingsInput v-model="spellcheck" type="toggle" :label="$t('settings.spellcheck')" />
         </div>
 
         <template #footer>
-            <button class="btn-choice cancel" @click="modal.close">Annuler</button>
-            <button class="btn-choice save" @click="save">Valider</button>
+            <button class="btn-choice cancel" @click="modal.close">{{ $t('global.cancel') }}</button>
+            <button class="btn-choice save" @click="save">{{ $t('global.validate') }}</button>
         </template>
     </Modal>
 </template>
@@ -54,7 +54,7 @@ defineExpose({
 .settings {
     display: flex;
     flex-direction: column;
-    gap: .5rem;
+    gap: 0.5rem;
 }
 
 .btn-choice {
diff --git a/src/features/sideBar/components/ModelMenu.vue b/src/features/sideBar/components/ModelMenu.vue
index eccdcaf9..77113d9b 100644
--- a/src/features/sideBar/components/ModelMenu.vue
+++ b/src/features/sideBar/components/ModelMenu.vue
@@ -7,7 +7,7 @@ const editorStore = useEditorStore();
 </script>
 
 <template>
-    <SideMenu title="Modèles de page" @close="editorStore.modelMenu = false">
+    <SideMenu :title="$t('models.title')" @close="editorStore.modelMenu = false">
         <div v-if="editorStore.pageModels.length > 0" class="models">
             <PageTemplate
                 v-for="(model, index) in editorStore.pageModels"
@@ -17,7 +17,7 @@ const editorStore = useEditorStore();
             />
         </div>
         <div v-else>
-            <h4 class="empty">Aucun modèle de page n'as été créé</h4>
+            <h4 class="empty">{{ $t('models.empty') }}</h4>
         </div>
     </SideMenu>
 </template>
diff --git a/src/features/sideBar/components/PageTemplate.vue b/src/features/sideBar/components/PageTemplate.vue
index ff7b968b..4216d9b9 100644
--- a/src/features/sideBar/components/PageTemplate.vue
+++ b/src/features/sideBar/components/PageTemplate.vue
@@ -3,6 +3,9 @@ import { SideAction } from '@/src/shared/interfaces';
 import ContentButton from '@/src/components/ContentButton.vue';
 import { ref } from 'vue';
 import { useEditorStore } from '@/src/shared/stores';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
 
 defineProps<{
     elements: SideAction[];
@@ -11,7 +14,7 @@ defineProps<{
 
 const editorStore = useEditorStore();
 
-const templateTooltip = 'Glisser/déposer pour ajouter un modèle';
+const templateTooltip = t('models.dragdrop');
 
 const dragging = ref(false);
 
@@ -26,7 +29,7 @@ function dragStart(event: DragEvent, elements) {
 
 <template>
     <div class="container">
-        <p class="page-title">{{ name ? name : 'Modèle' }}</p>
+        <p class="page-title">{{ name ? name : t('models.model') }}</p>
         <!--suppress VueUnrecognizedDirective -->
         <div
             v-tippy="{
diff --git a/src/features/topBar/HamburgerMenu.vue b/src/features/topBar/HamburgerMenu.vue
index d4701bc9..0a2f0e1a 100644
--- a/src/features/topBar/HamburgerMenu.vue
+++ b/src/features/topBar/HamburgerMenu.vue
@@ -58,11 +58,11 @@ const settingsModal = ref(null);
             @click="emit('undo')"
         >
             <i class="icon-arriere"></i>
-            <span>Undo</span>
+            <span>{{ $t('header.undo') }}</span>
         </button>
         <button
             v-tippy="{
-                content: `${modifier} + ${editorStore.platform === 'darwin' ? '⇧ + z' : 'y' }`,
+                content: `${modifier} + ${editorStore.platform === 'darwin' ? '⇧ + z' : 'y'}`,
                 placement: 'left',
                 arrow: true,
                 arrowType: 'round',
@@ -73,7 +73,7 @@ const settingsModal = ref(null);
             @click="emit('redo')"
         >
             <i class="icon-avant"></i>
-            <span>Redo</span>
+            <span>{{ $t('header.redo') }}</span>
         </button>
         <button
             v-tippy="{
@@ -88,22 +88,22 @@ const settingsModal = ref(null);
             @click="emit('save')"
         >
             <i class="icon-save"></i>
-            <span>Sauvegarder</span>
+            <span>{{ $t('global.save') }}</span>
         </button>
         <button class="menu-item" :disabled="loadingPreview" @click="emit('runPreview')">
             <i class="icon-play"></i>
-            <span>Aperçu</span>
+            <span>{{ $t('header.preview') }}</span>
         </button>
         <button class="menu-item" :disabled="exporting" @click="emit('exportProject')">
             <i class="icon-export"></i>
-            <span>Publier</span>
+            <span>{{ $t('header.publish') }}</span>
         </button>
 
         <SettingsModal ref="settingsModal">
             <template #trigger>
                 <button class="menu-item" @click="settingsModal.open">
                     <i class="icon-settings"></i>
-                    <span>Paramètre</span>
+                    <span>{{ $t('settings.title') }}</span>
                 </button>
             </template>
         </SettingsModal>
diff --git a/src/features/topBar/TopActionDropdown.vue b/src/features/topBar/TopActionDropdown.vue
index 061b9e2a..075e6c27 100644
--- a/src/features/topBar/TopActionDropdown.vue
+++ b/src/features/topBar/TopActionDropdown.vue
@@ -24,7 +24,7 @@ const emit = defineEmits<{
         <i :class="icon" />
         <span v-if="text" class="text-top-bar">{{ text }}</span>
         <select id="select-box" :value="inputValue" :disabled="disabled" class="select-box" @change="onSelect">
-            <option value="0">Ajuster</option>
+            <option value="0">{{ $t('header.adjust') }}</option>
             <option value="0.5">50%</option>
             <option value="0.75">75%</option>
             <option value="1">100%</option>
diff --git a/src/features/topBar/TopActionsMenu.vue b/src/features/topBar/TopActionsMenu.vue
index 0b699578..c9a69f79 100644
--- a/src/features/topBar/TopActionsMenu.vue
+++ b/src/features/topBar/TopActionsMenu.vue
@@ -28,7 +28,7 @@ const emit = defineEmits<{
 }>();
 
 // detect the platform
-const modifier = computed(() => editorStore.platform === 'darwin' ? '⌘' : 'Ctrl');
+const modifier = computed(() => (editorStore.platform === 'darwin' ? '⌘' : 'Ctrl'));
 
 const settingsModal = ref(null);
 </script>
@@ -59,7 +59,7 @@ const settingsModal = ref(null);
             />
             <TopActionButton
                 v-tippy="{
-                    content: `${modifier} + ${editorStore.platform === 'darwin' ? '⇧ + z' : 'y' }`,
+                    content: `${modifier} + ${editorStore.platform === 'darwin' ? '⇧ + z' : 'y'}`,
                     placement: 'bottom',
                     arrow: true,
                     arrowType: 'round',
@@ -81,21 +81,21 @@ const settingsModal = ref(null);
                     animation: 'fade',
                 }"
                 icon="icon-save"
-                text="Sauvegarder"
+                :text="$t('global.save')"
                 position="right"
                 :disabled="saving"
                 @click="emit('save')"
             />
             <TopActionButton
                 icon="icon-play"
-                text="Aperçu"
+                :text="$t('header.preview')"
                 position="right"
                 :disabled="loadingPreview"
                 @click="emit('runPreview')"
             />
             <TopActionButton
                 icon="icon-export"
-                text="Publier"
+                :text="$t('header.publish')"
                 position="right"
                 :disabled="exporting"
                 @click="emit('exportProject')"
@@ -105,7 +105,7 @@ const settingsModal = ref(null);
                 <template #trigger>
                     <TopActionButton
                         icon="icon-settings"
-                        text="Paramètres"
+                        :text="$t('settings.title')"
                         position="right"
                         @click="settingsModal.open"
                     />
diff --git a/src/features/topBar/TopBar.vue b/src/features/topBar/TopBar.vue
index 0e45eb53..cb18614f 100644
--- a/src/features/topBar/TopBar.vue
+++ b/src/features/topBar/TopBar.vue
@@ -4,6 +4,9 @@ import { computed, ref } from 'vue';
 import { editorService } from '@/src/shared/services';
 import { useVueFlow } from '@vue-flow/core';
 import TopActionsMenu from '@/src/features/topBar/TopActionsMenu.vue';
+import { useI18n } from 'vue-i18n';
+
+const { t, locale } = useI18n();
 
 const editorStore = useEditorStore();
 const undoRedoStore = useUndoRedoStore();
@@ -17,7 +20,7 @@ editorStore.$subscribe(() => {
 const savedSince = ref(since(editorStore.currentProject.modified));
 
 const zoom = ref(1);
-const zoomString = computed(() => (zoom.value === 0 ? 'Ajuster' : `${Math.round(zoom.value * 100)}%`));
+const zoomString = computed(() => (zoom.value === 0 ? t('header.adjust') : `${Math.round(zoom.value * 100)}%`));
 
 zoomTo(zoom.value);
 
@@ -35,17 +38,29 @@ function updateZoom(val: number) {
 }
 
 function since(date: string) {
-    if (!date) return 'jamais';
+    if (!date) return t('header.never');
     const milliseconds = Math.abs(Date.now() - new Date(date).getTime());
     const secs = Math.floor(Math.abs(milliseconds) / 1000);
     const mins = Math.floor(secs / 60);
     const hours = Math.floor(mins / 60);
     const days = Math.floor(hours / 24);
 
-    return (
-        'Il y a ' +
-        (days > 1 ? `${days} jours` : hours > 1 ? `${hours} heures` : mins > 1 ? `${mins} mins` : "moins d'une minute")
-    );
+    if (locale.value === 'fr') {
+        return (
+            'Il y a ' +
+            (days > 1 ? `${days} jours`
+            : hours > 1 ? `${hours} heures`
+            : mins > 1 ? `${mins} mins`
+            : "moins d'une minute")
+        );
+    } else {
+        return (
+            (days > 1 ? `${days} days`
+            : hours > 1 ? `${hours} hours`
+            : mins > 1 ? `${mins} mins`
+            : 'less than a minute') + ' ago'
+        );
+    }
 }
 
 setInterval(() => {
@@ -56,16 +71,18 @@ function separateFilePath(filepath: string) {
     const file = filepath.split('/').pop();
     return [filepath.replace('/' + file, ''), '/' + file];
 }
-
 </script>
 
 <template>
     <div class="top-bar">
         <div class="top-bar-content">
             <div class="top-bar-title">
-                <h3 v-if="!editorStore.currentProject.filepath">Nouvel ePoc</h3>
-                <h3 v-else><span>{{ separateFilePath(editorStore.currentProject.filepath)[0] }}</span>{{ separateFilePath(editorStore.currentProject.filepath)[1] }}</h3>
-                <small>Dernière sauvegarde : {{ savedSince }}</small>
+                <h3 v-if="!editorStore.currentProject.filepath">{{ t('header.new') }}</h3>
+                <h3 v-else>
+                    <span>{{ separateFilePath(editorStore.currentProject.filepath)[0] }}</span>
+                    {{ separateFilePath(editorStore.currentProject.filepath)[1] }}
+                </h3>
+                <small>{{ t('header.lastSave') }} {{ savedSince }}</small>
             </div>
             <TopActionsMenu
                 :undo-disabled="undoRedoStore.undoStack.length <= 0"
diff --git a/src/i18n/fr.ts b/src/i18n/fr.ts
index 1f8440f0..acdd8bfe 100644
--- a/src/i18n/fr.ts
+++ b/src/i18n/fr.ts
@@ -13,6 +13,7 @@ export const frMessages = {
         right: 'droite',
         left: 'gauche',
         add: 'Ajouter',
+        validate: 'Valider',
     },
     validation: {
         yes: 'OUI, SUPPRIMER',
@@ -47,4 +48,28 @@ export const frMessages = {
         accordion: 'Plier/déplier',
         accordionDescription: 'Plier/déplier avec titre et contenu',
     },
+    toast: {
+        modelSaved: 'Modèle sauvegardé 👌',
+        modelExists: 'Le modèle existe déjà 🤔',
+    },
+    settings: {
+        title: 'Paramètres',
+        spellcheck: 'Activer la vérification orthographique',
+    },
+    models: {
+        title: 'Modèles de page',
+        empty: "Aucun modèle de page n'as été créé",
+        dragdrop: 'Glisser/déposer pour ajouter un modèle',
+        model: 'Modèle',
+    },
+    header: {
+        undo: 'Annuler',
+        redo: 'Rétablir',
+        preview: 'Aperçu',
+        publish: 'Publier',
+        adjust: 'Ajuster',
+        never: 'jamais',
+        lastSave: 'Dernière sauvegarde :',
+        new: 'Nouvel ePoc',
+    },
 };
-- 
GitLab


From 9db85660c1efd5b27395c9bfaebbfcd2ae47cc07 Mon Sep 17 00:00:00 2001
From: VIAUD Nathan <nathan.viaud@inria.fr>
Date: Wed, 19 Mar 2025 10:05:24 +0100
Subject: [PATCH 06/18] WIP translate(forms)

---
 src/i18n/config.ts                        |  10 ++
 src/i18n/fr.ts                            | 114 ++++++++++++++
 src/main.ts                               |  19 +--
 src/shared/data/forms/badgeForm.data.ts   |  13 +-
 src/shared/data/forms/contentForm.data.ts |  37 +++--
 src/shared/data/forms/formButtons.data.ts |  32 ++--
 src/shared/data/forms/nodeForm.data.ts    | 181 +++++++++++-----------
 7 files changed, 273 insertions(+), 133 deletions(-)
 create mode 100644 src/i18n/config.ts

diff --git a/src/i18n/config.ts b/src/i18n/config.ts
new file mode 100644
index 00000000..22660577
--- /dev/null
+++ b/src/i18n/config.ts
@@ -0,0 +1,10 @@
+import { createI18n } from 'vue-i18n';
+import { frMessages } from './fr';
+
+export const i18n = createI18n({
+    locale: 'fr',
+    fallbackLocale: 'en',
+    messages: {
+        fr: frMessages,
+    },
+});
diff --git a/src/i18n/fr.ts b/src/i18n/fr.ts
index acdd8bfe..43f5cd0f 100644
--- a/src/i18n/fr.ts
+++ b/src/i18n/fr.ts
@@ -14,6 +14,12 @@ export const frMessages = {
         left: 'gauche',
         add: 'Ajouter',
         validate: 'Valider',
+        delete: 'Supprimer',
+        conditions: 'Conditions',
+        label: 'Label',
+        chapter: 'Chapitre',
+        objective: 'Objectif',
+        name: 'Nom',
     },
     validation: {
         yes: 'OUI, SUPPRIMER',
@@ -72,4 +78,112 @@ export const frMessages = {
         lastSave: 'Dernière sauvegarde :',
         new: 'Nouvel ePoc',
     },
+
+    forms: {
+        type: 'Saisissez...',
+        badge: {
+            updateIcon: "Modifier l'icône",
+            obtention: "Conditions d'obtention du badge",
+            presentation: 'Présentation du badge',
+            presentationPlaceholder: 'Saisissez une présentation du badge',
+        },
+        content: {
+            text: 'Contenu',
+            summary: 'Résumé',
+            video: {
+                label: 'Vidéo',
+                placeholder: 'Ajouter une vidéo',
+                hint: 'Format recommandé :',
+            },
+            transcription: {
+                label: 'Transcription',
+                placeholder: 'Ajouter une transcription',
+                hint: "Extensions acceptées : {extensions} <br>Pour les utilisateurs qui ne souhaitent pas ou ne sont pas en capacité d'écouter la vidéo",
+            },
+            thumbnail: {
+                label: 'Vignette',
+                placeholder: 'Ajouter une vignette',
+                hint: 'Format recommandé : idem à la vidéo',
+            },
+        },
+        buttons: {
+            addBadge: 'Ajouter un badge',
+            saveModel: 'Sauvegarder le modèle',
+            duplicateEvaluation: "Dupliquer l'évaluation",
+            duplicatePage: 'Dupliquer la page',
+            duplicateElement: "Dupliquer l'élement",
+            backToPage: 'Revenir à la page',
+            backToEpoc: "Revenir à l'ePoc",
+        },
+        node: {
+            conditionPlaceholder: 'Saisissez la condition {condition}...',
+            choice: 'Choix',
+            course: 'Cours {course}',
+            conditional: 'Contenus conditionnels',
+            title: 'Titre',
+            subtitle: 'Sous-titre',
+            duration: 'Durée (en minutes)',
+            objectives: 'Objectifs pédagogiques',
+            about: "À propos de l'ePoc",
+            cover: {
+                title: 'Image de couverture',
+                placeholder: 'Ajouter une image de couverture',
+                hint: 'Format recommandé : carré (180x180)<br> Image visible dans la liste des ePocs',
+            },
+            teaser: {
+                title: 'Teaser vidéo',
+                placeholder: 'Ajouter un teaser',
+                hint: "Format recommandé : 16:9<br> Vidéo visible dans la page de présentation de l'ePoc",
+            },
+            thumbnail: {
+                title: 'Vignette de la vidéo',
+                hint: "Format recommandé : idem que la vidéo <br> Image visible dans la page de présentation de l'ePoc",
+            },
+            presentation: 'Présentation',
+            edition: 'Édition',
+            author: {
+                title: 'Auteur | Auteurs',
+                placeholder: 'Jeanne Dupont',
+                image: {
+                    title: "Image de l'auteur",
+                    placeholder: 'Ajouter une image',
+                    hint: "Format recommandé : carré (100x100)<br> Image visible dans la page de présentation de l'ePoc",
+                },
+                position: {
+                    title: 'Fonction',
+                    placeholder: "Chercheur à l'Inria",
+                    hint: 'Profession, fonction, affiliation…',
+                },
+                biography: "Courte biographie de l'auteur",
+            },
+            certificateBadge: "Nombre de badge pour obtenir l'attestation",
+            certificateScore: "Score pour obtenir l'attestation",
+            certificateScoreHint:
+                "N'est pas pris en compte si le nombre de badge pour obtenir l'attestation est supérieur à 0",
+            chapterLabel: 'Label des chapitres',
+            chapterDuration: 'Durée des chapitres (en minutes)',
+            plugin: {
+                title: 'Plugin | Plugins',
+                script: 'Fichier de script',
+                scriptPlaceholder: 'Ajouter un fichier de script',
+                template: 'Template html du plugin',
+                templatePlaceholder: 'Ajouter un template html',
+            },
+            licence: {
+                title: 'Licence',
+                hint: 'Nom de la licence de votre contenu ePoc',
+                url: 'URL',
+                urlPlaceholder: 'https://creativecommons.org/licenses/by/4.0/deed',
+                urlHint: 'Text complet de la licence choisie',
+            },
+            page: {
+                title: 'Page',
+                hidden: 'Caché dans la table des matières',
+                conditional: "Ne s'affiche qu'à certaines conditions",
+                conditionalHint: "Option utilisée pour l'affichage conditionnel",
+                components: 'Composants',
+            },
+            activity: 'Évaluation',
+        },
+    },
 };
diff --git a/src/main.ts b/src/main.ts
index 11d66f3b..e4170c16 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -6,16 +6,17 @@ import { createPinia } from 'pinia';
 import VueTippy from 'vue-tippy';
 import 'tippy.js/dist/tippy.css';
 import draggable from 'vuedraggable';
-import { createI18n } from 'vue-i18n';
-import { frMessages } from './i18n/fr';
+import { i18n } from './i18n/config';
+// import { createI18n } from 'vue-i18n';
+//
 
-const i18n = createI18n({
-    locale: 'fr',
-    fallbackLocale: 'fr',
-    messages: {
-        fr: frMessages,
-    },
-});
+// const i18n = createI18n({
+//     locale: 'fr',
+//     fallbackLocale: 'fr',
+//     messages: {
+//         fr: frMessages,
+//     },
+// });
 
 const app = createApp(App);
 app.use(router);
diff --git a/src/shared/data/forms/badgeForm.data.ts b/src/shared/data/forms/badgeForm.data.ts
index cd2ff159..b6edd7ac 100644
--- a/src/shared/data/forms/badgeForm.data.ts
+++ b/src/shared/data/forms/badgeForm.data.ts
@@ -1,5 +1,8 @@
 import { Form } from '@/src/shared/interfaces';
 import { badgeButtons } from './formButtons.data';
+import { i18n } from '@/src/i18n/config';
+
+const { t } = i18n.global;
 
 export const customBadgeForm: Form = {
     type: 'badge',
@@ -14,19 +17,19 @@ export const customBadgeForm: Form = {
                     type: 'text',
                     label: 'Titre',
                     value: '',
-                    placeholder: 'Saisissez...',
+                    placeholder: t('forms.type'),
                 },
                 {
                     id: 'icon',
                     type: 'icon-picker',
                     label: 'Icône du badge',
                     value: '',
-                    placeholder: "Modifier l'icône",
+                    placeholder: t('forms.badge.updateIcon'),
                 },
             ],
         },
         {
-            name: "Conditions d'obtention du badge",
+            name: t('forms.badge.obtention'),
             inputs: [
                 {
                     id: 'conditions',
@@ -37,14 +40,14 @@ export const customBadgeForm: Form = {
             ],
         },
         {
-            name: 'Présentation du badge',
+            name: t('forms.badge.presentation'),
             inputs: [
                 {
                     id: 'description',
                     type: 'textarea',
                     label: '',
                     value: '',
-                    placeholder: 'Saisissez une présentation du badge',
+                    placeholder: t('forms.badge.presentationPlaceholder'),
                 },
             ],
         },
diff --git a/src/shared/data/forms/contentForm.data.ts b/src/shared/data/forms/contentForm.data.ts
index 20a0f2cd..afd08571 100644
--- a/src/shared/data/forms/contentForm.data.ts
+++ b/src/shared/data/forms/contentForm.data.ts
@@ -1,9 +1,12 @@
 import { Form } from '@/src/shared/interfaces';
 import { contentButtons } from './formButtons.data';
+import { i18n } from '@/src/i18n/config';
+
+const { t } = i18n.global;
 
 export const textForm: Form = {
     type: 'text',
-    name: 'Contenu',
+    name: t('forms.content.text'),
     icon: 'icon-texte',
     buttons: contentButtons,
     fields: [
@@ -14,7 +17,7 @@ export const textForm: Form = {
                     type: 'html',
                     label: '',
                     value: '',
-                    placeholder: 'Saisissez un résumé...',
+                    placeholder: t('type'),
                 },
             ],
         },
@@ -23,7 +26,7 @@ export const textForm: Form = {
 
 export const videoForm: Form = {
     type: 'video',
-    name: 'Vidéo',
+    name: t('forms.content.video.label'),
     icon: 'icon-video',
     buttons: contentButtons,
     fields: [
@@ -32,36 +35,36 @@ export const videoForm: Form = {
                 {
                     id: 'source',
                     type: 'file',
-                    label: 'Vidéo',
-                    placeholder: 'Ajouter une vidéo',
+                    label: t('forms.content.video.label'),
+                    placeholder: t('forms.content.video.placeholder'),
                     value: '',
                     accept: '.mp4',
-                    hint: 'Format recommandé: 16:9 (720x480)'
+                    hint: t('forms.content.video.hint'),
                 },
                 {
                     id: 'summary',
                     type: 'html',
-                    label: 'Résumé',
+                    label: t('forms.content.summary'),
                     value: '',
-                    placeholder: 'Saisissez...',
+                    placeholder: t('forms.type'),
                 },
                 {
                     id: 'transcript',
                     type: 'file',
-                    label: 'Transcription',
+                    label: t('forms.content.transcription.label'),
                     value: '',
-                    placeholder: 'Ajouter une transcription',
+                    placeholder: t('forms.content.transcription.placeholder'),
                     accept: '.txt,.vtt',
-                    hint: 'Extensions acceptées : .vtt, .txt <br>Pour les utilisateurs qui ne souhaitent pas ou ne sont pas en capacité d\'écouter la vidéo'
+                    hint: t('forms.content.transcription.hint', { extensions: '.txt,.vtt' }),
                 },
                 {
                     id: 'poster',
                     type: 'file',
-                    label: 'Vignette',
+                    label: t('forms.content.thumbnail.label'),
                     value: '',
-                    placeholder: 'Ajouter une vignette',
+                    placeholder: t('forms.content.thumbnail.placeholder'),
                     accept: '.png,.jpg,.jpeg,.gif,.bmp,.svg,.webp',
-                    hint: 'Format recommandé: idem à la vidéo'
+                    hint: t('forms.content.thumbnail.hint'),
                 },
             ],
         },
@@ -95,7 +98,7 @@ export const videoForm: Form = {
                             value: '',
                             placeholder: 'Ajouter des sous-titres',
                             accept: '.vtt',
-                            hint: 'Extensions acceptées : .vtt'
+                            hint: 'Extensions acceptées : .vtt',
                         },
                     ],
                 },
@@ -134,7 +137,7 @@ export const audioForm: Form = {
                     value: '',
                     placeholder: 'Ajouter une transcription',
                     accept: '.txt,.vtt',
-                    hint: 'Extensions acceptées : .vtt, .txt <br>Pour les utilisateurs qui ne souhaitent pas ou ne sont pas en capacité d\'écouter la piste audio'
+                    hint: "Extensions acceptées : .vtt, .txt <br>Pour les utilisateurs qui ne souhaitent pas ou ne sont pas en capacité d'écouter la piste audio",
                 },
                 {
                     id: 'subtitles',
@@ -143,7 +146,7 @@ export const audioForm: Form = {
                     value: '',
                     placeholder: 'Ajouter des sous-titres',
                     accept: '.vtt',
-                    hint: 'Extensions acceptées: .vtt'
+                    hint: 'Extensions acceptées: .vtt',
                 },
             ],
         },
diff --git a/src/shared/data/forms/formButtons.data.ts b/src/shared/data/forms/formButtons.data.ts
index 2787e704..3c16564b 100644
--- a/src/shared/data/forms/formButtons.data.ts
+++ b/src/shared/data/forms/formButtons.data.ts
@@ -1,39 +1,45 @@
 import { FormButton } from '@/src/shared/interfaces';
 import env from '@/src/shared/utils/env';
+import { i18n } from '@/src/i18n/config';
+
+const { t } = i18n.global;
 
 export const baseButtons = [
-    { label: 'Supprimer', icon: 'icon-supprimer', action: 'delete' },
-    { label: 'Ajouter un badge', icon: 'icon-plus', action: 'add-badge' },
+    { label: t('global.delete'), icon: 'icon-supprimer', action: 'delete' },
+    { label: t('forms.buttons.addBadge'), icon: 'icon-plus', action: 'add-badge' },
 ];
 
 export const pageButtons: FormButton[] =
     env.isDev ?
         [
             ...baseButtons,
-            { label: 'Dupliquer la page', icon: 'icon-plus', action: 'duplicate-page' },
-            { label: 'Sauvegarder le modèle', icon: 'icon-modele', action: 'save-model' },
+            { label: t('forms.buttons.duplicatePage'), icon: 'icon-plus', action: 'duplicate-page' },
+            { label: t('forms.buttons.saveModel'), icon: 'icon-modele', action: 'save-model' },
         ]
-        :   [...baseButtons, { label: 'Dupliquer la page', icon: 'icon-plus', action: 'duplicate-page' }];
+    :   [...baseButtons, { label: t('forms.buttons.duplicatePage'), icon: 'icon-plus', action: 'duplicate-page' }];
 
 export const activityButtons: FormButton[] =
     env.isDev ?
         [
             ...baseButtons,
-            { label: "Dupliquer l'évaluation", icon: 'icon-plus', action: 'duplicate-page' },
-            { label: 'Sauvegarder le modèle', icon: 'icon-modele', action: 'save-model' },
+            { label: t('forms.buttons.duplicateEvaluation'), icon: 'icon-plus', action: 'duplicate-page' },
+            { label: t('forms.buttons.saveModel'), icon: 'icon-modele', action: 'save-model' },
         ]
-        :   [...baseButtons, { label: "Dupliquer l'évaluation", icon: 'icon-plus', action: 'duplicate-page' }];
+    :   [
+            ...baseButtons,
+            { label: t('forms.buttons.duplicateEvaluation'), icon: 'icon-plus', action: 'duplicate-page' },
+        ];
 
 export const contentButtons: FormButton[] =
     env.isDev ?
         [
             ...baseButtons,
-            { label: 'Revenir à la page', icon: 'icon-ecran', action: 'back-to-page' },
-            { label: "Dupliquer l'élément", icon: 'icon-plus', action: 'duplicate-element' },
+            { label: t('forms.buttons.backToPage'), icon: 'icon-ecran', action: 'back-to-page' },
+            { label: t('forms.button.duplicateElement'), icon: 'icon-plus', action: 'duplicate-element' },
         ]
-        :   [...baseButtons];
+    :   [...baseButtons];
 
 export const badgeButtons: FormButton[] = [
-    { label: 'Supprimer', icon: 'icon-supprimer', action: 'delete-badge' },
-    { label: "Revenir à l'ePoc", icon: 'icon-epoc', action: 'back-to-epoc' },
+    { label: t('global.delete'), icon: 'icon-supprimer', action: 'delete-badge' },
+    { label: t('forms.buttons.backToEpoc'), icon: 'icon-epoc', action: 'back-to-epoc' },
 ];
diff --git a/src/shared/data/forms/nodeForm.data.ts b/src/shared/data/forms/nodeForm.data.ts
index b4e4d4ba..ed1171cb 100644
--- a/src/shared/data/forms/nodeForm.data.ts
+++ b/src/shared/data/forms/nodeForm.data.ts
@@ -1,9 +1,12 @@
 import { Form } from '@/src/shared/interfaces';
 import { activityButtons, baseButtons, pageButtons } from './formButtons.data';
+import { i18n } from '@/src/i18n/config';
+
+const { t } = i18n.global;
 
 export const conditionForm: Form = {
     type: 'condition',
-    name: 'Conditions',
+    name: t('global.conditions'),
     icon: 'icon-condition',
     buttons: baseButtons,
     fields: [
@@ -14,14 +17,14 @@ export const conditionForm: Form = {
                     type: 'text',
                     label: '',
                     value: '',
-                    placeholder: 'Saisissez la condition 1...',
+                    placeholder: t('forms.node.conditionPlaceholder', { condition: '1' }),
                 },
                 {
                     id: 'condition2',
                     type: 'text',
                     label: '',
                     value: '',
-                    placeholder: 'Saisissez la condition 2...',
+                    placeholder: t('forms.node.conditionPlaceholder', { condition: '2' }),
                 },
             ],
         },
@@ -39,9 +42,9 @@ export const legacyConditionForm: Form = {
                 {
                     id: 'label',
                     type: 'text',
-                    label: 'Label',
+                    label: t('global.label'),
                     value: '',
-                    placeholder: 'Saisissez...',
+                    placeholder: t('forms.type'),
                 },
             ],
         },
@@ -50,15 +53,15 @@ export const legacyConditionForm: Form = {
             inputs: [
                 {
                     id: 'choices',
-                    label: 'Choix',
+                    label: t('forms.node.choice'),
                     type: 'repeat',
-                    value: ['Parcours A', 'Parcours B'],
+                    value: [t('forms.node.course', { course: 'A' }), t('forms.node.course', { course: 'B' })],
                     inputs: [
                         {
                             id: '',
                             type: 'text',
                             label: '',
-                            placeholder: 'Parcours X',
+                            placeholder: t('forms.node.course', { course: 'X' }),
                             value: '',
                         },
                     ],
@@ -66,11 +69,11 @@ export const legacyConditionForm: Form = {
             ],
         },
         {
-            name: 'Contenus conditionnels',
+            name: t('forms.node.conditional'),
             inputs: [
                 {
                     id: 'conditionalFlag',
-                    label: 'Contenu',
+                    label: t('forms.content.text'),
                     type: 'repeat',
                     value: [],
                     inputs: [
@@ -78,7 +81,7 @@ export const legacyConditionForm: Form = {
                             id: 'id',
                             type: 'text',
                             label: '',
-                            placeholder: 'Contenu',
+                            placeholder: t('forms.type'),
                             value: '',
                         },
                         {
@@ -99,7 +102,7 @@ export const legacyConditionForm: Form = {
 
 export const chapterForm: Form = {
     type: 'chapter',
-    name: 'Chapitre',
+    name: t('global.chapter'),
     icon: 'icon-chapitre',
     buttons: baseButtons,
     fields: [
@@ -108,24 +111,24 @@ export const chapterForm: Form = {
                 {
                     id: 'title',
                     type: 'text',
-                    label: 'Titre',
+                    label: t('forms.node.title'),
                     value: '',
-                    placeholder: 'Saisissez...',
+                    placeholder: t('forms.type'),
                 },
                 {
                     id: 'duration',
                     type: 'score',
-                    label: 'Durée (en minutes)',
+                    label: t('forms.node.duration'),
                     value: 0,
                 },
             ],
         },
         {
-            name: 'Objectifs pédagogiques',
+            name: t('forms.node.objectives'),
             inputs: [
                 {
                     id: 'objectives',
-                    label: 'Objectif',
+                    label: t('global.objective'),
                     type: 'repeat',
                     value: [],
                     inputs: [
@@ -133,7 +136,7 @@ export const chapterForm: Form = {
                             id: '',
                             type: 'textarea',
                             label: '',
-                            placeholder: 'Saisissez un objectif ...',
+                            placeholder: t('forms.type'),
                             value: '',
                         },
                     ],
@@ -145,7 +148,7 @@ export const chapterForm: Form = {
 
 export const epocForm: Form = {
     type: 'epoc',
-    name: "A propos de l'ePoc",
+    name: t('forms.node.about'),
     icon: 'icon-epoc',
     buttons: [],
     fields: [
@@ -154,90 +157,90 @@ export const epocForm: Form = {
                 {
                     id: 'title',
                     type: 'text',
-                    label: 'Titre',
+                    label: t('forms.node.title'),
                     value: '',
-                    placeholder: 'Saisissez...',
+                    placeholder: t('forms.type'),
                 },
                 {
                     id: 'image',
                     type: 'file',
-                    label: 'Image de couverture',
-                    placeholder: 'Ajouter une image de couverture',
+                    label: t('forms.node.cover.title'),
+                    placeholder: t('forms.node.cover.placeholder'),
                     value: '',
                     accept: '.png,.jpg,.jpeg,.gif,.bmp,.svg,.webp',
-                    hint: 'Format recommandé : carré (180x180)<br> Image visible dans la liste des ePocs',
+                    hint: t('forms.node.cover.hint'),
                 },
                 {
                     id: 'teaser',
                     type: 'file',
-                    label: 'Teaser vidéo',
+                    label: t('forms.node.teaser.title'),
                     value: '',
-                    placeholder: 'Ajouter un teaser',
+                    placeholder: t('forms.node.teaser.placeholder'),
                     accept: '.mp4',
-                    hint: "Format recommandé : 16:9 (720x480) <br> Vidéo visible dans la page de présentation de l'ePoc",
+                    hint: t('forms.node.teaser.hint'),
                 },
                 {
                     id: 'thumbnail',
                     type: 'file',
-                    label: 'Vignette de la vidéo',
+                    label: t('forms.node.thumbnail.title'),
                     value: '',
-                    placeholder: 'Ajouter une vignette',
+                    placeholder: t('content.thumbnail.placeholder'),
                     accept: '.png,.jpg,.jpeg,.gif,.bmp,.svg,.webp',
-                    hint: "Format recommandé : idem que la vidéo <br> Image visible dans la page de présentation de l'ePoc",
+                    hint: t('forms.node.thumbnail.hint'),
                 },
                 {
                     id: 'summary',
                     type: 'html-text',
-                    label: 'Présentation',
+                    label: t('forms.node.presentation'),
                     value: '',
-                    placeholder: "Saisissez une présentation de l'ePoc...",
+                    placeholder: t('forms.type'),
                 },
                 {
                     id: 'edition',
                     type: 'text',
-                    label: 'Edition',
+                    label: t('forms.node.edition'),
                     value: String(new Date().getFullYear()),
                 },
             ],
         },
         {
-            name: 'Auteurs',
+            name: t('forms.node.author.title', 2),
             inputs: [
                 {
                     id: 'authors',
-                    label: 'Auteur',
+                    label: t('forms.node.author.title', 1),
                     type: 'repeat',
                     value: [],
                     inputs: [
                         {
                             id: 'name',
                             type: 'text',
-                            label: 'Nom',
-                            placeholder: 'Jeanne Dupont',
+                            label: t('global.name'),
+                            placeholder: t('forms.node.author.placeholder'),
                             value: '',
                         },
                         {
                             id: 'image',
                             type: 'file',
-                            label: 'Image',
-                            placeholder: 'Ajouter une image',
+                            label: t('forms.node.author.image.title'),
+                            placeholder: t('forms.node.author.image.placeholder'),
                             value: '',
                             accept: '.png,.jpg,.jpeg,.gif,.bmp,.svg,.webp',
-                            hint: "Format recommandé : carré (100x100)<br> Image visible dans la page de présentation de l'ePoc",
+                            hint: t('forms.node.author.image.hint'),
                         },
                         {
                             id: 'title',
                             type: 'text',
-                            label: 'Fonction',
-                            placeholder: "Chercheuse à l'Inria",
+                            label: t('forms.node.author.position.title'),
+                            placeholder: t('forms.node.author.position.placeholder'),
                             value: '',
-                            hint: 'Profession, fonction, affiliation…',
+                            hint: t('forms.node.author.position.hint'),
                         },
                         {
                             id: 'description',
                             type: 'html-text',
-                            label: 'Courte biographie',
-                            placeholder: 'Saisissez une courte biographie...',
+                            label: t('forms.node.author.biography'),
+                            placeholder: t('forms.type'),
                             value: '',
                         },
                     ],
@@ -245,11 +248,11 @@ export const epocForm: Form = {
             ],
         },
         {
-            name: 'Objectifs pédagogiques',
+            name: t('forms.node.objectives'),
             inputs: [
                 {
                     id: 'objectives',
-                    label: 'Objectif',
+                    label: t('global.objective'),
                     type: 'repeat',
                     value: [],
                     inputs: [
@@ -257,7 +260,7 @@ export const epocForm: Form = {
                             id: '',
                             type: 'textarea',
                             label: '',
-                            placeholder: 'Saisissez un objectif ...',
+                            placeholder: t('forms.type'),
                             value: '',
                         },
                     ],
@@ -265,50 +268,50 @@ export const epocForm: Form = {
             ],
         },
         {
-            name: 'Paramètres :',
+            name: t('settings.title'),
             inputs: [
                 {
                     id: 'certificateBadgeCount',
                     type: 'score',
-                    label: "Nombre de badge pour obtenir l'attestation",
+                    label: t('forms.node.certificateBadge'),
                     value: 1,
                 },
                 {
                     id: 'certificateScore',
                     type: 'score',
-                    label: "Score pour obtenir l'attestation",
+                    label: t('forms.node.certificateScore'),
                     value: 10,
-                    hint: "N'est pas pris en compte si le nombre de badge pour obtenir l'attestation est supérieur à 0",
+                    hint: t('forms.node.certificateScoreHint'),
                 },
                 {
                     id: 'chapterParameter',
                     type: 'text',
-                    label: 'Label des chapitres',
+                    label: t('forms.node.chapterLabel'),
                     value: '',
-                    placeholder: 'Saisissez...',
+                    placeholder: t('forms.type'),
                 },
                 {
                     id: 'chapterDuration',
                     type: 'score',
-                    label: 'Durée des chapitres (en minutes)',
+                    label: t('forms.node.chapterDuration'),
                     value: 0,
                 },
             ],
         },
         {
-            name: 'Plugins',
+            name: t('forms.node.plugin.title', 2),
             inputs: [
                 {
                     id: 'plugins',
-                    label: 'Plugin',
+                    label: t('forms.node.plugin.title', 1),
                     type: 'repeat',
                     value: [],
                     inputs: [
                         {
                             id: 'script',
                             type: 'file',
-                            label: 'Fichier de script',
-                            placeholder: 'Ajouter un script',
+                            label: t('forms.node.plugin.script'),
+                            placeholder: t('forms.node.plugin.scriptPlaceholder'),
                             targetDirectory: 'plugins',
                             value: '',
                             accept: '.js',
@@ -316,8 +319,8 @@ export const epocForm: Form = {
                         {
                             id: 'template',
                             type: 'file',
-                            label: 'Template html du plugin',
-                            placeholder: 'Ajouter un template',
+                            label: t('forms.node.plugin.template'),
+                            placeholder: t('forms.node.plugin.templatePlaceholder'),
                             targetDirectory: 'plugins',
                             value: '',
                             accept: 'html',
@@ -327,23 +330,23 @@ export const epocForm: Form = {
             ],
         },
         {
-            name: 'Licence',
+            name: t('forms.node.licence.title'),
             inputs: [
                 {
                     id: 'licenceName',
                     type: 'text',
-                    label: 'Nom',
+                    label: t('global.name'),
                     placeholder: 'CC-BY 4.0',
                     value: '',
-                    hint: 'Nom de la licence de votre contenu ePoc',
+                    hint: t('forms.node.licence.hint'),
                 },
                 {
                     id: 'licenceUrl',
                     type: 'text',
-                    label: 'URL',
-                    placeholder: 'https://creativecommons.org/licenses/by/4.0/deed',
+                    label: t('forms.node.licence.url'),
+                    placeholder: t('forms.node.licence.urlPlaceholder'),
                     value: '',
-                    hint: 'Texte complet de la licence choisie',
+                    hint: t('forms.node.licence.urlHint'),
                 },
             ],
         },
@@ -352,7 +355,7 @@ export const epocForm: Form = {
 
 export const pageForm: Form = {
     type: 'page',
-    name: 'Page',
+    name: t('forms.node.page.title'),
     icon: 'icon-ecran',
     buttons: pageButtons,
     fields: [
@@ -361,38 +364,38 @@ export const pageForm: Form = {
                 {
                     id: 'title',
                     type: 'text',
-                    label: 'Titre',
+                    label: t('forms.node.title'),
                     value: '',
-                    placeholder: 'Saisissez...',
+                    placeholder: t('forms.type'),
                 },
                 {
                     id: 'subtitle',
                     type: 'text',
-                    label: 'Sous-titre',
+                    label: t('forms.node.subtitle'),
                     value: '',
-                    placeholder: 'Saisissez...',
+                    placeholder: t('forms.type'),
                 },
                 {
                     id: 'hidden',
                     type: 'checkbox',
-                    label: 'Caché dans la table des matières',
+                    label: t('forms.node.page.hidden'),
                     value: false,
                 },
                 {
                     id: 'conditional',
                     type: 'checkbox',
-                    label: "Ne s'affiche qu'a certaines conditions",
+                    label: t('forms.node.page.conditional'),
                     value: false,
-                    hint: "Option utilisé pour l'affichage conditionnel",
+                    hint: t('forms.node.page.conditionalHint'),
                 },
             ],
         },
         {
-            name: 'Composants',
+            name: t('forms.node.page.components'),
             inputs: [
                 {
                     id: 'components',
-                    label: 'Composants',
+                    label: t('forms.node.page.components'),
                     type: 'repeat',
                     value: [],
                     addButton: false,
@@ -405,7 +408,7 @@ export const pageForm: Form = {
 
 export const activityForm: Form = {
     type: 'activity',
-    name: 'Évaluation',
+    name: t('forms.node.activity'),
     icon: 'icon-ecran',
     buttons: activityButtons,
     fields: [
@@ -414,45 +417,45 @@ export const activityForm: Form = {
                 {
                     id: 'title',
                     type: 'text',
-                    label: 'Titre',
+                    label: t('forms.node.title'),
                     value: '',
-                    placeholder: 'Saisissez...',
+                    placeholder: t('forms.type'),
                 },
                 {
                     id: 'subtitle',
                     type: 'text',
-                    label: 'Sous-titre',
+                    label: t('forms.node.subtitle'),
                     value: '',
-                    placeholder: 'Saisissez...',
+                    placeholder: t('forms.type'),
                 },
                 {
                     id: 'summary',
                     type: 'textarea',
-                    label: 'Résumé',
+                    label: t('forms.content.summary'),
                     value: '',
-                    placeholder: 'Saisissez...',
+                    placeholder: t('forms.type'),
                 },
                 {
                     id: 'hidden',
                     type: 'checkbox',
-                    label: 'Caché dans la table des matières',
+                    label: t('forms.node.page.hidden'),
                     value: false,
                 },
                 {
                     id: 'conditional',
                     type: 'checkbox',
-                    label: "Ne s'affiche qu'a certaines conditions",
+                    label: t('forms.node.page.conditional'),
                     value: false,
-                    hint: "Option utilisé pour l'affichage conditionnel",
+                    hint: t('forms.node.page.conditionalHint'),
                 },
             ],
         },
         {
-            name: 'Composants',
+            name: t('forms.node.page.components'),
             inputs: [
                 {
                     id: 'components',
-                    label: 'Composants',
+                    label: t('forms.node.page.components'),
                     type: 'repeat',
                     value: [],
                     addButton: false,
-- 
GitLab


From 67b453feb3a68b5fdc993824e8cd0f35b969e3ae Mon Sep 17 00:00:00 2001
From: VIAUD Nathan <nathan.viaud@inria.fr>
Date: Wed, 19 Mar 2025 10:23:39 +0100
Subject: [PATCH 07/18] WIP: translate (forms)

---
 src/i18n/fr.ts                              |  38 +++
 src/shared/data/forms/questionsForm.data.ts | 243 ++++++++++----------
 2 files changed, 161 insertions(+), 120 deletions(-)

diff --git a/src/i18n/fr.ts b/src/i18n/fr.ts
index 43f5cd0f..52117207 100644
--- a/src/i18n/fr.ts
+++ b/src/i18n/fr.ts
@@ -186,4 +186,42 @@ export const frMessages = {
             activity: 'Évaluation',
         },
     },
+    questions: {
+        configuration: "Configuration de l'évaluation",
+        question: 'Question',
+        askQuestion: 'Posez la question',
+        instruction: 'Consigne',
+        instructionPlaceholder: 'Instruction pour répondre à la question',
+        explanation: 'Explication',
+        typeExplanation: 'Saisissez une explication',
+        addExplanation: 'Ajouter une explication',
+        response: 'Réponse',
+        responses: 'Réponses',
+        typeResponse: 'Saisissez une réponse',
+        correctResponse: 'Bonne réponse',
+        categories: 'Catégories de réponses proposées',
+        category: 'Catégorie',
+        typeCategory: 'Saisissez un intitulé catégorie',
+        proposedChoices: 'Catégories de choix proposées',
+        proposedResponses: 'Réponses proposées',
+        cards: 'Cartes',
+        card: 'Carte',
+        typeProposition: 'Saisissez une proposition',
+        template: {
+            title: 'Template',
+            select: 'Selectionnez un template',
+            data: 'Données',
+            key: 'Clé',
+            value: 'Valeur',
+        },
+        types: {
+            qcm: 'QCM',
+            dragDrop: 'Drag & Drop',
+            reorder: 'Reorder',
+            swipe: 'Swipe',
+            dropdownList: 'Liste déroulante',
+            custom: 'Question personnalisée',
+        },
+        score: 'Score',
+    },
 };
diff --git a/src/shared/data/forms/questionsForm.data.ts b/src/shared/data/forms/questionsForm.data.ts
index dbf9d3bc..16543d50 100644
--- a/src/shared/data/forms/questionsForm.data.ts
+++ b/src/shared/data/forms/questionsForm.data.ts
@@ -1,79 +1,82 @@
 import { Form } from '@/src/shared/interfaces';
 import { contentButtons } from './formButtons.data';
+import { i18n } from '@/src/i18n/config';
+
+const { t } = i18n.global;
 
 export const qcmForm: Form = {
     type: 'choice',
-    name: 'QCM',
+    name: t('questions.types.qcm'),
     icon: 'icon-qcm',
     displayFieldIndex: true,
     buttons: contentButtons,
     fields: [
         {
-            name: "Configuration de l'évaluation",
+            name: t('questions.configuration'),
             inputs: [
                 {
                     id: 'score',
                     type: 'score',
-                    label: 'Score',
+                    label: t('questions.score'),
                     value: 0,
                 },
             ],
         },
         {
-            name: 'Question',
+            name: t('questions.question'),
             inputs: [
                 {
                     id: 'label',
                     type: 'textarea',
-                    label: 'Question',
+                    label: t('questions.question'),
                     value: '',
-                    placeholder: 'Posez la question',
+                    placeholder: t('questions.askQuestion'),
                 },
                 {
                     id: 'statement',
                     type: 'html-inline',
-                    label: 'Consigne',
+                    label: t('questions.instruction'),
                     value: '',
-                    placeholder: 'Instruction pour répondre à la question',
+                    placeholder: t('questions.instructionPlaceholder'),
                 },
             ],
         },
         {
-            name: 'Réponses',
+            name: t('questions.responses'),
             inputs: [
                 {
                     id: 'responses',
-                    label: 'Réponse',
+                    label: t('questions.response'),
                     type: 'repeat',
                     value: [],
                     inputs: [
                         {
                             id: 'label',
                             type: 'text',
-                            label: 'Réponse',
-                            placeholder: 'Saisissez une réponse...',
+                            label: t('questions.response'),
+                            placeholder: t('questions.typeResponse'),
                             value: '',
                         },
                         {
                             id: 'value',
                             type: 'hidden',
                             label: '',
-                            placeholder: 'Valeur cachée',
+                            placeholder: t('forms.type'),
                             value: '',
                         },
                         {
                             id: 'feedback',
                             type: 'textarea',
-                            label: 'Explication',
-                            placeholder: 'Saisissez une explication...',
+                            label: t('questions.explanation'),
+                            placeholder: t('questions.typeExplanation'),
                             value: '',
                             collapsible: true,
-                            collapsibleLabel: 'Ajouter une explication',
+                            collapsibleLabel: t('questions.addExplanation'),
                         },
                         {
                             id: 'isCorrect',
                             type: 'checkbox',
-                            label: 'Bonne réponse',
+                            label: t('questions.correctResponse'),
                             value: false,
                         },
                     ],
@@ -81,14 +84,14 @@ export const qcmForm: Form = {
             ],
         },
         {
-            name: 'Explication',
+            name: t('questions.explanation'),
             inputs: [
                 {
                     id: 'explanation',
                     type: 'html',
                     label: '',
                     value: '',
-                    placeholder: 'Saisissez une explication',
+                    placeholder: t('questions.typeExplanation'),
                 },
             ],
         },
@@ -97,47 +100,47 @@ export const qcmForm: Form = {
 
 export const dragDropForm: Form = {
     type: 'drag-and-drop',
-    name: 'Drag & Drop',
+    name: t('questions.types.dragDrop'),
     icon: 'icon-dragdrop',
     displayFieldIndex: true,
     buttons: contentButtons,
     fields: [
         {
-            name: "Configuration de l'évaluation",
+            name: t('questions.configuration'),
             inputs: [
                 {
                     id: 'score',
                     type: 'score',
-                    label: 'Score',
+                    label: t('questions.score'),
                     value: 0,
                 },
             ],
         },
         {
-            name: 'Question',
+            name: t('questions.question'),
             inputs: [
                 {
                     id: 'label',
                     type: 'textarea',
-                    label: 'Question',
+                    label: t('questions.question'),
                     value: '',
-                    placeholder: 'Posez la question',
+                    placeholder: t('questions.askQuestion'),
                 },
                 {
                     id: 'statement',
                     type: 'html-inline',
-                    label: 'Consigne',
+                    label: t('questions.instruction'),
                     value: '',
-                    placeholder: 'Instruction pour répondre à la question',
+                    placeholder: t('questions.instructionPlaceholder'),
                 },
             ],
         },
         {
-            name: 'Catégories de réponses proposées',
+            name: t('questions.categories'),
             inputs: [
                 {
                     id: 'categories',
-                    label: 'Catégorie',
+                    label: t('questions.category'),
                     type: 'repeat',
                     value: [],
                     inputs: [
@@ -145,7 +148,7 @@ export const dragDropForm: Form = {
                             id: '',
                             type: 'textarea',
                             label: '',
-                            placeholder: 'Saisissez un intitulé catégorie..',
+                            placeholder: t('questions.typeCategory'),
                             value: '',
                         },
                     ],
@@ -153,36 +156,36 @@ export const dragDropForm: Form = {
             ],
         },
         {
-            name: 'Réponses proposées',
+            name: t('questions.proposedResponses'),
             inputs: [
                 {
                     id: 'responses',
-                    label: 'Réponse',
+                    label: t('questions.response'),
                     type: 'repeat',
                     value: [],
                     inputs: [
                         {
                             id: 'label',
                             type: 'text',
-                            label: 'Réponse',
-                            placeholder: 'Saisissez une réponse...',
+                            label: t('questions.response'),
+                            placeholder: t('questions.typeResponse'),
                             value: '',
                         },
                         {
                             id: 'value',
                             type: 'hidden',
                             label: '',
-                            placeholder: 'Valeur cachée',
+                            placeholder: t('forms.type'),
                             value: '',
                         },
                         {
                             id: 'feedback',
                             type: 'textarea',
-                            label: 'Explication',
-                            placeholder: 'Saisissez une explication...',
+                            label: t('questions.explanation'),
+                            placeholder: t('questions.typeExplanation'),
                             value: '',
                             collapsible: true,
-                            collapsibleLabel: 'Ajouter une explication',
+                            collapsibleLabel: t('questions.addExplanation'),
                         },
                         {
                             id: 'category',
@@ -198,14 +201,14 @@ export const dragDropForm: Form = {
             ],
         },
         {
-            name: 'Explication',
+            name: t('questions.explanation'),
             inputs: [
                 {
                     id: 'explanation',
                     type: 'html',
                     label: '',
                     value: '',
-                    placeholder: 'Saisissez une explication',
+                    placeholder: t('questions.typeExplanation'),
                 },
             ],
         },
@@ -214,71 +217,71 @@ export const dragDropForm: Form = {
 
 export const reorderForm: Form = {
     type: 'reorder',
-    name: 'Reorder',
+    name: t('questions.types.reorder'),
     icon: 'icon-reorder',
     displayFieldIndex: true,
     buttons: contentButtons,
     fields: [
         {
-            name: "Configuration de l'évaluation",
+            name: t('questions.configuration'),
             inputs: [
                 {
                     id: 'score',
                     type: 'score',
-                    label: 'Score',
+                    label: t('questions.score'),
                     value: 0,
                 },
             ],
         },
         {
-            name: 'Question',
+            name: t('questions.question'),
             inputs: [
                 {
                     id: 'label',
                     type: 'textarea',
-                    label: 'Question',
+                    label: t('questions.question'),
                     value: '',
-                    placeholder: 'Posez la question',
+                    placeholder: t('questions.askQuestion'),
                 },
                 {
                     id: 'statement',
                     type: 'html-inline',
-                    label: 'Consigne',
+                    label: t('questions.instruction'),
                     value: '',
-                    placeholder: 'Instruction pour répondre à la question',
+                    placeholder: t('questions.instructionPlaceholder'),
                 },
             ],
         },
         {
-            name: 'Réponses',
+            name: t('questions.responses'),
             inputs: [
                 {
                     id: 'responses',
-                    label: 'Réponse',
+                    label: t('questions.response'),
                     type: 'repeat',
                     value: [],
                     inputs: [
                         {
                             id: 'label',
                             type: 'text',
-                            label: 'Réponse',
-                            placeholder: 'Saisissez une réponse...',
+                            label: t('questions.response'),
+                            placeholder: t('questions.typeResponse'),
                             value: '',
                         },
                         {
                             id: 'feedback',
                             type: 'textarea',
-                            label: 'Explication',
-                            placeholder: 'Saisissez une explication...',
+                            label: t('questions.explanation'),
+                            placeholder: t('questions.typeExplanation'),
                             value: '',
                             collapsible: true,
-                            collapsibleLabel: 'Ajouter une explication',
+                            collapsibleLabel: t('questions.addExplanation'),
                         },
                         {
                             id: 'value',
                             type: 'hidden',
                             label: '',
-                            placeholder: 'Valeur cachée',
+                            placeholder: t('forms.type'),
                             value: '',
                         },
                     ],
@@ -286,14 +289,14 @@ export const reorderForm: Form = {
             ],
         },
         {
-            name: 'Explication',
+            name: t('questions.explanation'),
             inputs: [
                 {
                     id: 'explanation',
                     type: 'html',
                     label: '',
                     value: '',
-                    placeholder: 'Saisissez une explication...',
+                    placeholder: t('questions.typeExplanation'),
                 },
             ],
         },
@@ -302,47 +305,47 @@ export const reorderForm: Form = {
 
 export const swipeForm: Form = {
     type: 'swipe',
-    name: 'Swipe',
+    name: t('questions.types.swipe'),
     icon: 'icon-swipe',
     displayFieldIndex: true,
     buttons: contentButtons,
     fields: [
         {
-            name: "Configuration de l'évaluation",
+            name: t('questions.configuration'),
             inputs: [
                 {
                     id: 'score',
                     type: 'score',
-                    label: 'Score',
+                    label: t('questions.score'),
                     value: 0,
                 },
             ],
         },
         {
-            name: 'Question',
+            name: t('questions.question'),
             inputs: [
                 {
                     id: 'label',
                     type: 'textarea',
-                    label: 'Question',
+                    label: t('questions.question'),
                     value: '',
-                    placeholder: 'Posez la question',
+                    placeholder: t('questions.askQuestion'),
                 },
                 {
                     id: 'statement',
                     type: 'html-inline',
-                    label: 'Consigne',
+                    label: t('questions.instruction'),
                     value: '',
-                    placeholder: 'Instruction pour répondre à la question',
+                    placeholder: t('questions.instructionPlaceholder'),
                 },
             ],
         },
         {
-            name: 'Catégories de choix proposées',
+            name: t('questions.proposedChoices'),
             inputs: [
                 {
                     id: 'categories',
-                    label: 'Choix',
+                    label: t('questions.choice'),
                     type: 'repeat',
                     value: ['Droite', 'Gauche'],
                     addButton: false,
@@ -351,7 +354,7 @@ export const swipeForm: Form = {
                             id: '',
                             type: 'text',
                             label: '',
-                            placeholder: 'Saisissez une réponse...',
+                            placeholder: t('questions.typeResponse'),
                             value: '',
                         },
                     ],
@@ -359,35 +362,35 @@ export const swipeForm: Form = {
             ],
         },
         {
-            name: 'Réponse proposée',
+            name: t('questions.proposedResponses'),
             inputs: [
                 {
                     id: 'responses',
-                    label: 'Carte',
+                    label: t('questions.card'),
                     type: 'repeat',
                     value: [],
                     inputs: [
                         {
                             id: 'label',
                             type: 'text',
-                            label: 'Réponse',
-                            placeholder: 'Saisissez une proposition',
+                            label: t('questions.response'),
+                            placeholder: t('questions.typeProposition'),
                             value: '',
                         },
                         {
                             id: 'feedback',
                             type: 'textarea',
-                            label: 'Explication',
-                            placeholder: 'Saisissez une explication...',
+                            label: t('questions.explanation'),
+                            placeholder: t('questions.typeExplanation'),
                             value: '',
                             collapsible: true,
-                            collapsibleLabel: 'Ajouter une explication',
+                            collapsibleLabel: t('questions.addExplanation'),
                         },
                         {
                             id: 'value',
                             type: 'hidden',
                             label: '',
-                            placeholder: 'Valeur cachée',
+                            placeholder: t('forms.type'),
                             value: '',
                         },
                         {
@@ -404,14 +407,14 @@ export const swipeForm: Form = {
             ],
         },
         {
-            name: 'Explication',
+            name: t('questions.explanation'),
             inputs: [
                 {
                     id: 'explanation',
                     type: 'html',
                     label: '',
                     value: '',
-                    placeholder: 'Saisissez une explication...',
+                    placeholder: t('questions.typeExplanation'),
                 },
             ],
         },
@@ -420,47 +423,47 @@ export const swipeForm: Form = {
 
 export const listForm: Form = {
     type: 'dropdown-list',
-    name: 'Liste déroulante',
+    name: t('questions.types.dropdownList'),
     icon: 'icon-liste',
     displayFieldIndex: true,
     buttons: contentButtons,
     fields: [
         {
-            name: "Configuration de l'évaluation",
+            name: t('questions.configuration'),
             inputs: [
                 {
                     id: 'score',
                     type: 'score',
-                    label: 'Score',
+                    label: t('questions.score'),
                     value: 0,
                 },
             ],
         },
         {
-            name: 'Question',
+            name: t('questions.question'),
             inputs: [
                 {
                     id: 'label',
                     type: 'textarea',
-                    label: 'Question',
+                    label: t('questions.question'),
                     value: '',
-                    placeholder: 'Posez la question',
+                    placeholder: t('questions.askQuestion'),
                 },
                 {
                     id: 'statement',
                     type: 'html-inline',
-                    label: 'Consigne',
+                    label: t('questions.instruction'),
                     value: '',
-                    placeholder: 'Instruction pour répondre à la question',
+                    placeholder: t('questions.instructionPlaceholder'),
                 },
             ],
         },
         {
-            name: 'Catégories de choix proposées',
+            name: t('questions.proposedChoices'),
             inputs: [
                 {
                     id: 'categories',
-                    label: 'Choix',
+                    label: t('questions.choice'),
                     type: 'repeat',
                     value: [],
                     inputs: [
@@ -468,7 +471,7 @@ export const listForm: Form = {
                             id: '',
                             type: 'text',
                             label: '',
-                            placeholder: 'Saisissez une réponse...',
+                            placeholder: t('questions.typeResponse'),
                             value: '',
                         },
                     ],
@@ -476,35 +479,35 @@ export const listForm: Form = {
             ],
         },
         {
-            name: 'Cartes',
+            name: t('questions.cards'),
             inputs: [
                 {
                     id: 'responses',
-                    label: 'Carte',
+                    label: t('questions.card'),
                     type: 'repeat',
                     value: [],
                     inputs: [
                         {
                             id: 'label',
                             type: 'text',
-                            label: 'Réponse',
-                            placeholder: 'Saisissez une question...',
+                            label: t('questions.response'),
+                            placeholder: t('questions.typeProposition'),
                             value: '',
                         },
                         {
                             id: 'feedback',
                             type: 'textarea',
-                            label: 'Explication',
-                            placeholder: 'Saisissez une explication...',
+                            label: t('questions.explanation'),
+                            placeholder: t('questions.typeExplanation'),
                             value: '',
                             collapsible: true,
-                            collapsibleLabel: 'Ajouter une explication',
+                            collapsibleLabel: t('questions.addExplanation'),
                         },
                         {
                             id: 'value',
                             type: 'hidden',
                             label: '',
-                            placeholder: 'Valeur cachée',
+                            placeholder: t('forms.type'),
                             value: '',
                         },
                         {
@@ -521,14 +524,14 @@ export const listForm: Form = {
             ],
         },
         {
-            name: 'Explication',
+            name: t('questions.explanation'),
             inputs: [
                 {
                     id: 'explanation',
                     type: 'html',
                     label: '',
                     value: '',
-                    placeholder: 'Saisissez une explication...',
+                    placeholder: t('questions.typeExplanation'),
                 },
             ],
         },
@@ -537,48 +540,48 @@ export const listForm: Form = {
 
 export const customQuestionForm: Form = {
     type: 'custom',
-    name: 'Question personnalisée',
+    name: t('questions.types.custom'),
     icon: 'icon-terminal',
     displayFieldIndex: true,
     buttons: contentButtons,
     fields: [
         {
-            name: "Configuration de l'évaluation",
+            name: t('questions.configuration'),
             inputs: [
                 {
                     id: 'score',
                     type: 'score',
-                    label: 'Score',
+                    label: t('questions.score'),
                     value: 0,
                 },
             ],
         },
         {
-            name: 'Question',
+            name: t('questions.question'),
             inputs: [
                 {
                     id: 'label',
                     type: 'textarea',
-                    label: 'Question',
+                    label: t('questions.question'),
                     value: '',
-                    placeholder: 'Posez la question',
+                    placeholder: t('questions.askQuestion'),
                 },
                 {
                     id: 'statement',
                     type: 'html-inline',
-                    label: 'Consigne',
+                    label: t('questions.instruction'),
                     value: '',
-                    placeholder: 'Instruction pour répondre à la question',
+                    placeholder: t('questions.instructionPlaceholder'),
                 },
             ],
         },
         {
-            name: 'Template',
+            name: t('questions.template.title'),
             inputs: [
                 {
                     id: 'template',
                     type: 'select',
-                    label: 'Selectionnez un template',
+                    label: t('questions.template.select'),
                     value: '',
                     options: [],
                     linkedOptions: 'plugins.*.template',
@@ -586,26 +589,26 @@ export const customQuestionForm: Form = {
             ],
         },
         {
-            name: 'Données',
+            name: t('questions.template.data'),
             inputs: [
                 {
                     type: 'repeat',
                     id: 'data',
-                    label: 'Données',
+                    label: t('questions.template.data'),
                     value: [],
                     inputs: [
                         {
                             id: 'key',
                             type: 'text',
-                            label: 'Clé',
-                            placeholder: 'Clé',
+                            label: t('questions.template.key'),
+                            placeholder: t('questions.template.key'),
                             value: '',
                         },
                         {
                             id: 'value',
                             type: 'textarea',
-                            label: 'Valeur',
-                            placeholder: 'Valeur',
+                            label: t('questions.template.value'),
+                            placeholder: t('questions.template.value'),
                             value: '',
                         },
                     ],
@@ -613,25 +616,25 @@ export const customQuestionForm: Form = {
             ],
         },
         {
-            name: 'Réponse',
+            name: t('questions.response'),
             inputs: [
                 {
                     id: 'correctResponse',
-                    label: 'Réponse',
+                    label: t('questions.response'),
                     type: 'text',
                     value: '',
                 },
             ],
         },
         {
-            name: 'Explication',
+            name: t('questions.explanation'),
             inputs: [
                 {
                     id: 'explanation',
                     type: 'html',
                     label: '',
                     value: '',
-                    placeholder: 'Saisissez une explication',
+                    placeholder: t('questions.typeExplanation'),
                 },
             ],
         },
-- 
GitLab


From 70537d5997be76cf26b4e443a4cb2a8eb3fe4fc7 Mon Sep 17 00:00:00 2001
From: VIAUD Nathan <nathan.viaud@inria.fr>
Date: Wed, 19 Mar 2025 13:36:09 +0100
Subject: [PATCH 08/18] WIP: translate(finished french translation)

---
 src/i18n/fr.ts                             | 84 ++++++++++++++++++++++
 src/shared/data/badge.data.ts              | 24 ++++---
 src/shared/data/forms/formButtons.data.ts  |  2 +-
 src/shared/data/sideBar.data.ts            | 59 +++++++--------
 src/shared/services/graph/badge.service.ts | 57 ++++++++-------
 5 files changed, 160 insertions(+), 66 deletions(-)

diff --git a/src/i18n/fr.ts b/src/i18n/fr.ts
index 52117207..977cb6b9 100644
--- a/src/i18n/fr.ts
+++ b/src/i18n/fr.ts
@@ -45,6 +45,57 @@ export const frMessages = {
         selectIcon: 'Sélectionner une icône',
         defaultIcons: 'Icônes par défaut',
         customIcons: 'Icônes personnalisées',
+        phrase: {
+            type: {
+                video: 'la vidéo',
+                chapter: 'le chapitre',
+                page: 'la page',
+                html: 'le texte',
+                audio: "l'audio",
+                activity: "l'évaluation",
+                question: 'la question',
+            },
+            verb: {
+                started: {
+                    true: 'Avoir commencé',
+                    false: 'Ne pas avoir pas commencé',
+                },
+                completed: {
+                    true: 'Avoir terminé',
+                    false: 'Ne pas avoir terminé',
+                },
+                viewed: {
+                    true: 'Avoir vu',
+                    false: 'Ne pas avoir vu',
+                },
+                read: {
+                    true: 'Avoir lu',
+                    false: 'Ne pas avoir lu',
+                },
+                played: {
+                    true: 'Avoir lancé',
+                    false: 'Ne pas avoir lancé',
+                },
+                watched: {
+                    true: 'Avoir regardé',
+                    false: 'Ne pas avoir regardé',
+                },
+                listened: {
+                    true: 'Avoir écouté',
+                    false: 'Ne pas avoir écouté',
+                },
+                attempted: {
+                    true: 'Avoir tenté',
+                    false: 'Ne pas avoir tenté',
+                },
+                passed: {
+                    true: 'Avoir réussi',
+                    false: 'Avoir échoué',
+                },
+                scored: "Avoir obtenu un score d'au moins",
+            },
+            scored: '{verb} {value} à',
+        },
     },
     inputs: {
         manageConditions: 'Configurer les conditions',
@@ -224,4 +275,37 @@ export const frMessages = {
         },
         score: 'Score',
     },
+    sidebar: {
+        content: {
+            text: 'Texte',
+            video: 'Vidéo',
+            audio: 'Audio',
+            textTooltip: 'Glisser/déposer pour ajouter un texte',
+            videoTooltip: 'Glisser/déposer pour ajouter une vidéo',
+            audioTooltip: 'Glisser/déposer pour ajouter un audio',
+        },
+        pages: {
+            question: 'Question',
+            conditions: 'Conditions',
+            conditionsLegacy: 'Conditions (legacy)',
+            model: 'Modèle',
+            badge: 'Badge',
+            questionTooltip: 'Cliquer pour ajouter une question',
+            conditionsTooltip: 'Glisser/déposer pour ajouter une condition',
+            modelTooltip: 'Cliquer pour ouvrir le menu modèle',
+            badgeTooltip: 'Cliquer pour ouvrir le menu badge',
+        },
+    },
+    verbs: {
+        started: 'Commencé',
+        completed: 'Terminé',
+        viewed: 'Vu',
+        read: 'Lu',
+        played: 'Joué',
+        watched: 'Regardé',
+        listened: 'Écouté',
+        attempted: 'Tenté',
+        scored: 'Obtenu un score de',
+        passed: 'Réussi',
+    },
 };
diff --git a/src/shared/data/badge.data.ts b/src/shared/data/badge.data.ts
index 678d8aa1..b083d38e 100644
--- a/src/shared/data/badge.data.ts
+++ b/src/shared/data/badge.data.ts
@@ -1,20 +1,24 @@
 import env from '@/src/shared/utils/env';
 import { ElementType, VerbKey, Verbs } from '@/src/shared/interfaces';
+import { i18n } from '@/src/i18n/config';
+
+const { t } = i18n.global;
+
 export const iconsPath = env.isDev ? '/img/badge/icon' : 'img/badge/icon';
 
 export const defaultBadgeIcons = ['audio', 'check', 'condition', 'cup', 'puzzle', 'question', 'star', 'video'];
 
 export const verbs: Verbs = {
-    started: { label: 'Commencé', valueType: 'boolean' },
-    completed: { label: 'Terminé', valueType: 'boolean' },
-    viewed: { label: 'Vu', valueType: 'boolean' },
-    read: { label: 'Lu', valueType: 'boolean' },
-    played: { label: 'Joué', valueType: 'boolean' },
-    watched: { label: 'Regardé', valueType: 'boolean' },
-    listened: { label: 'Écouté', valueType: 'boolean' },
-    attempted: { label: 'Tenté', valueType: 'boolean' },
-    scored: { label: 'Obtenu un score de', valueType: 'number' },
-    passed: { label: 'Réussi', valueType: 'boolean' },
+    started: { label: t('verbs.started'), valueType: 'boolean' },
+    completed: { label: t('verbs.completed'), valueType: 'boolean' },
+    viewed: { label: t('verbs.viewed'), valueType: 'boolean' },
+    read: { label: t('verbs.read'), valueType: 'boolean' },
+    played: { label: t('verbs.played'), valueType: 'boolean' },
+    watched: { label: t('verbs.watched'), valueType: 'boolean' },
+    listened: { label: t('verbs.listened'), valueType: 'boolean' },
+    attempted: { label: t('verbs.attempted'), valueType: 'boolean' },
+    scored: { label: t('verbs.scored'), valueType: 'number' },
+    passed: { label: t('verbs.passed'), valueType: 'boolean' },
 };
 
 export const elementVerbs: Record<ElementType, VerbKey[]> = {
diff --git a/src/shared/data/forms/formButtons.data.ts b/src/shared/data/forms/formButtons.data.ts
index 3c16564b..eef8dbcc 100644
--- a/src/shared/data/forms/formButtons.data.ts
+++ b/src/shared/data/forms/formButtons.data.ts
@@ -35,7 +35,7 @@ export const contentButtons: FormButton[] =
         [
             ...baseButtons,
             { label: t('forms.buttons.backToPage'), icon: 'icon-ecran', action: 'back-to-page' },
-            { label: t('forms.button.duplicateElement'), icon: 'icon-plus', action: 'duplicate-element' },
+            { label: t('forms.buttons.duplicateElement'), icon: 'icon-plus', action: 'duplicate-element' },
         ]
     :   [...baseButtons];
 
diff --git a/src/shared/data/sideBar.data.ts b/src/shared/data/sideBar.data.ts
index 8ec7d312..440e4859 100644
--- a/src/shared/data/sideBar.data.ts
+++ b/src/shared/data/sideBar.data.ts
@@ -1,35 +1,38 @@
 import { SideAction } from '@/src/shared/interfaces';
+import { i18n } from '@/src/i18n/config';
+
+const { t } = i18n.global;
 
 export const questions: SideAction[] = [
     {
         icon: 'icon-qcm',
         type: 'choice',
-        label: 'QCM',
+        label: t('questions.types.qcm'),
     },
     {
         icon: 'icon-dragdrop',
         type: 'drag-and-drop',
-        label: 'Drag & Drop',
+        label: t('questions.types.dragDrop'),
     },
     {
         icon: 'icon-reorder',
         type: 'reorder',
-        label: 'Reorder',
+        label: t('questions.types.reorder'),
     },
     {
         icon: 'icon-swipe',
         type: 'swipe',
-        label: 'Swipe',
+        label: t('questions.types.swipe'),
     },
     {
         icon: 'icon-liste',
         type: 'dropdown-list',
-        label: 'Liste déroulante',
+        label: t('questions.types.dropdownList'),
     },
     {
         icon: 'icon-terminal',
         type: 'custom',
-        label: 'Question personnalisée',
+        label: t('questions.types.custom'),
     }
 ];
 
@@ -37,20 +40,20 @@ const contents: SideAction[] = [
     {
         icon: 'icon-texte',
         type: 'text',
-        label: 'Texte',
-        tooltip: 'Glisser/déposer pour ajouter un texte',
+        label: t('sidebar.content.text'),
+        tooltip: t('sidebar.content.textTooltip'),
     },
     {
         icon: 'icon-video',
         type: 'video',
-        label: 'Vidéo',
-        tooltip: 'Glisser/déposer pour ajouter une vidéo',
+        label: t('sidebar.content.video'),
+        tooltip: t('sidebar.content.videoTooltip'),
     },
     {
         icon: 'icon-audio',
         type: 'audio',
-        label: 'Audio',
-        tooltip: 'Glisser/déposer pour ajouter un audio',
+        label: t('sidebar.content.audio'),
+        tooltip: t('sidebar.content.audioTooltip'),
     },
 ];
 
@@ -60,49 +63,49 @@ export const standardPages: SideAction[] = [
     {
         icon: 'icon-texte',
         type: 'text',
-        label: 'Texte',
-        tooltip: 'Glisser/déposer pour ajouter un texte',
+        label: t('sidebar.content.text'),
+        tooltip: t('sidebar.content.textTooltip'),
     },
     {
         icon: 'icon-video',
         type: 'video',
-        label: 'Vidéo',
-        tooltip: 'Glisser/déposer pour ajouter une vidéo',
+        label: t('sidebar.content.video'),
+        tooltip: t('sidebar.content.videoTooltip'),
     },
     {
         icon: 'icon-audio',
         type: 'audio',
-        label: 'Audio',
-        tooltip: 'Glisser/déposer pour ajouter un audio',
+        label: t('sidebar.content.audio'),
+        tooltip: t('sidebar.content.audioTooltip'),
     },
     {
         icon: 'icon-question',
         type: 'question',
-        label: 'Question',
-        tooltip: 'Cliquer pour ajouter une question',
+        label: t('sidebar.pages.question'),
+        tooltip: t('sidebar.pages.questionTooltip'),
     },
     {
         icon: 'icon-condition',
         type: 'condition',
-        label: 'Conditions',
-        tooltip: 'Glisser/déposer pour ajouter une condition',
+        label: t('sidebar.pages.conditions'),
+        tooltip: t('sidebar.pages.conditionsTooltip'),
     },
     {
         icon: 'icon-condition-legacy',
         type: 'legacy-condition',
-        label: 'Conditions (legacy)',
-        tooltip: 'Glisser/déposer pour ajouter une condition',
+        label: t('sidebar.pages.conditionsLegacy'),
+        tooltip: t('sidebar.pages.conditionsTooltip'),
     },
     {
         icon: 'icon-modele',
         type: 'model',
-        label: 'Modèle',
-        tooltip: 'Cliquer pour ouvrir le menu modèle',
+        label: t('sidebar.pages.model'),
+        tooltip: t('sidebar.pages.modelTooltip'),
     },
     {
         icon: 'icon-badge',
         type: 'badge',
-        label: 'Badge',
-        tooltip: 'Cliquer pour ouvrir le menu badge',
+        label: t('sidebar.pages.badge'),
+        tooltip: t('sidebar.pages.badgeTooltip'),
     },
 ];
diff --git a/src/shared/services/graph/badge.service.ts b/src/shared/services/graph/badge.service.ts
index c9c0272c..568e4cc6 100644
--- a/src/shared/services/graph/badge.service.ts
+++ b/src/shared/services/graph/badge.service.ts
@@ -6,6 +6,9 @@ import { saveState } from '@/src/shared/services/undoRedo.service';
 import { elementVerbs, verbs } from '@/src/shared/data';
 import { generateContentId, graphService } from '@/src/shared/services';
 import { Operators } from '@epoc/epoc-types/dist/v2';
+import { i18n } from '@/src/i18n/config';
+
+const { t } = i18n.global;
 
 const { findNode } = useVueFlow('main');
 
@@ -65,60 +68,60 @@ export function createRule(entry: Condition[]): Rule {
 }
 
 const phraseType = {
-    video: 'la vidéo',
-    chapter: 'le chapitre',
-    page: 'la page',
-    html: 'le texte',
-    audio: "l'audio",
-    activity: "l'évaluation",
-    question: 'la question',
+    video: t('badge.phrase.type.video'),
+    chapter: t('badge.phrase.type.chapter'),
+    page: t('badge.phrase.type.page'),
+    html: t('badge.phrase.type.html'),
+    audio: t('badge.phrase.type.audio'),
+    activity: t('badge.phrase.type.activity'),
+    question: t('badge.phrase.type.question'),
 };
 
 const phraseVerb = {
     started: {
-        true: 'Avoir commencé',
-        false: 'Ne pas avoir pas commencé',
+        true: t('badge.phrase.verb.started.true'),
+        false: t('badge.phrase.verb.started.false'),
     },
     completed: {
-        true: 'Avoir terminé',
-        false: 'Ne pas avoir terminé',
+        true: t('badge.phrase.verb.completed.true'),
+        false: t('badge.phrase.verb.completed.false'),
     },
     viewed: {
-        true: 'Avoir vu',
-        false: 'Ne pas avoir vu',
+        true: t('badge.phrase.verb.viewed.true'),
+        false: t('badge.phrase.verb.viewed.false'),
     },
     read: {
-        true: 'Avoir lu',
-        false: 'Ne pas avoir lu',
+        true: t('badge.phrase.verb.read.true'),
+        false: t('badge.phrase.verb.read.false'),
     },
     played: {
-        true: 'Avoir lancé',
-        false: 'Ne pas avoir lancé',
+        true: t('badge.phrase.verb.played.true'),
+        false: t('badge.phrase.verb.played.false'),
     },
     watched: {
-        true: 'Avoir regardé',
-        false: 'Ne pas avoir regardé',
+        true: t('badge.phrase.verb.watched.true'),
+        false: t('badge.phrase.verb.watched.false'),
     },
     listened: {
-        true: 'Avoir écouté',
-        false: 'Ne pas avoir écouté',
+        true: t('badge.phrase.verb.listened.true'),
+        false: t('badge.phrase.verb.listened.false'),
     },
     attempted: {
-        true: 'Avoir tenté',
-        false: 'Ne pas avoir tenté',
+        true: t('badge.phrase.verb.attempted.true'),
+        false: t('badge.phrase.verb.attempted.false'),
     },
     passed: {
-        true: 'Avoir réussi',
-        false: 'Avoir échoué',
+        true: t('badge.phrase.verb.passed.true'),
+        false: t('badge.phrase.verb.passed.false'),
     },
-    scored: "Avoir obtenu un score d'au moins",
+    scored: t('badge.phrase.verb.scored'),
 };
 
 export function createPhrase(condition: Condition, elementType: ElementType) {
     const { verb, value } = condition;
     let firstPart: string;
     if (verb === 'scored') {
-        firstPart = `${phraseVerb[verb]} ${value} à`;
+        firstPart = t('badge.phrase.scored', { value, verb: phraseVerb[verb] });
     } else {
         firstPart = `${phraseVerb[verb][value]}`;
     }
-- 
GitLab


From a85c422748e1469a31b8849f8985f6dbc02a4eb7 Mon Sep 17 00:00:00 2001
From: VIAUD Nathan <nathan.viaud@inria.fr>
Date: Wed, 19 Mar 2025 14:39:50 +0100
Subject: [PATCH 09/18] feat: translate adding en

---
 src/features/ePocFlow/nodes/ActivityNode.vue |   6 +-
 src/features/ePocFlow/nodes/ChapterNode.vue  |   9 +-
 src/i18n/config.ts                           |   4 +-
 src/i18n/en.ts                               | 322 +++++++++++++++++++
 src/i18n/fr.ts                               |  13 +
 src/main.ts                                  |  10 -
 src/shared/data/forms/badgeForm.data.ts      |   4 +-
 src/shared/data/forms/contentForm.data.ts    |  36 +--
 src/shared/data/forms/nodeForm.data.ts       |   2 +-
 src/shared/data/forms/questionsForm.data.ts  |   7 +-
 src/shared/services/graph/chapter.service.ts |  49 +--
 src/shared/utils/string.ts                   |   3 +
 12 files changed, 400 insertions(+), 65 deletions(-)
 create mode 100644 src/i18n/en.ts
 create mode 100644 src/shared/utils/string.ts

diff --git a/src/features/ePocFlow/nodes/ActivityNode.vue b/src/features/ePocFlow/nodes/ActivityNode.vue
index 8e41271b..eb29a66b 100644
--- a/src/features/ePocFlow/nodes/ActivityNode.vue
+++ b/src/features/ePocFlow/nodes/ActivityNode.vue
@@ -4,8 +4,10 @@ import { computed, ref } from 'vue';
 import { useEditorStore } from '@/src/shared/stores';
 import { getSelectedNodes } from '@/src/shared/services/graph';
 import { closeFormPanel, exitSelectNodeMode, getConnectedBadges, graphService } from '@/src/shared/services';
-
 import DraggableNode from '@/src/features/ePocFlow/nodes/content/DraggableNode.vue';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
 
 const editorStore = useEditorStore();
 
@@ -85,7 +87,7 @@ const activityIndex = computed(() => {
                 class="node-title"
                 :class="{ active: editorStore.openedElementId ? editorStore.openedElementId === props.id : false }"
             >
-                {{ currentNode.data.formValues?.title || 'Évaluation' }}
+                {{ currentNode.data.formValues?.title || t('forms.node.activity') }}
             </p>
             <Handle
                 :data-testid="`target-activity-${activityIndex}`"
diff --git a/src/features/ePocFlow/nodes/ChapterNode.vue b/src/features/ePocFlow/nodes/ChapterNode.vue
index e3746d85..40db90e7 100644
--- a/src/features/ePocFlow/nodes/ChapterNode.vue
+++ b/src/features/ePocFlow/nodes/ChapterNode.vue
@@ -5,6 +5,9 @@ import { Position } from '@vue-flow/core';
 import { computed } from 'vue';
 import ContentButton from '@/src/components/ContentButton.vue';
 import { closeFormPanel, exitSelectNodeMode, getConnectedBadges, graphService } from '@/src/shared/services';
+import { useI18n } from 'vue-i18n';
+
+const { t } = useI18n();
 
 const editorStore = useEditorStore();
 
@@ -16,15 +19,13 @@ const currentNode = findNode(props.id);
 
 const subtitle = computed(() => {
     const epocNode = findNode('1');
-    const chapterParameter = epocNode?.data?.formValues?.chapterParameter || 'Chapitre';
+    const chapterParameter = epocNode?.data?.formValues?.chapterParameter || t('global.chapter');
     const label = chapterParameter.length > 8 ? chapterParameter.substring(0, 7) + '...' : chapterParameter;
 
     return `${label} ${currentNode.data.index}`;
 });
 
-const isSource = computed(() =>
-    getConnectedEdges([currentNode], edges.value).some((edge) => edge.source === props.id),
-);
+const isSource = computed(() => getConnectedEdges([currentNode], edges.value).some((edge) => edge.source === props.id));
 
 const classList = {
     clickable: true,
diff --git a/src/i18n/config.ts b/src/i18n/config.ts
index 22660577..e547d5c5 100644
--- a/src/i18n/config.ts
+++ b/src/i18n/config.ts
@@ -1,10 +1,12 @@
 import { createI18n } from 'vue-i18n';
 import { frMessages } from './fr';
+import { enMessages } from './en';
 
 export const i18n = createI18n({
-    locale: 'fr',
+    locale: 'en',
     fallbackLocale: 'en',
     messages: {
         fr: frMessages,
+        en: enMessages,
     },
 });
diff --git a/src/i18n/en.ts b/src/i18n/en.ts
new file mode 100644
index 00000000..2f31507e
--- /dev/null
+++ b/src/i18n/en.ts
@@ -0,0 +1,322 @@
+export const enMessages = {
+    global: {
+        open: 'Open',
+        cancel: 'Cancel',
+        accept: 'Accept',
+        element: 'Element',
+        pleaseSelect: 'Please select',
+        and: 'and',
+        condition: 'Condition',
+        save: 'Save',
+        true: 'True',
+        false: 'False',
+        right: 'right',
+        left: 'left',
+        add: 'Add',
+        validate: 'Confirm',
+        delete: 'Delete',
+        conditions: 'Conditions',
+        label: 'Label',
+        chapter: 'Chapter',
+        objective: 'Objective',
+        name: 'Name',
+        file: 'File',
+    },
+    validation: {
+        yes: 'YES, DELETE',
+        no: 'NO, DO NOT DELETE',
+        confirm: 'Are you sure you want to delete this element?',
+    },
+    landing: {
+        published: 'This ePoc is a published version, you need to import it before editing here',
+        loadingPath: 'Loading {path}',
+        loadingEpoc: 'Loading ePoc',
+        open: 'Open existing project',
+        create: 'Create new project',
+        recents: 'Recent files',
+    },
+    editor: {
+        select: 'Click on the content element affected by the condition to select it',
+    },
+    badge: {
+        supported: 'Supported file: SVG',
+        select: 'Select a file',
+        conditions: 'Badge earning conditions',
+        add: 'Add a condition',
+        selectIcon: 'Select an icon',
+        defaultIcons: 'Default icons',
+        customIcons: 'Custom icons',
+        phrase: {
+            type: {
+                video: 'the video',
+                chapter: 'the chapter',
+                page: 'the page',
+                html: 'the text',
+                audio: 'the audio',
+                activity: 'the assessment',
+                question: 'the question',
+            },
+            verb: {
+                started: {
+                    true: 'Have started',
+                    false: 'Have not started',
+                },
+                completed: {
+                    true: 'Have completed',
+                    false: 'Have not completed',
+                },
+                viewed: {
+                    true: 'Have viewed',
+                    false: 'Have not viewed',
+                },
+                read: {
+                    true: 'Have read',
+                    false: 'Have not read',
+                },
+                played: {
+                    true: 'Have played',
+                    false: 'Have not played',
+                },
+                watched: {
+                    true: 'Have watched',
+                    false: 'Have not watched',
+                },
+                listened: {
+                    true: 'Have listened to',
+                    false: 'Have not listened to',
+                },
+                attempted: {
+                    true: 'Have attempted',
+                    false: 'Have not attempted',
+                },
+                passed: {
+                    true: 'Have passed',
+                    false: 'Have failed',
+                },
+                scored: 'Have scored at least',
+            },
+            scored: '{verb} {value} on',
+        },
+    },
+    inputs: {
+        manageConditions: 'Configure conditions',
+        updateIcon: 'Update icon',
+        leftChoice: 'Left choice',
+        rightChoice: 'Right choice',
+        accordion: 'Expand/Collapse',
+        accordionDescription: 'Expand/Collapse with title and content',
+    },
+    toast: {
+        modelSaved: 'Template saved 👌',
+        modelExists: 'Template already exists 🤔',
+    },
+    settings: {
+        title: 'Settings',
+        spellcheck: 'Enable spell checking',
+    },
+    models: {
+        title: 'Page templates',
+        empty: 'No page template has been created',
+        dragdrop: 'Drag/drop to add a template',
+        model: 'Template',
+    },
+    header: {
+        undo: 'Undo',
+        redo: 'Redo',
+        preview: 'Preview',
+        publish: 'Publish',
+        adjust: 'Adjust',
+        never: 'never',
+        lastSave: 'Last save:',
+        new: 'New ePoc',
+    },
+    forms: {
+        type: 'Type here...',
+        badge: {
+            updateIcon: 'Update icon',
+            obtention: 'Badge earning conditions',
+            presentation: 'Badge presentation',
+            presentationPlaceholder: 'Enter badge presentation',
+            icon: 'Badge icon',
+        },
+        content: {
+            text: 'Content',
+            summary: 'Summary',
+            video: {
+                label: 'Video',
+                placeholder: 'Add a video',
+                hint: 'Recommended format:',
+            },
+            audio: {
+                label: 'Audio track',
+                placeholder: 'Add an audio track',
+                hint: 'Recommended format: MP3',
+            },
+            subtitle: {
+                label: 'Language name',
+                code: 'Language code',
+                placeholder: 'Add subtitles',
+                hint: 'Accepted extensions: {extensions}',
+            },
+            transcription: {
+                label: 'Transcription',
+                placeholder: 'Add a transcription',
+                hint: 'Accepted extensions: {extensions} <br>For users who do not wish or are unable to watch the video',
+            },
+            thumbnail: {
+                label: 'Thumbnail',
+                placeholder: 'Add a thumbnail',
+                hint: 'Recommended format: same as video',
+            },
+        },
+        buttons: {
+            addBadge: 'Add badge',
+            saveModel: 'Save template',
+            duplicateEvaluation: 'Duplicate assessment',
+            duplicatePage: 'Duplicate page',
+            duplicateElement: 'Duplicate element',
+            backToPage: 'Back to page',
+            backToEpoc: 'Back to ePoc',
+        },
+        node: {
+            conditionPlaceholder: 'Enter condition {condition}...',
+            choice: 'Choice',
+            course: 'Course {course}',
+            conditional: 'Conditional content',
+            title: 'Title',
+            subtitle: 'Subtitle',
+            duration: 'Duration (in minutes)',
+            objectives: 'Learning objectives',
+            about: 'About the ePoc',
+            cover: {
+                title: 'Cover image',
+                placeholder: 'Add a cover image',
+                hint: 'Recommended format: square (180x180)<br> Image visible in the ePoc list',
+            },
+            teaser: {
+                title: 'Video teaser',
+                placeholder: 'Add a teaser',
+                hint: 'Recommended format: 16:9<br> Video visible on the ePoc presentation page',
+            },
+            thumbnail: {
+                title: 'Video thumbnail',
+                hint: 'Recommended format: same as video <br> Image visible on the ePoc presentation page',
+            },
+            presentation: 'Presentation',
+            edition: 'Edition',
+            author: {
+                title: 'Author | Authors',
+                placeholder: 'Jane Doe',
+                image: {
+                    title: 'Author image',
+                    placeholder: 'Add an image',
+                    hint: 'Recommended format: square (100x100)<br> Image visible on the ePoc presentation page',
+                },
+                position: {
+                    title: 'Position',
+                    placeholder: 'Researcher at Inria',
+                    hint: 'Profession, position, affiliation...',
+                },
+                biography: 'Short author biography',
+            },
+            certificateBadge: 'Number of badges required for certification',
+            certificateScore: 'Score required for certification',
+            certificateScoreHint: 'Not considered if the number of badges required for certification is greater than 0',
+            chapterLabel: 'Chapter label',
+            chapterDuration: 'Chapter duration (in minutes)',
+            plugin: {
+                title: 'Plugin | Plugins',
+                script: 'Script file',
+                scriptPlaceholder: 'Add a script file',
+                template: 'Plugin HTML template',
+                templatePlaceholder: 'Add HTML template',
+            },
+            licence: {
+                title: 'License',
+                hint: 'Name of your ePoc content license',
+                url: 'URL',
+                urlPlaceholder: 'https://creativecommons.org/licenses/by/4.0/deed',
+                urlHint: 'Full text of the chosen license',
+            },
+            page: {
+                title: 'Page',
+                hidden: 'Hidden in table of contents',
+                conditional: 'Only displays under certain conditions',
+                conditionalHint: 'Option used for conditional display',
+                components: 'Components',
+            },
+            activity: 'Assessment',
+        },
+    },
+    questions: {
+        configuration: 'Assessment configuration',
+        question: 'Question',
+        askQuestion: 'Ask the question',
+        instruction: 'Instruction',
+        instructionPlaceholder: 'Instruction to answer the question',
+        explanation: 'Explanation',
+        typeExplanation: 'Enter an explanation',
+        addExplanation: 'Add an explanation',
+        response: 'Answer',
+        responses: 'Answers',
+        typeResponse: 'Enter an answer',
+        correctResponse: 'Correct answer',
+        categories: 'Proposed answer categories',
+        category: 'Category',
+        typeCategory: 'Enter a category title',
+        proposedChoices: 'Proposed choice categories',
+        proposedResponses: 'Proposed answers',
+        cards: 'Cards',
+        card: 'Card',
+        typeProposition: 'Enter a proposition',
+        template: {
+            title: 'Template',
+            select: 'Select a template',
+            data: 'Data',
+            key: 'Key',
+            value: 'Value',
+        },
+        types: {
+            qcm: 'MCQ',
+            dragDrop: 'Drag & Drop',
+            reorder: 'Reorder',
+            swipe: 'Swipe',
+            dropdownList: 'Dropdown list',
+            custom: 'Custom question',
+        },
+        score: 'Score',
+    },
+    sidebar: {
+        content: {
+            text: 'Text',
+            video: 'Video',
+            audio: 'Audio',
+            textTooltip: 'Drag/drop to add text',
+            videoTooltip: 'Drag/drop to add video',
+            audioTooltip: 'Drag/drop to add audio',
+        },
+        pages: {
+            question: 'Question',
+            conditions: 'Conditions',
+            conditionsLegacy: 'Conditions (legacy)',
+            model: 'Template',
+            badge: 'Badge',
+            questionTooltip: 'Click to add a question',
+            conditionsTooltip: 'Drag/drop to add a condition',
+            modelTooltip: 'Click to open template menu',
+            badgeTooltip: 'Click to open badge menu',
+        },
+    },
+    verbs: {
+        started: 'Started',
+        completed: 'Completed',
+        viewed: 'Viewed',
+        read: 'Read',
+        played: 'Played',
+        watched: 'Watched',
+        listened: 'Listened',
+        attempted: 'Attempted',
+        scored: 'Scored',
+        passed: 'Passed',
+    },
+};
diff --git a/src/i18n/fr.ts b/src/i18n/fr.ts
index 977cb6b9..d315c3d1 100644
--- a/src/i18n/fr.ts
+++ b/src/i18n/fr.ts
@@ -20,6 +20,7 @@ export const frMessages = {
         chapter: 'Chapitre',
         objective: 'Objectif',
         name: 'Nom',
+        file: 'Fichier',
     },
     validation: {
         yes: 'OUI, SUPPRIMER',
@@ -137,6 +138,7 @@ export const frMessages = {
             obtention: "Conditions d'obtention du badge",
             presentation: 'Présentation du badge',
             presentationPlaceholder: 'Saisissez une présentation du badge',
+            icon: 'Icône du badge',
         },
         content: {
             text: 'Contenu',
@@ -146,6 +148,17 @@ export const frMessages = {
                 placeholder: 'Ajouter une vidéo',
                 hint: 'Format recommandé :',
             },
+            audio: {
+                label: 'Piste audio',
+                placeholder: 'Ajouter une piste audio',
+                hint: 'Format recommandé : MP3',
+            },
+            subtitle: {
+                label: 'Nom de la langue',
+                code: 'Code de la langue',
+                placeholder: 'Ajouter des sous-titre',
+                hint: 'Extensions acceptées : {extensions}',
+            },
             transcription: {
                 label: 'Transcription',
                 placeholder: 'Ajouter une transcription',
diff --git a/src/main.ts b/src/main.ts
index e4170c16..17fe8774 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -7,16 +7,6 @@ import VueTippy from 'vue-tippy';
 import 'tippy.js/dist/tippy.css';
 import draggable from 'vuedraggable';
 import { i18n } from './i18n/config';
-// import { createI18n } from 'vue-i18n';
-//
-
-// const i18n = createI18n({
-//     locale: 'fr',
-//     fallbackLocale: 'fr',
-//     messages: {
-//         fr: frMessages,
-//     },
-// });
 
 const app = createApp(App);
 app.use(router);
diff --git a/src/shared/data/forms/badgeForm.data.ts b/src/shared/data/forms/badgeForm.data.ts
index b6edd7ac..b51a952b 100644
--- a/src/shared/data/forms/badgeForm.data.ts
+++ b/src/shared/data/forms/badgeForm.data.ts
@@ -15,14 +15,14 @@ export const customBadgeForm: Form = {
                 {
                     id: 'title',
                     type: 'text',
-                    label: 'Titre',
+                    label: t('forms.node.title'),
                     value: '',
                     placeholder: t('forms.type'),
                 },
                 {
                     id: 'icon',
                     type: 'icon-picker',
-                    label: 'Icône du badge',
+                    label: t('forms.badge.icon'),
                     value: '',
                     placeholder: t('forms.badge.updateIcon'),
                 },
diff --git a/src/shared/data/forms/contentForm.data.ts b/src/shared/data/forms/contentForm.data.ts
index afd08571..2e64b44a 100644
--- a/src/shared/data/forms/contentForm.data.ts
+++ b/src/shared/data/forms/contentForm.data.ts
@@ -69,36 +69,36 @@ export const videoForm: Form = {
             ],
         },
         {
-            name: 'Sous-titres',
+            name: t('forms.node.subtitle'),
             inputs: [
                 {
                     id: 'subtitles',
-                    label: 'Sous-titres',
+                    label: t('forms.node.subtitle'),
                     type: 'repeat',
                     value: [],
                     inputs: [
                         {
                             id: 'label',
                             type: 'text',
-                            label: 'Nom de la langue',
+                            label: t('forms.content.subtitle.label'),
                             value: '',
                             placeholder: 'English',
                         },
                         {
                             id: 'lang',
                             type: 'text',
-                            label: 'Code de langue',
+                            label: t('forms.content.subtitle.code'),
                             value: '',
                             placeholder: 'en',
                         },
                         {
                             id: 'src',
                             type: 'file',
-                            label: 'Fichier',
+                            label: t('global.file'),
                             value: '',
-                            placeholder: 'Ajouter des sous-titres',
+                            placeholder: t('forms.content.subtitle.placeholder'),
                             accept: '.vtt',
-                            hint: 'Extensions acceptées : .vtt',
+                            hint: t('forms.content.subtitle.hint', { extensions: '.vtt' }),
                         },
                     ],
                 },
@@ -109,7 +109,7 @@ export const videoForm: Form = {
 
 export const audioForm: Form = {
     type: 'audio',
-    name: 'Audio',
+    name: t('forms.content.audio.label'),
     icon: 'icon-audio',
     buttons: contentButtons,
     fields: [
@@ -118,35 +118,35 @@ export const audioForm: Form = {
                 {
                     id: 'source',
                     type: 'file',
-                    label: 'Piste audio',
-                    placeholder: 'Ajouter une piste audio',
+                    label: t('forms.content.audio.label'),
+                    placeholder: t('forms.content.audio.placeholder'),
                     value: '',
                     accept: '.mp3',
                 },
                 {
                     id: 'summary',
                     type: 'html',
-                    label: 'Résumé',
+                    label: t('forms.content.summary'),
                     value: '',
-                    placeholder: 'Saisissez...',
+                    placeholder: t('forms.type'),
                 },
                 {
                     id: 'transcript',
                     type: 'file',
-                    label: 'Transcription',
+                    label: t('forms.content.transcription.label'),
+                    placeholder: t('forms.content.transcription.placeholder'),
                     value: '',
-                    placeholder: 'Ajouter une transcription',
                     accept: '.txt,.vtt',
-                    hint: "Extensions acceptées : .vtt, .txt <br>Pour les utilisateurs qui ne souhaitent pas ou ne sont pas en capacité d'écouter la piste audio",
+                    hint: t('forms.content.transcription.hint', { extensions: '.txt,.vtt' }),
                 },
                 {
                     id: 'subtitles',
                     type: 'file',
-                    label: 'Sous-titres',
+                    label: t('forms.content.subtitle.label'),
                     value: '',
-                    placeholder: 'Ajouter des sous-titres',
+                    placeholder: t('forms.content.subtitle.placeholder'),
                     accept: '.vtt',
-                    hint: 'Extensions acceptées: .vtt',
+                    hint: t('forms.content.subtitle.hint', { extensions: '.vtt' }),
                 },
             ],
         },
diff --git a/src/shared/data/forms/nodeForm.data.ts b/src/shared/data/forms/nodeForm.data.ts
index ed1171cb..ec796cf5 100644
--- a/src/shared/data/forms/nodeForm.data.ts
+++ b/src/shared/data/forms/nodeForm.data.ts
@@ -184,7 +184,7 @@ export const epocForm: Form = {
                     type: 'file',
                     label: t('forms.node.thumbnail.title'),
                     value: '',
-                    placeholder: t('content.thumbnail.placeholder'),
+                    placeholder: t('forms.content.thumbnail.placeholder'),
                     accept: '.png,.jpg,.jpeg,.gif,.bmp,.svg,.webp',
                     hint: t('forms.node.thumbnail.hint'),
                 },
diff --git a/src/shared/data/forms/questionsForm.data.ts b/src/shared/data/forms/questionsForm.data.ts
index 16543d50..f04c2341 100644
--- a/src/shared/data/forms/questionsForm.data.ts
+++ b/src/shared/data/forms/questionsForm.data.ts
@@ -1,6 +1,7 @@
 import { Form } from '@/src/shared/interfaces';
 import { contentButtons } from './formButtons.data';
 import { i18n } from '@/src/i18n/config';
+import { capitalizeFirstLetter } from '../../utils/string';
 
 const { t } = i18n.global;
 
@@ -345,9 +346,9 @@ export const swipeForm: Form = {
             inputs: [
                 {
                     id: 'categories',
-                    label: t('questions.choice'),
+                    label: t('forms.node.choice'),
                     type: 'repeat',
-                    value: ['Droite', 'Gauche'],
+                    value: [capitalizeFirstLetter(t('global.right')), capitalizeFirstLetter(t('global.left'))],
                     addButton: false,
                     inputs: [
                         {
@@ -463,7 +464,7 @@ export const listForm: Form = {
             inputs: [
                 {
                     id: 'categories',
-                    label: t('questions.choice'),
+                    label: t('forms.node.choice'),
                     type: 'repeat',
                     value: [],
                     inputs: [
diff --git a/src/shared/services/graph/chapter.service.ts b/src/shared/services/graph/chapter.service.ts
index ec165cde..e1a013b9 100644
--- a/src/shared/services/graph/chapter.service.ts
+++ b/src/shared/services/graph/chapter.service.ts
@@ -2,6 +2,9 @@ import { Node, useVueFlow } from '@vue-flow/core';
 import { Chapter } from '@epoc/epoc-types/src/v1';
 import { generateContentId, generateId, graphService } from '@/src/shared/services';
 const { nodes, findNode, addNodes } = useVueFlow('main');
+import { i18n } from '@/src/i18n/config';
+
+const { t } = i18n.global;
 
 /**
  * Add a new chapter to the graph
@@ -16,7 +19,7 @@ export function addChapter(chapterId?: string, chapter?: Chapter, offsetY?: numb
         action: { icon: 'icon-chapitre', type: 'chapter' },
         formType: 'chapter',
         formValues: {},
-        title: 'Chapitre ' + (chapters.length + 1),
+        title: t('global.chapter') + (chapters.length + 1),
         contentId: generateContentId(),
         index: chapters.length + 1,
     };
@@ -51,8 +54,8 @@ export function addChapter(chapterId?: string, chapter?: Chapter, offsetY?: numb
 export function updateNextChapters(chapterId: string): void {
     const nextChapters = getNextChapters(chapterId);
 
-    for(const chapter of nextChapters) {
-        chapter.data.index --;
+    for (const chapter of nextChapters) {
+        chapter.data.index--;
     }
 }
 
@@ -64,7 +67,7 @@ export function getPreviousChapters(id: string) {
     const chapter = findNode(id);
     const chapters = nodes.value.filter((node) => node.type === 'chapter');
 
-    if(chapter.data.index === 1) return [];
+    if (chapter.data.index === 1) return [];
     else return chapters.filter((c) => c.data.index < chapter.data.index).sort((a, b) => a.data.index - b.data.index);
 }
 
@@ -76,7 +79,7 @@ export function getNextChapters(id: string) {
     const chapter = findNode(id);
     const chapters = nodes.value.filter((node) => node.type === 'chapter');
 
-    if(chapter.data.index === chapters.length) return [];
+    if (chapter.data.index === chapters.length) return [];
     else return chapters.filter((c) => c.data.index > chapter.data.index).sort((a, b) => a.data.index - b.data.index);
 }
 
@@ -89,7 +92,7 @@ export function getPreviousChapter(id: string) {
     const chapter = findNode(id);
     const chapters = nodes.value.filter((node) => node.type === 'chapter');
 
-    if(chapter.data.index === 1) return null;
+    if (chapter.data.index === 1) return null;
     else return chapters.find((c) => c.data.index === chapter.data.index - 1);
 }
 
@@ -97,11 +100,10 @@ export function getNextChapter(id: string) {
     const chapter = findNode(id);
     const chapters = nodes.value.filter((node) => node.type === 'chapter');
 
-    if(chapter.data.index === chapters.length) return null;
+    if (chapter.data.index === chapters.length) return null;
     else return chapters.find((c) => c.data.index === chapter.data.index + 1);
 }
 
-
 /**
  * Swap the chapter with the previous chapter
  * @param {string} id
@@ -111,13 +113,13 @@ export function swapChapterWithPrevious(id: string) {
     const chapter = findNode(id);
     const previousChapter = getPreviousChapter(id);
 
-    if(!previousChapter) return;
+    if (!previousChapter) return;
 
     moveChapterContents(chapter.id, previousChapter.position.y - chapter.position.y);
     moveChapterContents(previousChapter.id, chapter.position.y - previousChapter.position.y);
 
-    chapter.data.index --;
-    previousChapter.data.index ++;
+    chapter.data.index--;
+    previousChapter.data.index++;
 
     const tmpY = chapter.position.y;
     chapter.position.y = previousChapter.position.y;
@@ -132,20 +134,19 @@ export function swapChapterWithNext(id: string) {
     const chapter = findNode(id);
     const nextChapter = getNextChapter(id);
 
-    if(!nextChapter) return;
+    if (!nextChapter) return;
 
     moveChapterContents(chapter.id, nextChapter.position.y - chapter.position.y);
     moveChapterContents(nextChapter.id, chapter.position.y - nextChapter.position.y);
 
-    chapter.data.index ++;
-    nextChapter.data.index --;
+    chapter.data.index++;
+    nextChapter.data.index--;
 
     const tmpY = chapter.position.y;
     chapter.position.y = nextChapter.position.y;
     nextChapter.position.y = tmpY;
 }
 
-
 /**
  * Manage the dragging of a chapter by keeping the others at the MIN_DISTANCE between each others
  * @param {string} id
@@ -161,29 +162,29 @@ export function handleChapterDrag(id: string) {
     const nextChapters = getNextChapters(id);
 
     // Assuring the position of the chapter is valid
-    if(chapter.position.x !== 0) chapter.position.x = 0;
+    if (chapter.position.x !== 0) chapter.position.x = 0;
     if (chapter.position.y < minY) chapter.position.y = minY;
 
     // Push up all the chapters after this chapter
-    for(let i = 0; i < previousChapters.length; i++) {
+    for (let i = 0; i < previousChapters.length; i++) {
         const c = previousChapters[i];
 
-        if(c.position.x !== 0 ) c.position.x = 0;
+        if (c.position.x !== 0) c.position.x = 0;
 
         // Get the min distance for the current c
         const currentMinY = chapter.position.y - MIN_DISTANCE * (previousChapters.length - i);
-        if(c.position.y > currentMinY) c.position.y = currentMinY;
+        if (c.position.y > currentMinY) c.position.y = currentMinY;
     }
 
     // Push down all the chapters below this chapter
-    for(let i = 0; i < nextChapters.length; i++) {
+    for (let i = 0; i < nextChapters.length; i++) {
         const c = nextChapters[i];
 
-        if(c.position.x !== 0 ) c.position.x = 0;
+        if (c.position.x !== 0) c.position.x = 0;
 
         // Get the max distance for the current c
-        const maxY = chapter.position.y  + MIN_DISTANCE * (i + 1);
-        if(c.position.y < maxY) c.position.y = maxY;
+        const maxY = chapter.position.y + MIN_DISTANCE * (i + 1);
+        if (c.position.y < maxY) c.position.y = maxY;
     }
 }
 
@@ -196,7 +197,7 @@ export function moveChapterContents(chapterId: string, offsetY: number) {
     const chapter = findNode(chapterId);
 
     let nextNode = graphService.getNextNode(chapter);
-    while(nextNode) {
+    while (nextNode) {
         nextNode.position.y += offsetY;
         nextNode = graphService.getNextNode(nextNode);
     }
diff --git a/src/shared/utils/string.ts b/src/shared/utils/string.ts
new file mode 100644
index 00000000..4a515878
--- /dev/null
+++ b/src/shared/utils/string.ts
@@ -0,0 +1,3 @@
+export function capitalizeFirstLetter(val) {
+    return String(val).charAt(0).toUpperCase() + String(val).slice(1);
+}
-- 
GitLab


From f53f116b41b724f0ab178bb46f107e6fc53736ad Mon Sep 17 00:00:00 2001
From: VIAUD Nathan <nathan.viaud@inria.fr>
Date: Thu, 20 Mar 2025 16:54:27 +0100
Subject: [PATCH 10/18] WIP: add lang selector in settings

---
 src/components/ui/UiSelect.vue                | 42 +++++++++++++++++
 .../inputs/card/components/SelectInput.vue    | 47 ++++---------------
 src/features/settings/SettingsInput.vue       | 18 ++++---
 src/features/settings/SettingsModal.vue       | 12 +++++
 src/i18n/en.ts                                |  1 +
 src/i18n/fr.ts                                |  1 +
 6 files changed, 76 insertions(+), 45 deletions(-)
 create mode 100644 src/components/ui/UiSelect.vue

diff --git a/src/components/ui/UiSelect.vue b/src/components/ui/UiSelect.vue
new file mode 100644
index 00000000..4d110cd2
--- /dev/null
+++ b/src/components/ui/UiSelect.vue
@@ -0,0 +1,42 @@
+<script setup lang="ts">
+import { useVModel } from '@vueuse/core';
+
+const props = defineProps<{
+    id: string;
+    modelValue: string;
+    options: { value: string | number; label: string }[];
+}>();
+
+const emit = defineEmits<{
+    (e: 'update:modelValue', value: string);
+    (e: 'change');
+}>();
+
+const data = useVModel(props, 'modelValue', emit);
+</script>
+
+<template>
+    <select :id="id" v-model="data" @change="handleChange">
+        <option v-for="option in options" :key="option.value" :value="option.value">
+            {{ option.label }}
+        </option>
+    </select>
+</template>
+
+<style scoped lang="scss">
+select {
+    appearance: none;
+    padding: 0.5rem;
+    padding-right: 2rem;
+    border: 1px solid var(--border);
+    border-radius: 4px;
+    background-color: var(--item-background);
+    cursor: pointer;
+    font-size: 1rem;
+    color: var(--text);
+    background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgd2lkdGg9IjExcHgiIGhlaWdodD0iN3B4IiB2aWV3Qm94PSIwIDAgMTEuMCA3LjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxkZWZzPjxjbGlwUGF0aCBpZD0iaTAiPjxwYXRoIGQ9Ik0yNDE4LDAgTDI0MTgsMjQyNiBMMCwyNDI2IEwwLDAgTDI0MTgsMCBaIj48L3BhdGg+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImkxIj48cGF0aCBkPSJNOS4yMDE3MjIyNywwIEM5LjYxNjUwNDA2LDAgOS45OTA4OTcyMSwwLjI3MzU1MDk4NyAxMC4xNDk4ODU5LDAuNjk0MzM1OTM3IEMxMC4zMDg4NzQ2LDEuMTE1MTIwODkgMTAuMjQ5ODk0OSwxLjU5OTYwOTM3IDkuOTU0OTk2NjMsMS45MTk1MzE0NiBMNS44ODA5MDQ1NSw2LjQxOTUzMTQ2IEM1LjY1MzMxOTM0LDYuNjQxMDE1NDEgNS4zOTA0NzQ3Niw2Ljc1IDUuMTI3NjMwMTksNi43NSBDNC44NjQ3ODU2Miw2Ljc1IDQuNjAyNTgxNzgsNi42NDAxMzY3MiA0LjQwMjI0Mjg2LDYuNDIwNDEwMTYgTDAuMzI4MTUwMjkxLDEuOTIwNDEwMTYgQzAuMDA2MTQ2NTU1MTksMS41OTk2MDkzNyAtMC4wODE2OTQ5MjIzLDEuMTE0NDUzMDIgMC4wNzcxMDE1NTQ3LDAuNjk2MDkzODU3IEMwLjIzNTg5ODAzMiwwLjI3NzczNDY5NyAwLjYxMDIyNzEwNiwwIDEuMDI0Njg4NTMsMCBMOS4yMDE3MjIyNywwIFoiPjwvcGF0aD48L2NsaXBQYXRoPjwvZGVmcz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTMwNC4wIC0xMDYyLjApIj48ZyBjbGlwLXBhdGg9InVybCgjaTApIj48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg5ODAuMCA5MC4wKSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjAuMCA3MjkuMCkiPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAuMCA1MS4wKSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTUuMCAxNTAuMCkiPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAuMCAyNC4wKSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjg5LjM3MjM2OTgwNjgwMSAxOC4wKSI+PGcgY2xpcC1wYXRoPSJ1cmwoI2kxKSI+PHBvbHlnb24gcG9pbnRzPSItMS4xMTAyMjMwMmUtMTYsMCAxMC4yMzYwMTA0LDAgMTAuMjM2MDEwNCw2Ljc1IC0xLjExMDIyMzAyZS0xNiw2Ljc1IC0xLjExMDIyMzAyZS0xNiwwIiBzdHJva2U9Im5vbmUiIGZpbGw9IiMzNTQyNTgiPjwvcG9seWdvbj48L2c+PC9nPjwvZz48L2c+PC9nPjwvZz48L2c+PC9nPjwvZz48L3N2Zz4=');
+    background-repeat: no-repeat;
+    background-position: right 0.7rem top 50%;
+    background-size: 0.8rem auto;
+}
+</style>
diff --git a/src/features/forms/components/inputs/card/components/SelectInput.vue b/src/features/forms/components/inputs/card/components/SelectInput.vue
index 6ec9d6c2..14cef722 100644
--- a/src/features/forms/components/inputs/card/components/SelectInput.vue
+++ b/src/features/forms/components/inputs/card/components/SelectInput.vue
@@ -1,6 +1,8 @@
 <script setup lang="ts">
+import UiSelect from '@/src/components/ui/UiSelect.vue';
 import { getCurrentState } from '@/src/shared/services/undoRedo.service';
 import { useEditorStore } from '@/src/shared/stores';
+import { ref, watch, computed } from 'vue';
 
 const editorStore = useEditorStore();
 
@@ -51,47 +53,16 @@ function walkObjectPath(object: any, path: string) {
     }
 }
 
-function onChange(event: Event) {
-    const target = event.target as HTMLInputElement;
-    const value = target.value;
+const input = ref(props.inputValue);
+watch(input, (newValue) => {
     const state = getCurrentState(true);
-
-    emit('change', value);
+    emit('change', newValue);
     emit('saveGivenState', state);
-}
+});
+
+const items = computed(() => [...getOptions().map((item: string) => ({ value: item, label: item }))]);
 </script>
 
 <template>
-    <div class="select">
-        <select :id="id" :value="inputValue" class="select-box" @change="onChange">
-            <option value="">{{ $t('global.pleaseSelect') }}</option>
-            <option v-for="(option, index) in getOptions()" :key="index" :value="option">{{ option }}</option>
-        </select>
-    </div>
+    <UiSelect v-model="input" :options="items" />
 </template>
-
-<style scoped lang="scss">
-.select {
-    display: flex;
-    flex-direction: column;
-    margin: 1rem 0 0.5rem 0;
-    label {
-        margin-bottom: 0.5rem;
-    }
-    select {
-        appearance: none;
-        padding: 0.5rem;
-        border: 1px solid var(--border);
-        border-radius: 4px;
-        background-color: var(--item-background);
-        cursor: pointer;
-        font-size: 1rem;
-        color: var(--text);
-
-        background-image: url('data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c3ZnIHZlcnNpb249IjEuMSIgd2lkdGg9IjExcHgiIGhlaWdodD0iN3B4IiB2aWV3Qm94PSIwIDAgMTEuMCA3LjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiPjxkZWZzPjxjbGlwUGF0aCBpZD0iaTAiPjxwYXRoIGQ9Ik0yNDE4LDAgTDI0MTgsMjQyNiBMMCwyNDI2IEwwLDAgTDI0MTgsMCBaIj48L3BhdGg+PC9jbGlwUGF0aD48Y2xpcFBhdGggaWQ9ImkxIj48cGF0aCBkPSJNOS4yMDE3MjIyNywwIEM5LjYxNjUwNDA2LDAgOS45OTA4OTcyMSwwLjI3MzU1MDk4NyAxMC4xNDk4ODU5LDAuNjk0MzM1OTM3IEMxMC4zMDg4NzQ2LDEuMTE1MTIwODkgMTAuMjQ5ODk0OSwxLjU5OTYwOTM3IDkuOTU0OTk2NjMsMS45MTk1MzE0NiBMNS44ODA5MDQ1NSw2LjQxOTUzMTQ2IEM1LjY1MzMxOTM0LDYuNjQxMDE1NDEgNS4zOTA0NzQ3Niw2Ljc1IDUuMTI3NjMwMTksNi43NSBDNC44NjQ3ODU2Miw2Ljc1IDQuNjAyNTgxNzgsNi42NDAxMzY3MiA0LjQwMjI0Mjg2LDYuNDIwNDEwMTYgTDAuMzI4MTUwMjkxLDEuOTIwNDEwMTYgQzAuMDA2MTQ2NTU1MTksMS41OTk2MDkzNyAtMC4wODE2OTQ5MjIzLDEuMTE0NDUzMDIgMC4wNzcxMDE1NTQ3LDAuNjk2MDkzODU3IEMwLjIzNTg5ODAzMiwwLjI3NzczNDY5NyAwLjYxMDIyNzEwNiwwIDEuMDI0Njg4NTMsMCBMOS4yMDE3MjIyNywwIFoiPjwvcGF0aD48L2NsaXBQYXRoPjwvZGVmcz48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSgtMTMwNC4wIC0xMDYyLjApIj48ZyBjbGlwLXBhdGg9InVybCgjaTApIj48ZyB0cmFuc2Zvcm09InRyYW5zbGF0ZSg5ODAuMCA5MC4wKSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjAuMCA3MjkuMCkiPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAuMCA1MS4wKSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMTUuMCAxNTAuMCkiPjxnIHRyYW5zZm9ybT0idHJhbnNsYXRlKDAuMCAyNC4wKSI+PGcgdHJhbnNmb3JtPSJ0cmFuc2xhdGUoMjg5LjM3MjM2OTgwNjgwMSAxOC4wKSI+PGcgY2xpcC1wYXRoPSJ1cmwoI2kxKSI+PHBvbHlnb24gcG9pbnRzPSItMS4xMTAyMjMwMmUtMTYsMCAxMC4yMzYwMTA0LDAgMTAuMjM2MDEwNCw2Ljc1IC0xLjExMDIyMzAyZS0xNiw2Ljc1IC0xLjExMDIyMzAyZS0xNiwwIiBzdHJva2U9Im5vbmUiIGZpbGw9IiMzNTQyNTgiPjwvcG9seWdvbj48L2c+PC9nPjwvZz48L2c+PC9nPjwvZz48L2c+PC9nPjwvZz48L3N2Zz4=');
-        background-repeat: no-repeat;
-        background-position: right 0.7rem top 50%;
-        background-size: 0.8rem auto;
-    }
-}
-</style>
diff --git a/src/features/settings/SettingsInput.vue b/src/features/settings/SettingsInput.vue
index d391601d..259fd89d 100644
--- a/src/features/settings/SettingsInput.vue
+++ b/src/features/settings/SettingsInput.vue
@@ -1,11 +1,16 @@
 <script setup lang="ts">
-import ToggleInput from "./ToggleInput.vue";
+import ToggleInput from './ToggleInput.vue';
 import { useVModel } from '@vueuse/core';
+import UiSelect from '@/src/components/ui/UiSelect.vue';
 
 const props = defineProps<{
-    type: 'toggle';
+    type: 'toggle' | 'select';
     label: string;
-    modelValue: boolean;
+    modelValue: boolean | string;
+    options?: {
+        label: string;
+        value: string;
+    }[];
 }>();
 
 const emit = defineEmits<{
@@ -14,19 +19,18 @@ const emit = defineEmits<{
 
 const data = useVModel(props, 'modelValue', emit);
 
-const id = "id" + Math.random().toString(16).slice(2)
-
+const id = 'id' + Math.random().toString(16).slice(2);
 </script>
 
 <template>
     <div class="settings-input">
         <label :for="id">{{ label }}</label>
-        <ToggleInput :id="id" v-model="data" />
+        <ToggleInput v-if="type === 'toggle'" :id="id" v-model="data" />
+        <UiSelect v-else-if="type === 'select'" :id="id" v-model="data" :options="options" />
     </div>
 </template>
 
 <style scoped lang="scss">
-
 .settings-input {
     display: flex;
     align-items: center;
diff --git a/src/features/settings/SettingsModal.vue b/src/features/settings/SettingsModal.vue
index 1ddf1f1c..19209bfd 100644
--- a/src/features/settings/SettingsModal.vue
+++ b/src/features/settings/SettingsModal.vue
@@ -3,6 +3,9 @@ import Modal from '@/src/components/LayoutModal.vue';
 import SettingsInput from './SettingsInput.vue';
 import { ref, onMounted, watch } from 'vue';
 import { useSettingsStore } from '@/src/shared/stores';
+import { useI18n } from 'vue-i18n';
+
+const { locale } = useI18n();
 
 const modal = ref(null);
 const settingsStore = useSettingsStore();
@@ -41,6 +44,15 @@ defineExpose({
 
         <div class="settings">
             <SettingsInput v-model="spellcheck" type="toggle" :label="$t('settings.spellcheck')" />
+            <SettingsInput
+                v-model="locale"
+                type="select"
+                :label="$t('settings.lang')"
+                :options="[
+                    { label: 'English', value: 'en' },
+                    { label: 'Français', value: 'fr' },
+                ]"
+            />
         </div>
 
         <template #footer>
diff --git a/src/i18n/en.ts b/src/i18n/en.ts
index 2f31507e..f022404d 100644
--- a/src/i18n/en.ts
+++ b/src/i18n/en.ts
@@ -113,6 +113,7 @@ export const enMessages = {
     settings: {
         title: 'Settings',
         spellcheck: 'Enable spell checking',
+        lang: 'Language',
     },
     models: {
         title: 'Page templates',
diff --git a/src/i18n/fr.ts b/src/i18n/fr.ts
index d315c3d1..fddafbd6 100644
--- a/src/i18n/fr.ts
+++ b/src/i18n/fr.ts
@@ -113,6 +113,7 @@ export const frMessages = {
     settings: {
         title: 'Paramètres',
         spellcheck: 'Activer la vérification orthographique',
+        lang: 'Langue',
     },
     models: {
         title: 'Modèles de page',
-- 
GitLab


From f969b5a8c01a86a409ba2b7045494ac524bf6fc0 Mon Sep 17 00:00:00 2001
From: VIAUD Nathan <nathan.viaud@inria.fr>
Date: Thu, 20 Mar 2025 17:03:42 +0100
Subject: [PATCH 11/18] WIP: lang select save data in electron store

---
 src/shared/interfaces/settings.interface.ts |  1 +
 src/shared/services/editor.service.ts       |  5 ++---
 src/shared/stores/settingsStore.ts          | 19 ++++++++++++++-----
 3 files changed, 17 insertions(+), 8 deletions(-)

diff --git a/src/shared/interfaces/settings.interface.ts b/src/shared/interfaces/settings.interface.ts
index c7b59d43..7b4a70bf 100644
--- a/src/shared/interfaces/settings.interface.ts
+++ b/src/shared/interfaces/settings.interface.ts
@@ -1,3 +1,4 @@
 export interface Settings {
     spellcheck: boolean;
+    locale: string;
 }
diff --git a/src/shared/services/editor.service.ts b/src/shared/services/editor.service.ts
index 7d3bf840..b551e1fc 100644
--- a/src/shared/services/editor.service.ts
+++ b/src/shared/services/editor.service.ts
@@ -157,9 +157,8 @@ const setup = function () {
     api.receive('settings', (data: string) => {
         const { settings } = JSON.parse(data);
         const settingsStore = useSettingsStore();
-        settingsStore.settings = {
-            spellcheck: settings?.spellcheck === undefined ? true : settings.spellcheck,
-        };
+
+        settingsStore.initSettings(settings);
     });
 
     // Adding the version to the editorStore
diff --git a/src/shared/stores/settingsStore.ts b/src/shared/stores/settingsStore.ts
index 43a720fc..31ec72d6 100644
--- a/src/shared/stores/settingsStore.ts
+++ b/src/shared/stores/settingsStore.ts
@@ -1,14 +1,15 @@
 import { defineStore } from 'pinia';
 import { editorService } from '@/src/shared/services';
 import { Settings } from '@/src/shared/interfaces';
+import { i18n } from '@/src/i18n/config';
 
 interface SettingsState {
-    settings?: Settings
+    settings?: Settings;
 }
 
 export const useSettingsStore = defineStore('settings', {
     state: (): SettingsState => ({
-        settings: undefined
+        settings: undefined,
     }),
 
     actions: {
@@ -16,14 +17,22 @@ export const useSettingsStore = defineStore('settings', {
             editorService.fetchSettings();
         },
 
+        initSettings(settings: Settings) {
+            this.settings = settings;
+            if (this.settings.locale && this.settings.locale !== i18n.global.locale) {
+                i18n.global.locale = this.settings.locale;
+            }
+        },
+
         sendSettings() {
             editorService.setSettings(JSON.parse(JSON.stringify(this.settings)));
         },
 
         setSettings(spellcheck: boolean) {
             this.settings.spellcheck = spellcheck;
+            this.settings.locale = i18n.global.locale;
 
             this.sendSettings();
-        }
-    }
-})
+        },
+    },
+});
-- 
GitLab


From e946c73288921ba74451ea5f1543a2f8720832ee Mon Sep 17 00:00:00 2001
From: VIAUD Nathan <nathan.viaud@inria.fr>
Date: Fri, 21 Mar 2025 12:19:04 +0100
Subject: [PATCH 12/18] feat: WIP dynamic translation

---
 .../badge/components/ConditionItem.vue        |   8 +-
 src/shared/data/badge.data.ts                 |  27 +-
 src/shared/data/form.data.ts                  |   8 +-
 src/shared/data/forms/badgeForm.data.ts       | 105 +++---
 src/shared/data/forms/contentForm.data.ts     | 303 +++++++++---------
 src/shared/data/forms/formButtons.data.ts     |  95 +++---
 src/shared/data/forms/nodeForm.data.ts        | 227 ++++++-------
 src/shared/data/forms/questionsForm.data.ts   | 293 +++++++++--------
 src/shared/data/sideBar.data.ts               |  75 +++--
 src/shared/services/graph/badge.service.ts    |  79 +++--
 src/shared/services/graph/content.service.ts  |   4 +-
 src/shared/services/graph/element.service.ts  |   2 +
 src/shared/services/import.service.ts         |  20 +-
 src/shared/stores/editorStore.ts              |  16 +-
 14 files changed, 661 insertions(+), 601 deletions(-)

diff --git a/src/features/badge/components/ConditionItem.vue b/src/features/badge/components/ConditionItem.vue
index 90b6b492..e57b6b9b 100644
--- a/src/features/badge/components/ConditionItem.vue
+++ b/src/features/badge/components/ConditionItem.vue
@@ -56,6 +56,12 @@ function handleVerbChange(value: string) {
     resetValue(true);
     updateCondition(value, 'verb');
 }
+
+const verbs = computed(() => {
+    if (!elementType.value) return [];
+    const res = getVerbs(elementType.value);
+    return res.value;
+});
 </script>
 
 <template>
@@ -90,7 +96,7 @@ function handleVerbChange(value: string) {
                     @change="handleVerbChange(($event.target as HTMLSelectElement).value)"
                 >
                     <option value="">{{ $t('global.pleaseSelect') }}</option>
-                    <option v-for="(description, verb) in getVerbs(elementType)" :key="verb" :value="verb">
+                    <option v-for="(description, verb) in verbs" :key="verb" :value="verb">
                         {{ description.label }}
                     </option>
                 </select>
diff --git a/src/shared/data/badge.data.ts b/src/shared/data/badge.data.ts
index b083d38e..efc04b6f 100644
--- a/src/shared/data/badge.data.ts
+++ b/src/shared/data/badge.data.ts
@@ -1,25 +1,24 @@
 import env from '@/src/shared/utils/env';
 import { ElementType, VerbKey, Verbs } from '@/src/shared/interfaces';
 import { i18n } from '@/src/i18n/config';
-
-const { t } = i18n.global;
+import { computed, ComputedRef } from 'vue';
 
 export const iconsPath = env.isDev ? '/img/badge/icon' : 'img/badge/icon';
 
 export const defaultBadgeIcons = ['audio', 'check', 'condition', 'cup', 'puzzle', 'question', 'star', 'video'];
 
-export const verbs: Verbs = {
-    started: { label: t('verbs.started'), valueType: 'boolean' },
-    completed: { label: t('verbs.completed'), valueType: 'boolean' },
-    viewed: { label: t('verbs.viewed'), valueType: 'boolean' },
-    read: { label: t('verbs.read'), valueType: 'boolean' },
-    played: { label: t('verbs.played'), valueType: 'boolean' },
-    watched: { label: t('verbs.watched'), valueType: 'boolean' },
-    listened: { label: t('verbs.listened'), valueType: 'boolean' },
-    attempted: { label: t('verbs.attempted'), valueType: 'boolean' },
-    scored: { label: t('verbs.scored'), valueType: 'number' },
-    passed: { label: t('verbs.passed'), valueType: 'boolean' },
-};
+export const verbs: ComputedRef<Verbs> = computed(() => ({
+    started: { label: i18n.global.t('verbs.started'), valueType: 'boolean' },
+    completed: { label: i18n.global.t('verbs.completed'), valueType: 'boolean' },
+    viewed: { label: i18n.global.t('verbs.viewed'), valueType: 'boolean' },
+    read: { label: i18n.global.t('verbs.read'), valueType: 'boolean' },
+    played: { label: i18n.global.t('verbs.played'), valueType: 'boolean' },
+    watched: { label: i18n.global.t('verbs.watched'), valueType: 'boolean' },
+    listened: { label: i18n.global.t('verbs.listened'), valueType: 'boolean' },
+    attempted: { label: i18n.global.t('verbs.attempted'), valueType: 'boolean' },
+    scored: { label: i18n.global.t('verbs.scored'), valueType: 'number' },
+    passed: { label: i18n.global.t('verbs.passed'), valueType: 'boolean' },
+}));
 
 export const elementVerbs: Record<ElementType, VerbKey[]> = {
     chapter: ['started'],
diff --git a/src/shared/data/form.data.ts b/src/shared/data/form.data.ts
index ddc65676..33483987 100644
--- a/src/shared/data/form.data.ts
+++ b/src/shared/data/form.data.ts
@@ -1,4 +1,10 @@
 import { Form } from '@/src/shared/interfaces';
 import { elementForms, questionForms, nodeForms, badgeForms } from './forms';
+import { computed, ComputedRef } from 'vue';
 
-export const formsModel: Form[] = [...elementForms, ...questionForms, ...nodeForms, ...badgeForms];
+export const formsModel: ComputedRef<Form[]> = computed(() => [
+    ...elementForms.value,
+    ...questionForms.value,
+    ...nodeForms.value,
+    ...badgeForms.value,
+]);
diff --git a/src/shared/data/forms/badgeForm.data.ts b/src/shared/data/forms/badgeForm.data.ts
index b51a952b..3573e693 100644
--- a/src/shared/data/forms/badgeForm.data.ts
+++ b/src/shared/data/forms/badgeForm.data.ts
@@ -1,57 +1,58 @@
 import { Form } from '@/src/shared/interfaces';
 import { badgeButtons } from './formButtons.data';
+import { computed, ComputedRef } from 'vue';
 import { i18n } from '@/src/i18n/config';
 
-const { t } = i18n.global;
+export const customBadgeForm: ComputedRef<Form> = computed(() => {
+    return {
+        type: 'badge',
+        name: 'Paramètres du badge',
+        icon: 'icon-badge',
+        buttons: badgeButtons.value,
+        fields: [
+            {
+                inputs: [
+                    {
+                        id: 'title',
+                        type: 'text',
+                        label: i18n.global.t('forms.node.title'),
+                        value: '',
+                        placeholder: i18n.global.t('forms.type'),
+                    },
+                    {
+                        id: 'icon',
+                        type: 'icon-picker',
+                        label: i18n.global.t('forms.badge.icon'),
+                        value: '',
+                        placeholder: i18n.global.t('forms.badge.updateIcon'),
+                    },
+                ],
+            },
+            {
+                name: i18n.global.t('forms.badge.obtention'),
+                inputs: [
+                    {
+                        id: 'conditions',
+                        type: 'badge-conditions',
+                        label: '',
+                        value: [],
+                    },
+                ],
+            },
+            {
+                name: i18n.global.t('forms.badge.presentation'),
+                inputs: [
+                    {
+                        id: 'description',
+                        type: 'textarea',
+                        label: '',
+                        value: '',
+                        placeholder: i18n.global.t('forms.badge.presentationPlaceholder'),
+                    },
+                ],
+            },
+        ],
+    };
+});
 
-export const customBadgeForm: Form = {
-    type: 'badge',
-    name: 'Paramètres du badge',
-    icon: 'icon-badge',
-    buttons: badgeButtons,
-    fields: [
-        {
-            inputs: [
-                {
-                    id: 'title',
-                    type: 'text',
-                    label: t('forms.node.title'),
-                    value: '',
-                    placeholder: t('forms.type'),
-                },
-                {
-                    id: 'icon',
-                    type: 'icon-picker',
-                    label: t('forms.badge.icon'),
-                    value: '',
-                    placeholder: t('forms.badge.updateIcon'),
-                },
-            ],
-        },
-        {
-            name: t('forms.badge.obtention'),
-            inputs: [
-                {
-                    id: 'conditions',
-                    type: 'badge-conditions',
-                    label: '',
-                    value: [],
-                },
-            ],
-        },
-        {
-            name: t('forms.badge.presentation'),
-            inputs: [
-                {
-                    id: 'description',
-                    type: 'textarea',
-                    label: '',
-                    value: '',
-                    placeholder: t('forms.badge.presentationPlaceholder'),
-                },
-            ],
-        },
-    ],
-};
-
-export const badgeForms: Form[] = [customBadgeForm];
+export const badgeForms: ComputedRef<Form[]> = computed(() => [customBadgeForm.value]);
diff --git a/src/shared/data/forms/contentForm.data.ts b/src/shared/data/forms/contentForm.data.ts
index 2e64b44a..83e62ac4 100644
--- a/src/shared/data/forms/contentForm.data.ts
+++ b/src/shared/data/forms/contentForm.data.ts
@@ -1,156 +1,161 @@
 import { Form } from '@/src/shared/interfaces';
 import { contentButtons } from './formButtons.data';
+import { computed, ComputedRef } from 'vue';
 import { i18n } from '@/src/i18n/config';
 
-const { t } = i18n.global;
+export const textForm: ComputedRef<Form> = computed(() => {
+    return {
+        type: 'text',
+        name: i18n.global.t('forms.content.text'),
+        icon: 'icon-texte',
+        buttons: contentButtons.value,
+        fields: [
+            {
+                inputs: [
+                    {
+                        id: 'html',
+                        type: 'html',
+                        label: '',
+                        value: '',
+                        placeholder: i18n.global.t('type'),
+                    },
+                ],
+            },
+        ],
+    };
+});
 
-export const textForm: Form = {
-    type: 'text',
-    name: t('forms.content.text'),
-    icon: 'icon-texte',
-    buttons: contentButtons,
-    fields: [
-        {
-            inputs: [
-                {
-                    id: 'html',
-                    type: 'html',
-                    label: '',
-                    value: '',
-                    placeholder: t('type'),
-                },
-            ],
-        },
-    ],
-};
+export const videoForm: ComputedRef<Form> = computed(() => {
+    return {
+        type: 'video',
+        name: i18n.global.t('forms.content.video.label'),
+        icon: 'icon-video',
+        buttons: contentButtons.value,
+        fields: [
+            {
+                inputs: [
+                    {
+                        id: 'source',
+                        type: 'file',
+                        label: i18n.global.t('forms.content.video.label'),
+                        placeholder: i18n.global.t('forms.content.video.placeholder'),
+                        value: '',
+                        accept: '.mp4',
+                        hint: i18n.global.t('forms.content.video.hint'),
+                    },
+                    {
+                        id: 'summary',
+                        type: 'html',
+                        label: i18n.global.t('forms.content.summary'),
+                        value: '',
+                        placeholder: i18n.global.t('forms.type'),
+                    },
+                    {
+                        id: 'transcript',
+                        type: 'file',
+                        label: i18n.global.t('forms.content.transcription.label'),
+                        value: '',
+                        placeholder: i18n.global.t('forms.content.transcription.placeholder'),
+                        accept: '.txt,.vtt',
+                        hint: i18n.global.t('forms.content.transcription.hint', { extensions: '.txt,.vtt' }),
+                    },
+                    {
+                        id: 'poster',
+                        type: 'file',
+                        label: i18n.global.t('forms.content.thumbnail.label'),
+                        value: '',
+                        placeholder: i18n.global.t('forms.content.thumbnail.placeholder'),
+                        accept: '.png,.jpg,.jpeg,.gif,.bmp,.svg,.webp',
+                        hint: i18n.global.t('forms.content.thumbnail.hint'),
+                    },
+                ],
+            },
+            {
+                name: i18n.global.t('forms.node.subtitle'),
+                inputs: [
+                    {
+                        id: 'subtitles',
+                        label: i18n.global.t('forms.node.subtitle'),
+                        type: 'repeat',
+                        value: [],
+                        inputs: [
+                            {
+                                id: 'label',
+                                type: 'text',
+                                label: i18n.global.t('forms.content.subtitle.label'),
+                                value: '',
+                                placeholder: 'English',
+                            },
+                            {
+                                id: 'lang',
+                                type: 'text',
+                                label: i18n.global.t('forms.content.subtitle.code'),
+                                value: '',
+                                placeholder: 'en',
+                            },
+                            {
+                                id: 'src',
+                                type: 'file',
+                                label: i18n.global.t('global.file'),
+                                value: '',
+                                placeholder: i18n.global.t('forms.content.subtitle.placeholder'),
+                                accept: '.vtt',
+                                hint: i18n.global.t('forms.content.subtitle.hint', { extensions: '.vtt' }),
+                            },
+                        ],
+                    },
+                ],
+            },
+        ],
+    };
+});
 
-export const videoForm: Form = {
-    type: 'video',
-    name: t('forms.content.video.label'),
-    icon: 'icon-video',
-    buttons: contentButtons,
-    fields: [
-        {
-            inputs: [
-                {
-                    id: 'source',
-                    type: 'file',
-                    label: t('forms.content.video.label'),
-                    placeholder: t('forms.content.video.placeholder'),
-                    value: '',
-                    accept: '.mp4',
-                    hint: t('forms.content.video.hint'),
-                },
-                {
-                    id: 'summary',
-                    type: 'html',
-                    label: t('forms.content.summary'),
-                    value: '',
-                    placeholder: t('forms.type'),
-                },
-                {
-                    id: 'transcript',
-                    type: 'file',
-                    label: t('forms.content.transcription.label'),
-                    value: '',
-                    placeholder: t('forms.content.transcription.placeholder'),
-                    accept: '.txt,.vtt',
-                    hint: t('forms.content.transcription.hint', { extensions: '.txt,.vtt' }),
-                },
-                {
-                    id: 'poster',
-                    type: 'file',
-                    label: t('forms.content.thumbnail.label'),
-                    value: '',
-                    placeholder: t('forms.content.thumbnail.placeholder'),
-                    accept: '.png,.jpg,.jpeg,.gif,.bmp,.svg,.webp',
-                    hint: t('forms.content.thumbnail.hint'),
-                },
-            ],
-        },
-        {
-            name: t('forms.node.subtitle'),
-            inputs: [
-                {
-                    id: 'subtitles',
-                    label: t('forms.node.subtitle'),
-                    type: 'repeat',
-                    value: [],
-                    inputs: [
-                        {
-                            id: 'label',
-                            type: 'text',
-                            label: t('forms.content.subtitle.label'),
-                            value: '',
-                            placeholder: 'English',
-                        },
-                        {
-                            id: 'lang',
-                            type: 'text',
-                            label: t('forms.content.subtitle.code'),
-                            value: '',
-                            placeholder: 'en',
-                        },
-                        {
-                            id: 'src',
-                            type: 'file',
-                            label: t('global.file'),
-                            value: '',
-                            placeholder: t('forms.content.subtitle.placeholder'),
-                            accept: '.vtt',
-                            hint: t('forms.content.subtitle.hint', { extensions: '.vtt' }),
-                        },
-                    ],
-                },
-            ],
-        },
-    ],
-};
+export const audioForm: ComputedRef<Form> = computed(() => {
+    return {
+        type: 'audio',
+        name: i18n.global.t('forms.content.audio.label'),
+        icon: 'icon-audio',
+        buttons: contentButtons.value,
+        fields: [
+            {
+                inputs: [
+                    {
+                        id: 'source',
+                        type: 'file',
+                        label: i18n.global.t('forms.content.audio.label'),
+                        placeholder: i18n.global.t('forms.content.audio.placeholder'),
+                        value: '',
+                        accept: '.mp3',
+                    },
+                    {
+                        id: 'summary',
+                        type: 'html',
+                        label: i18n.global.t('forms.content.summary'),
+                        value: '',
+                        placeholder: i18n.global.t('forms.type'),
+                    },
+                    {
+                        id: 'transcript',
+                        type: 'file',
+                        label: i18n.global.t('forms.content.transcription.label'),
+                        placeholder: i18n.global.t('forms.content.transcription.placeholder'),
+                        value: '',
+                        accept: '.txt,.vtt',
+                        hint: i18n.global.t('forms.content.transcription.hint', { extensions: '.txt,.vtt' }),
+                    },
+                    {
+                        id: 'subtitles',
+                        type: 'file',
+                        label: i18n.global.t('forms.content.subtitle.label'),
+                        value: '',
+                        placeholder: i18n.global.t('forms.content.subtitle.placeholder'),
+                        accept: '.vtt',
+                        hint: i18n.global.t('forms.content.subtitle.hint', { extensions: '.vtt' }),
+                    },
+                ],
+            },
+        ],
+    };
+});
 
-export const audioForm: Form = {
-    type: 'audio',
-    name: t('forms.content.audio.label'),
-    icon: 'icon-audio',
-    buttons: contentButtons,
-    fields: [
-        {
-            inputs: [
-                {
-                    id: 'source',
-                    type: 'file',
-                    label: t('forms.content.audio.label'),
-                    placeholder: t('forms.content.audio.placeholder'),
-                    value: '',
-                    accept: '.mp3',
-                },
-                {
-                    id: 'summary',
-                    type: 'html',
-                    label: t('forms.content.summary'),
-                    value: '',
-                    placeholder: t('forms.type'),
-                },
-                {
-                    id: 'transcript',
-                    type: 'file',
-                    label: t('forms.content.transcription.label'),
-                    placeholder: t('forms.content.transcription.placeholder'),
-                    value: '',
-                    accept: '.txt,.vtt',
-                    hint: t('forms.content.transcription.hint', { extensions: '.txt,.vtt' }),
-                },
-                {
-                    id: 'subtitles',
-                    type: 'file',
-                    label: t('forms.content.subtitle.label'),
-                    value: '',
-                    placeholder: t('forms.content.subtitle.placeholder'),
-                    accept: '.vtt',
-                    hint: t('forms.content.subtitle.hint', { extensions: '.vtt' }),
-                },
-            ],
-        },
-    ],
-};
-
-export const elementForms: Form[] = [textForm, videoForm, audioForm];
+export const elementForms: ComputedRef<Form[]> = computed(() => [textForm.value, videoForm.value, audioForm.value]);
diff --git a/src/shared/data/forms/formButtons.data.ts b/src/shared/data/forms/formButtons.data.ts
index eef8dbcc..65a7a12c 100644
--- a/src/shared/data/forms/formButtons.data.ts
+++ b/src/shared/data/forms/formButtons.data.ts
@@ -1,45 +1,68 @@
 import { FormButton } from '@/src/shared/interfaces';
 import env from '@/src/shared/utils/env';
+import { computed, ComputedRef } from 'vue';
 import { i18n } from '@/src/i18n/config';
 
-const { t } = i18n.global;
+export const baseButtons: ComputedRef<FormButton[]> = computed(() => {
+    return [
+        { label: i18n.global.t('global.delete'), icon: 'icon-supprimer', action: 'delete' },
+        { label: i18n.global.t('forms.buttons.addBadge'), icon: 'icon-plus', action: 'add-badge' },
+    ];
+});
 
-export const baseButtons = [
-    { label: t('global.delete'), icon: 'icon-supprimer', action: 'delete' },
-    { label: t('forms.buttons.addBadge'), icon: 'icon-plus', action: 'add-badge' },
-];
+export const pageButtons: ComputedRef<FormButton[]> = computed(() => {
+    if (env.isDev) {
+        return [
+            ...baseButtons.value,
+            { label: i18n.global.t('forms.buttons.duplicatePage'), icon: 'icon-plus', action: 'duplicate-page' },
+            { label: i18n.global.t('forms.buttons.saveModel'), icon: 'icon-modele', action: 'save-model' },
+        ];
+    }
 
-export const pageButtons: FormButton[] =
-    env.isDev ?
-        [
-            ...baseButtons,
-            { label: t('forms.buttons.duplicatePage'), icon: 'icon-plus', action: 'duplicate-page' },
-            { label: t('forms.buttons.saveModel'), icon: 'icon-modele', action: 'save-model' },
-        ]
-    :   [...baseButtons, { label: t('forms.buttons.duplicatePage'), icon: 'icon-plus', action: 'duplicate-page' }];
+    return [
+        ...baseButtons.value,
+        { label: i18n.global.t('forms.buttons.duplicatePage'), icon: 'icon-plus', action: 'duplicate-page' },
+    ];
+});
 
-export const activityButtons: FormButton[] =
-    env.isDev ?
-        [
-            ...baseButtons,
-            { label: t('forms.buttons.duplicateEvaluation'), icon: 'icon-plus', action: 'duplicate-page' },
-            { label: t('forms.buttons.saveModel'), icon: 'icon-modele', action: 'save-model' },
-        ]
-    :   [
-            ...baseButtons,
-            { label: t('forms.buttons.duplicateEvaluation'), icon: 'icon-plus', action: 'duplicate-page' },
-        ];
+export const activityButtons: ComputedRef<FormButton[]> = computed(() => {
+    return env.isDev ?
+            [
+                ...baseButtons.value,
+                {
+                    label: i18n.global.t('forms.buttons.duplicateEvaluation'),
+                    icon: 'icon-plus',
+                    action: 'duplicate-page',
+                },
+                { label: i18n.global.t('forms.buttons.saveModel'), icon: 'icon-modele', action: 'save-model' },
+            ]
+        :   [
+                ...baseButtons.value,
+                {
+                    label: i18n.global.t('forms.buttons.duplicateEvaluation'),
+                    icon: 'icon-plus',
+                    action: 'duplicate-page',
+                },
+            ];
+});
 
-export const contentButtons: FormButton[] =
-    env.isDev ?
-        [
-            ...baseButtons,
-            { label: t('forms.buttons.backToPage'), icon: 'icon-ecran', action: 'back-to-page' },
-            { label: t('forms.buttons.duplicateElement'), icon: 'icon-plus', action: 'duplicate-element' },
-        ]
-    :   [...baseButtons];
+export const contentButtons: ComputedRef<FormButton[]> = computed(() => {
+    return env.isDev ?
+            [
+                ...baseButtons.value,
+                { label: i18n.global.t('forms.buttons.backToPage'), icon: 'icon-ecran', action: 'back-to-page' },
+                {
+                    label: i18n.global.t('forms.buttons.duplicateElement'),
+                    icon: 'icon-plus',
+                    action: 'duplicate-element',
+                },
+            ]
+        :   [...baseButtons.value];
+});
 
-export const badgeButtons: FormButton[] = [
-    { label: t('global.delete'), icon: 'icon-supprimer', action: 'delete-badge' },
-    { label: t('forms.buttons.backToEpoc'), icon: 'icon-epoc', action: 'back-to-epoc' },
-];
+export const badgeButtons: ComputedRef<FormButton[]> = computed(() => {
+    return [
+        { label: i18n.global.t('global.delete'), icon: 'icon-supprimer', action: 'delete-badge' },
+        { label: i18n.global.t('forms.buttons.backToEpoc'), icon: 'icon-epoc', action: 'back-to-epoc' },
+    ];
+});
diff --git a/src/shared/data/forms/nodeForm.data.ts b/src/shared/data/forms/nodeForm.data.ts
index ec796cf5..8de1e773 100644
--- a/src/shared/data/forms/nodeForm.data.ts
+++ b/src/shared/data/forms/nodeForm.data.ts
@@ -1,14 +1,13 @@
 import { Form } from '@/src/shared/interfaces';
 import { activityButtons, baseButtons, pageButtons } from './formButtons.data';
+import { computed, ComputedRef } from 'vue';
 import { i18n } from '@/src/i18n/config';
 
-const { t } = i18n.global;
-
-export const conditionForm: Form = {
+export const conditionForm: ComputedRef<Form> = computed(() => ({
     type: 'condition',
-    name: t('global.conditions'),
+    name: i18n.global.t('global.conditions'),
     icon: 'icon-condition',
-    buttons: baseButtons,
+    buttons: baseButtons.value,
     fields: [
         {
             inputs: [
@@ -17,34 +16,34 @@ export const conditionForm: Form = {
                     type: 'text',
                     label: '',
                     value: '',
-                    placeholder: t('forms.node.conditionPlaceholder', { condition: '1' }),
+                    placeholder: i18n.global.t('forms.node.conditionPlaceholder', { condition: '1' }),
                 },
                 {
                     id: 'condition2',
                     type: 'text',
                     label: '',
                     value: '',
-                    placeholder: t('forms.node.conditionPlaceholder', { condition: '2' }),
+                    placeholder: i18n.global.t('forms.node.conditionPlaceholder', { condition: '2' }),
                 },
             ],
         },
     ],
-};
+}));
 
-export const legacyConditionForm: Form = {
+export const legacyConditionForm: ComputedRef<Form> = computed(() => ({
     type: 'legacy-condition',
     name: 'Conditions (legacy)',
     icon: 'icon-condition',
-    buttons: baseButtons,
+    buttons: baseButtons.value,
     fields: [
         {
             inputs: [
                 {
                     id: 'label',
                     type: 'text',
-                    label: t('global.label'),
+                    label: i18n.global.t('global.label'),
                     value: '',
-                    placeholder: t('forms.type'),
+                    placeholder: i18n.global.t('forms.type'),
                 },
             ],
         },
@@ -53,15 +52,18 @@ export const legacyConditionForm: Form = {
             inputs: [
                 {
                     id: 'choices',
-                    label: t('forms.node.choice'),
+                    label: i18n.global.t('forms.node.choice'),
                     type: 'repeat',
-                    value: [t('forms.node.course', { course: 'A' }), t('forms.node.course', { course: 'B' })],
+                    value: [
+                        i18n.global.t('forms.node.course', { course: 'A' }),
+                        i18n.global.t('forms.node.course', { course: 'B' }),
+                    ],
                     inputs: [
                         {
                             id: '',
                             type: 'text',
                             label: '',
-                            placeholder: t('forms.node.course', { course: 'X' }),
+                            placeholder: i18n.global.t('forms.node.course', { course: 'X' }),
                             value: '',
                         },
                     ],
@@ -69,11 +71,11 @@ export const legacyConditionForm: Form = {
             ],
         },
         {
-            name: t('forms.node.conditional'),
+            name: i18n.global.t('forms.node.conditional'),
             inputs: [
                 {
                     id: 'conditionalFlag',
-                    label: t('forms.content.text'),
+                    label: i18n.global.t('forms.content.text'),
                     type: 'repeat',
                     value: [],
                     inputs: [
@@ -81,7 +83,7 @@ export const legacyConditionForm: Form = {
                             id: 'id',
                             type: 'text',
                             label: '',
-                            placeholder: t('forms.type'),
+                            placeholder: i18n.global.t('forms.type'),
                             value: '',
                         },
                         {
@@ -98,37 +100,37 @@ export const legacyConditionForm: Form = {
             ],
         },
     ],
-};
+}));
 
-export const chapterForm: Form = {
+export const chapterForm: ComputedRef<Form> = computed(() => ({
     type: 'chapter',
-    name: t('global.chapter'),
+    name: i18n.global.t('global.chapter'),
     icon: 'icon-chapitre',
-    buttons: baseButtons,
+    buttons: baseButtons.value,
     fields: [
         {
             inputs: [
                 {
                     id: 'title',
                     type: 'text',
-                    label: t('forms.node.title'),
+                    label: i18n.global.t('forms.node.title'),
                     value: '',
-                    placeholder: t('forms.type'),
+                    placeholder: i18n.global.t('forms.type'),
                 },
                 {
                     id: 'duration',
                     type: 'score',
-                    label: t('forms.node.duration'),
+                    label: i18n.global.t('forms.node.duration'),
                     value: 0,
                 },
             ],
         },
         {
-            name: t('forms.node.objectives'),
+            name: i18n.global.t('forms.node.objectives'),
             inputs: [
                 {
                     id: 'objectives',
-                    label: t('global.objective'),
+                    label: i18n.global.t('global.objective'),
                     type: 'repeat',
                     value: [],
                     inputs: [
@@ -136,7 +138,7 @@ export const chapterForm: Form = {
                             id: '',
                             type: 'textarea',
                             label: '',
-                            placeholder: t('forms.type'),
+                            placeholder: i18n.global.t('forms.type'),
                             value: '',
                         },
                     ],
@@ -144,11 +146,11 @@ export const chapterForm: Form = {
             ],
         },
     ],
-};
+}));
 
-export const epocForm: Form = {
+export const epocForm: ComputedRef<Form> = computed(() => ({
     type: 'epoc',
-    name: t('forms.node.about'),
+    name: i18n.global.t('forms.node.about'),
     icon: 'icon-epoc',
     buttons: [],
     fields: [
@@ -157,90 +159,90 @@ export const epocForm: Form = {
                 {
                     id: 'title',
                     type: 'text',
-                    label: t('forms.node.title'),
+                    label: i18n.global.t('forms.node.title'),
                     value: '',
-                    placeholder: t('forms.type'),
+                    placeholder: i18n.global.t('forms.type'),
                 },
                 {
                     id: 'image',
                     type: 'file',
-                    label: t('forms.node.cover.title'),
-                    placeholder: t('forms.node.cover.placeholder'),
+                    label: i18n.global.t('forms.node.cover.title'),
+                    placeholder: i18n.global.t('forms.node.cover.placeholder'),
                     value: '',
                     accept: '.png,.jpg,.jpeg,.gif,.bmp,.svg,.webp',
-                    hint: t('forms.node.cover.hint'),
+                    hint: i18n.global.t('forms.node.cover.hint'),
                 },
                 {
                     id: 'teaser',
                     type: 'file',
-                    label: t('forms.node.teaser.title'),
+                    label: i18n.global.t('forms.node.teaser.title'),
                     value: '',
-                    placeholder: t('forms.node.teaser.placeholder'),
+                    placeholder: i18n.global.t('forms.node.teaser.placeholder'),
                     accept: '.mp4',
-                    hint: t('forms.node.teaser.hint'),
+                    hint: i18n.global.t('forms.node.teaser.hint'),
                 },
                 {
                     id: 'thumbnail',
                     type: 'file',
-                    label: t('forms.node.thumbnail.title'),
+                    label: i18n.global.t('forms.node.thumbnail.title'),
                     value: '',
-                    placeholder: t('forms.content.thumbnail.placeholder'),
+                    placeholder: i18n.global.t('forms.content.thumbnail.placeholder'),
                     accept: '.png,.jpg,.jpeg,.gif,.bmp,.svg,.webp',
-                    hint: t('forms.node.thumbnail.hint'),
+                    hint: i18n.global.t('forms.node.thumbnail.hint'),
                 },
                 {
                     id: 'summary',
                     type: 'html-text',
-                    label: t('forms.node.presentation'),
+                    label: i18n.global.t('forms.node.presentation'),
                     value: '',
-                    placeholder: t('forms.type'),
+                    placeholder: i18n.global.t('forms.type'),
                 },
                 {
                     id: 'edition',
                     type: 'text',
-                    label: t('forms.node.edition'),
+                    label: i18n.global.t('forms.node.edition'),
                     value: String(new Date().getFullYear()),
                 },
             ],
         },
         {
-            name: t('forms.node.author.title', 2),
+            name: i18n.global.t('forms.node.author.title', 2),
             inputs: [
                 {
                     id: 'authors',
-                    label: t('forms.node.author.title', 1),
+                    label: i18n.global.t('forms.node.author.title', 1),
                     type: 'repeat',
                     value: [],
                     inputs: [
                         {
                             id: 'name',
                             type: 'text',
-                            label: t('global.name'),
-                            placeholder: t('forms.node.author.placeholder'),
+                            label: i18n.global.t('global.name'),
+                            placeholder: i18n.global.t('forms.node.author.placeholder'),
                             value: '',
                         },
                         {
                             id: 'image',
                             type: 'file',
-                            label: t('forms.node.author.image.title'),
-                            placeholder: t('forms.node.author.image.placeholder'),
+                            label: i18n.global.t('forms.node.author.image.title'),
+                            placeholder: i18n.global.t('forms.node.author.image.placeholder'),
                             value: '',
                             accept: '.png,.jpg,.jpeg,.gif,.bmp,.svg,.webp',
-                            hint: t('forms.node.author.image.hint'),
+                            hint: i18n.global.t('forms.node.author.image.hint'),
                         },
                         {
                             id: 'title',
                             type: 'text',
-                            label: t('forms.node.author.position.title'),
-                            placeholder: t('forms.node.author.position.placeholder'),
+                            label: i18n.global.t('forms.node.author.position.title'),
+                            placeholder: i18n.global.t('forms.node.author.position.placeholder'),
                             value: '',
-                            hint: t('forms.node.author.position.hint'),
+                            hint: i18n.global.t('forms.node.author.position.hint'),
                         },
                         {
                             id: 'description',
                             type: 'html-text',
-                            label: t('forms.node.author.biography'),
-                            placeholder: t('forms.type'),
+                            label: i18n.global.t('forms.node.author.biography'),
+                            placeholder: i18n.global.t('forms.type'),
                             value: '',
                         },
                     ],
@@ -248,11 +250,11 @@ export const epocForm: Form = {
             ],
         },
         {
-            name: t('forms.node.objectives'),
+            name: i18n.global.t('forms.node.objectives'),
             inputs: [
                 {
                     id: 'objectives',
-                    label: t('global.objective'),
+                    label: i18n.global.t('global.objective'),
                     type: 'repeat',
                     value: [],
                     inputs: [
@@ -260,7 +262,7 @@ export const epocForm: Form = {
                             id: '',
                             type: 'textarea',
                             label: '',
-                            placeholder: t('forms.type'),
+                            placeholder: i18n.global.t('forms.type'),
                             value: '',
                         },
                     ],
@@ -268,50 +270,50 @@ export const epocForm: Form = {
             ],
         },
         {
-            name: t('settings.title'),
+            name: i18n.global.t('settings.title'),
             inputs: [
                 {
                     id: 'certificateBadgeCount',
                     type: 'score',
-                    label: t('forms.node.certificateBadge'),
+                    label: i18n.global.t('forms.node.certificateBadge'),
                     value: 1,
                 },
                 {
                     id: 'certificateScore',
                     type: 'score',
-                    label: t('forms.node.certificateScore'),
+                    label: i18n.global.t('forms.node.certificateScore'),
                     value: 10,
-                    hint: t('forms.node.certificateScoreHint'),
+                    hint: i18n.global.t('forms.node.certificateScoreHint'),
                 },
                 {
                     id: 'chapterParameter',
                     type: 'text',
-                    label: t('forms.node.chapterLabel'),
+                    label: i18n.global.t('forms.node.chapterLabel'),
                     value: '',
-                    placeholder: t('forms.type'),
+                    placeholder: i18n.global.t('forms.type'),
                 },
                 {
                     id: 'chapterDuration',
                     type: 'score',
-                    label: t('forms.node.chapterDuration'),
+                    label: i18n.global.t('forms.node.chapterDuration'),
                     value: 0,
                 },
             ],
         },
         {
-            name: t('forms.node.plugin.title', 2),
+            name: i18n.global.t('forms.node.plugin.title', 2),
             inputs: [
                 {
                     id: 'plugins',
-                    label: t('forms.node.plugin.title', 1),
+                    label: i18n.global.t('forms.node.plugin.title', 1),
                     type: 'repeat',
                     value: [],
                     inputs: [
                         {
                             id: 'script',
                             type: 'file',
-                            label: t('forms.node.plugin.script'),
-                            placeholder: t('forms.node.plugin.scriptPlaceholder'),
+                            label: i18n.global.t('forms.node.plugin.script'),
+                            placeholder: i18n.global.t('forms.node.plugin.scriptPlaceholder'),
                             targetDirectory: 'plugins',
                             value: '',
                             accept: '.js',
@@ -319,8 +321,8 @@ export const epocForm: Form = {
                         {
                             id: 'template',
                             type: 'file',
-                            label: t('forms.node.plugin.template'),
-                            placeholder: t('forms.node.plugin.templatePlaceholder'),
+                            label: i18n.global.t('forms.node.plugin.template'),
+                            placeholder: i18n.global.t('forms.node.plugin.templatePlaceholder'),
                             targetDirectory: 'plugins',
                             value: '',
                             accept: 'html',
@@ -330,72 +332,72 @@ export const epocForm: Form = {
             ],
         },
         {
-            name: t('forms.node.licence.title'),
+            name: i18n.global.t('forms.node.licence.title'),
             inputs: [
                 {
                     id: 'licenceName',
                     type: 'text',
-                    label: t('global.name'),
+                    label: i18n.global.t('global.name'),
                     placeholder: 'CC-BY 4.0',
                     value: '',
-                    hint: t('forms.node.licence.hint'),
+                    hint: i18n.global.t('forms.node.licence.hint'),
                 },
                 {
                     id: 'licenceUrl',
                     type: 'text',
-                    label: t('forms.node.licence.url'),
-                    placeholder: t('forms.node.licence.urlPlaceholder'),
+                    label: i18n.global.t('forms.node.licence.url'),
+                    placeholder: i18n.global.t('forms.node.licence.urlPlaceholder'),
                     value: '',
-                    hint: t('forms.node.licence.urlHint'),
+                    hint: i18n.global.t('forms.node.licence.urlHint'),
                 },
             ],
         },
     ],
-};
+}));
 
-export const pageForm: Form = {
+export const pageForm: ComputedRef<Form> = computed(() => ({
     type: 'page',
-    name: t('forms.node.page.title'),
+    name: i18n.global.t('forms.node.page.title'),
     icon: 'icon-ecran',
-    buttons: pageButtons,
+    buttons: pageButtons.value,
     fields: [
         {
             inputs: [
                 {
                     id: 'title',
                     type: 'text',
-                    label: t('forms.node.title'),
+                    label: i18n.global.t('forms.node.title'),
                     value: '',
-                    placeholder: t('forms.type'),
+                    placeholder: i18n.global.t('forms.type'),
                 },
                 {
                     id: 'subtitle',
                     type: 'text',
-                    label: t('forms.node.subtitle'),
+                    label: i18n.global.t('forms.node.subtitle'),
                     value: '',
-                    placeholder: t('forms.type'),
+                    placeholder: i18n.global.t('forms.type'),
                 },
                 {
                     id: 'hidden',
                     type: 'checkbox',
-                    label: t('forms.node.page.hidden'),
+                    label: i18n.global.t('forms.node.page.hidden'),
                     value: false,
                 },
                 {
                     id: 'conditional',
                     type: 'checkbox',
-                    label: t('forms.node.page.conditional'),
+                    label: i18n.global.t('forms.node.page.conditional'),
                     value: false,
-                    hint: t('forms.node.page.conditionalHint'),
+                    hint: i18n.global.t('forms.node.page.conditionalHint'),
                 },
             ],
         },
         {
-            name: t('forms.node.page.components'),
+            name: i18n.global.t('forms.node.page.components'),
             inputs: [
                 {
                     id: 'components',
-                    label: t('forms.node.page.components'),
+                    label: i18n.global.t('forms.node.page.components'),
                     type: 'repeat',
                     value: [],
                     addButton: false,
@@ -404,58 +406,58 @@ export const pageForm: Form = {
             ],
         },
     ],
-};
+}));
 
-export const activityForm: Form = {
+export const activityForm: ComputedRef<Form> = computed(() => ({
     type: 'activity',
-    name: t('forms.node.activity'),
+    name: i18n.global.t('forms.node.activity'),
     icon: 'icon-ecran',
-    buttons: activityButtons,
+    buttons: activityButtons.value,
     fields: [
         {
             inputs: [
                 {
                     id: 'title',
                     type: 'text',
-                    label: t('forms.node.title'),
+                    label: i18n.global.t('forms.node.title'),
                     value: '',
-                    placeholder: t('forms.type'),
+                    placeholder: i18n.global.t('forms.type'),
                 },
                 {
                     id: 'subtitle',
                     type: 'text',
-                    label: t('forms.node.subtitle'),
+                    label: i18n.global.t('forms.node.subtitle'),
                     value: '',
-                    placeholder: t('forms.type'),
+                    placeholder: i18n.global.t('forms.type'),
                 },
                 {
                     id: 'summary',
                     type: 'textarea',
-                    label: t('forms.content.summary'),
+                    label: i18n.global.t('forms.content.summary'),
                     value: '',
-                    placeholder: t('forms.type'),
+                    placeholder: i18n.global.t('forms.type'),
                 },
                 {
                     id: 'hidden',
                     type: 'checkbox',
-                    label: t('forms.node.page.hidden'),
+                    label: i18n.global.t('forms.node.page.hidden'),
                     value: false,
                 },
                 {
                     id: 'conditional',
                     type: 'checkbox',
-                    label: t('forms.node.page.conditional'),
+                    label: i18n.global.t('forms.node.page.conditional'),
                     value: false,
-                    hint: t('forms.node.page.conditionalHint'),
+                    hint: i18n.global.t('forms.node.page.conditionalHint'),
                 },
             ],
         },
         {
-            name: t('forms.node.page.components'),
+            name: i18n.global.t('forms.node.page.components'),
             inputs: [
                 {
                     id: 'components',
-                    label: t('forms.node.page.components'),
+                    label: i18n.global.t('forms.node.page.components'),
                     type: 'repeat',
                     value: [],
                     addButton: false,
@@ -464,6 +466,13 @@ export const activityForm: Form = {
             ],
         },
     ],
-};
+}));
 
-export const nodeForms: Form[] = [chapterForm, pageForm, epocForm, conditionForm, legacyConditionForm, activityForm];
+export const nodeForms: ComputedRef<Form[]> = computed(() => [
+    chapterForm.value,
+    pageForm.value,
+    epocForm.value,
+    conditionForm.value,
+    legacyConditionForm.value,
+    activityForm.value,
+]);
diff --git a/src/shared/data/forms/questionsForm.data.ts b/src/shared/data/forms/questionsForm.data.ts
index f04c2341..0ecc3e08 100644
--- a/src/shared/data/forms/questionsForm.data.ts
+++ b/src/shared/data/forms/questionsForm.data.ts
@@ -2,82 +2,81 @@ import { Form } from '@/src/shared/interfaces';
 import { contentButtons } from './formButtons.data';
 import { i18n } from '@/src/i18n/config';
 import { capitalizeFirstLetter } from '../../utils/string';
+import { ComputedRef, computed } from 'vue';
 
-const { t } = i18n.global;
-
-export const qcmForm: Form = {
+export const qcmForm: ComputedRef<Form> = computed(() => ({
     type: 'choice',
-    name: t('questions.types.qcm'),
+    name: i18n.global.t('questions.types.qcm'),
     icon: 'icon-qcm',
     displayFieldIndex: true,
-    buttons: contentButtons,
+    buttons: contentButtons.value,
     fields: [
         {
-            name: t('questions.configuration'),
+            name: i18n.global.t('questions.configuration'),
             inputs: [
                 {
                     id: 'score',
                     type: 'score',
-                    label: t('questions.score'),
+                    label: i18n.global.t('questions.score'),
                     value: 0,
                 },
             ],
         },
         {
-            name: t('questions.question'),
+            name: i18n.global.t('questions.question'),
             inputs: [
                 {
                     id: 'label',
                     type: 'textarea',
-                    label: t('questions.question'),
+                    label: i18n.global.t('questions.question'),
                     value: '',
-                    placeholder: t('questions.askQuestion'),
+                    placeholder: i18n.global.t('questions.askQuestion'),
                 },
                 {
                     id: 'statement',
                     type: 'html-inline',
-                    label: t('questions.instruction'),
+                    label: i18n.global.t('questions.instruction'),
                     value: '',
-                    placeholder: t('questions.instructionPlaceholder'),
+                    placeholder: i18n.global.t('questions.instructionPlaceholder'),
                 },
             ],
         },
         {
-            name: t('questions.responses'),
+            name: i18n.global.t('questions.responses'),
             inputs: [
                 {
                     id: 'responses',
-                    label: t('questions.response'),
+                    label: i18n.global.t('questions.response'),
                     type: 'repeat',
                     value: [],
                     inputs: [
                         {
                             id: 'label',
                             type: 'text',
-                            label: t('questions.response'),
-                            placeholder: t('questions.typeResponse'),
+                            label: i18n.global.t('questions.response'),
+                            placeholder: i18n.global.t('questions.typeResponse'),
                             value: '',
                         },
                         {
                             id: 'value',
                             type: 'hidden',
                             label: '',
-                            placeholder: t('forms.type'),
+                            placeholder: i18n.global.t('forms.type'),
                             value: '',
                         },
                         {
                             id: 'feedback',
                             type: 'textarea',
-                            label: t('questions.explanation'),
-                            placeholder: t('questions.typeExplanation'),
+                            label: i18n.global.t('questions.explanation'),
+                            placeholder: i18n.global.t('questions.typeExplanation'),
                             value: '',
                             collapsible: true,
-                            collapsibleLabel: t('questions.addExplanation'),
+                            collapsibleLabel: i18n.global.t('questions.addExplanation'),
                         },
                         {
                             id: 'isCorrect',
                             type: 'checkbox',
-                            label: t('questions.correctResponse'),
+                            label: i18n.global.t('questions.correctResponse'),
                             value: false,
                         },
                     ],
@@ -85,63 +84,63 @@ export const qcmForm: Form = {
             ],
         },
         {
-            name: t('questions.explanation'),
+            name: i18n.global.t('questions.explanation'),
             inputs: [
                 {
                     id: 'explanation',
                     type: 'html',
                     label: '',
                     value: '',
-                    placeholder: t('questions.typeExplanation'),
+                    placeholder: i18n.global.t('questions.typeExplanation'),
                 },
             ],
         },
     ],
-};
+}));
 
-export const dragDropForm: Form = {
+export const dragDropForm: ComputedRef<Form> = computed(() => ({
     type: 'drag-and-drop',
-    name: t('questions.types.dragDrop'),
+    name: i18n.global.t('questions.types.dragDrop'),
     icon: 'icon-dragdrop',
     displayFieldIndex: true,
-    buttons: contentButtons,
+    buttons: contentButtons.value,
     fields: [
         {
-            name: t('questions.configuration'),
+            name: i18n.global.t('questions.configuration'),
             inputs: [
                 {
                     id: 'score',
                     type: 'score',
-                    label: t('questions.score'),
+                    label: i18n.global.t('questions.score'),
                     value: 0,
                 },
             ],
         },
         {
-            name: t('questions.question'),
+            name: i18n.global.t('questions.question'),
             inputs: [
                 {
                     id: 'label',
                     type: 'textarea',
-                    label: t('questions.question'),
+                    label: i18n.global.t('questions.question'),
                     value: '',
-                    placeholder: t('questions.askQuestion'),
+                    placeholder: i18n.global.t('questions.askQuestion'),
                 },
                 {
                     id: 'statement',
                     type: 'html-inline',
-                    label: t('questions.instruction'),
+                    label: i18n.global.t('questions.instruction'),
                     value: '',
-                    placeholder: t('questions.instructionPlaceholder'),
+                    placeholder: i18n.global.t('questions.instructionPlaceholder'),
                 },
             ],
         },
         {
-            name: t('questions.categories'),
+            name: i18n.global.t('questions.categories'),
             inputs: [
                 {
                     id: 'categories',
-                    label: t('questions.category'),
+                    label: i18n.global.t('questions.category'),
                     type: 'repeat',
                     value: [],
                     inputs: [
@@ -149,7 +148,7 @@ export const dragDropForm: Form = {
                             id: '',
                             type: 'textarea',
                             label: '',
-                            placeholder: t('questions.typeCategory'),
+                            placeholder: i18n.global.t('questions.typeCategory'),
                             value: '',
                         },
                     ],
@@ -157,36 +156,36 @@ export const dragDropForm: Form = {
             ],
         },
         {
-            name: t('questions.proposedResponses'),
+            name: i18n.global.t('questions.proposedResponses'),
             inputs: [
                 {
                     id: 'responses',
-                    label: t('questions.response'),
+                    label: i18n.global.t('questions.response'),
                     type: 'repeat',
                     value: [],
                     inputs: [
                         {
                             id: 'label',
                             type: 'text',
-                            label: t('questions.response'),
-                            placeholder: t('questions.typeResponse'),
+                            label: i18n.global.t('questions.response'),
+                            placeholder: i18n.global.t('questions.typeResponse'),
                             value: '',
                         },
                         {
                             id: 'value',
                             type: 'hidden',
                             label: '',
-                            placeholder: t('forms.type'),
+                            placeholder: i18n.global.t('forms.type'),
                             value: '',
                         },
                         {
                             id: 'feedback',
                             type: 'textarea',
-                            label: t('questions.explanation'),
-                            placeholder: t('questions.typeExplanation'),
+                            label: i18n.global.t('questions.explanation'),
+                            placeholder: i18n.global.t('questions.typeExplanation'),
                             value: '',
                             collapsible: true,
-                            collapsibleLabel: t('questions.addExplanation'),
+                            collapsibleLabel: i18n.global.t('questions.addExplanation'),
                         },
                         {
                             id: 'category',
@@ -202,87 +201,87 @@ export const dragDropForm: Form = {
             ],
         },
         {
-            name: t('questions.explanation'),
+            name: i18n.global.t('questions.explanation'),
             inputs: [
                 {
                     id: 'explanation',
                     type: 'html',
                     label: '',
                     value: '',
-                    placeholder: t('questions.typeExplanation'),
+                    placeholder: i18n.global.t('questions.typeExplanation'),
                 },
             ],
         },
     ],
-};
+}));
 
-export const reorderForm: Form = {
+export const reorderForm: ComputedRef<Form> = computed(() => ({
     type: 'reorder',
-    name: t('questions.types.reorder'),
+    name: i18n.global.t('questions.types.reorder'),
     icon: 'icon-reorder',
     displayFieldIndex: true,
-    buttons: contentButtons,
+    buttons: contentButtons.value,
     fields: [
         {
-            name: t('questions.configuration'),
+            name: i18n.global.t('questions.configuration'),
             inputs: [
                 {
                     id: 'score',
                     type: 'score',
-                    label: t('questions.score'),
+                    label: i18n.global.t('questions.score'),
                     value: 0,
                 },
             ],
         },
         {
-            name: t('questions.question'),
+            name: i18n.global.t('questions.question'),
             inputs: [
                 {
                     id: 'label',
                     type: 'textarea',
-                    label: t('questions.question'),
+                    label: i18n.global.t('questions.question'),
                     value: '',
-                    placeholder: t('questions.askQuestion'),
+                    placeholder: i18n.global.t('questions.askQuestion'),
                 },
                 {
                     id: 'statement',
                     type: 'html-inline',
-                    label: t('questions.instruction'),
+                    label: i18n.global.t('questions.instruction'),
                     value: '',
-                    placeholder: t('questions.instructionPlaceholder'),
+                    placeholder: i18n.global.t('questions.instructionPlaceholder'),
                 },
             ],
         },
         {
-            name: t('questions.responses'),
+            name: i18n.global.t('questions.responses'),
             inputs: [
                 {
                     id: 'responses',
-                    label: t('questions.response'),
+                    label: i18n.global.t('questions.response'),
                     type: 'repeat',
                     value: [],
                     inputs: [
                         {
                             id: 'label',
                             type: 'text',
-                            label: t('questions.response'),
-                            placeholder: t('questions.typeResponse'),
+                            label: i18n.global.t('questions.response'),
+                            placeholder: i18n.global.t('questions.typeResponse'),
                             value: '',
                         },
                         {
                             id: 'feedback',
                             type: 'textarea',
-                            label: t('questions.explanation'),
-                            placeholder: t('questions.typeExplanation'),
+                            label: i18n.global.t('questions.explanation'),
+                            placeholder: i18n.global.t('questions.typeExplanation'),
                             value: '',
                             collapsible: true,
-                            collapsibleLabel: t('questions.addExplanation'),
+                            collapsibleLabel: i18n.global.t('questions.addExplanation'),
                         },
                         {
                             id: 'value',
                             type: 'hidden',
                             label: '',
-                            placeholder: t('forms.type'),
+                            placeholder: i18n.global.t('forms.type'),
                             value: '',
                         },
                     ],
@@ -290,72 +289,75 @@ export const reorderForm: Form = {
             ],
         },
         {
-            name: t('questions.explanation'),
+            name: i18n.global.t('questions.explanation'),
             inputs: [
                 {
                     id: 'explanation',
                     type: 'html',
                     label: '',
                     value: '',
-                    placeholder: t('questions.typeExplanation'),
+                    placeholder: i18n.global.t('questions.typeExplanation'),
                 },
             ],
         },
     ],
-};
+}));
 
-export const swipeForm: Form = {
+export const swipeForm: ComputedRef<Form> = computed(() => ({
     type: 'swipe',
-    name: t('questions.types.swipe'),
+    name: i18n.global.t('questions.types.swipe'),
     icon: 'icon-swipe',
     displayFieldIndex: true,
-    buttons: contentButtons,
+    buttons: contentButtons.value,
     fields: [
         {
-            name: t('questions.configuration'),
+            name: i18n.global.t('questions.configuration'),
             inputs: [
                 {
                     id: 'score',
                     type: 'score',
-                    label: t('questions.score'),
+                    label: i18n.global.t('questions.score'),
                     value: 0,
                 },
             ],
         },
         {
-            name: t('questions.question'),
+            name: i18n.global.t('questions.question'),
             inputs: [
                 {
                     id: 'label',
                     type: 'textarea',
-                    label: t('questions.question'),
+                    label: i18n.global.t('questions.question'),
                     value: '',
-                    placeholder: t('questions.askQuestion'),
+                    placeholder: i18n.global.t('questions.askQuestion'),
                 },
                 {
                     id: 'statement',
                     type: 'html-inline',
-                    label: t('questions.instruction'),
+                    label: i18n.global.t('questions.instruction'),
                     value: '',
-                    placeholder: t('questions.instructionPlaceholder'),
+                    placeholder: i18n.global.t('questions.instructionPlaceholder'),
                 },
             ],
         },
         {
-            name: t('questions.proposedChoices'),
+            name: i18n.global.t('questions.proposedChoices'),
             inputs: [
                 {
                     id: 'categories',
-                    label: t('forms.node.choice'),
+                    label: i18n.global.t('forms.node.choice'),
                     type: 'repeat',
-                    value: [capitalizeFirstLetter(t('global.right')), capitalizeFirstLetter(t('global.left'))],
+                    value: [
+                        capitalizeFirstLetter(i18n.global.t('global.right')),
+                        capitalizeFirstLetter(i18n.global.t('global.left')),
+                    ],
                     addButton: false,
                     inputs: [
                         {
                             id: '',
                             type: 'text',
                             label: '',
-                            placeholder: t('questions.typeResponse'),
+                            placeholder: i18n.global.t('questions.typeResponse'),
                             value: '',
                         },
                     ],
@@ -363,35 +365,35 @@ export const swipeForm: Form = {
             ],
         },
         {
-            name: t('questions.proposedResponses'),
+            name: i18n.global.t('questions.proposedResponses'),
             inputs: [
                 {
                     id: 'responses',
-                    label: t('questions.card'),
+                    label: i18n.global.t('questions.card'),
                     type: 'repeat',
                     value: [],
                     inputs: [
                         {
                             id: 'label',
                             type: 'text',
-                            label: t('questions.response'),
-                            placeholder: t('questions.typeProposition'),
+                            label: i18n.global.t('questions.response'),
+                            placeholder: i18n.global.t('questions.typeProposition'),
                             value: '',
                         },
                         {
                             id: 'feedback',
                             type: 'textarea',
-                            label: t('questions.explanation'),
-                            placeholder: t('questions.typeExplanation'),
+                            label: i18n.global.t('questions.explanation'),
+                            placeholder: i18n.global.t('questions.typeExplanation'),
                             value: '',
                             collapsible: true,
-                            collapsibleLabel: t('questions.addExplanation'),
+                            collapsibleLabel: i18n.global.t('questions.addExplanation'),
                         },
                         {
                             id: 'value',
                             type: 'hidden',
                             label: '',
-                            placeholder: t('forms.type'),
+                            placeholder: i18n.global.t('forms.type'),
                             value: '',
                         },
                         {
@@ -408,63 +410,63 @@ export const swipeForm: Form = {
             ],
         },
         {
-            name: t('questions.explanation'),
+            name: i18n.global.t('questions.explanation'),
             inputs: [
                 {
                     id: 'explanation',
                     type: 'html',
                     label: '',
                     value: '',
-                    placeholder: t('questions.typeExplanation'),
+                    placeholder: i18n.global.t('questions.typeExplanation'),
                 },
             ],
         },
     ],
-};
+}));
 
-export const listForm: Form = {
+export const listForm: ComputedRef<Form> = computed(() => ({
     type: 'dropdown-list',
-    name: t('questions.types.dropdownList'),
+    name: i18n.global.t('questions.types.dropdownList'),
     icon: 'icon-liste',
     displayFieldIndex: true,
-    buttons: contentButtons,
+    buttons: contentButtons.value,
     fields: [
         {
-            name: t('questions.configuration'),
+            name: i18n.global.t('questions.configuration'),
             inputs: [
                 {
                     id: 'score',
                     type: 'score',
-                    label: t('questions.score'),
+                    label: i18n.global.t('questions.score'),
                     value: 0,
                 },
             ],
         },
         {
-            name: t('questions.question'),
+            name: i18n.global.t('questions.question'),
             inputs: [
                 {
                     id: 'label',
                     type: 'textarea',
-                    label: t('questions.question'),
+                    label: i18n.global.t('questions.question'),
                     value: '',
-                    placeholder: t('questions.askQuestion'),
+                    placeholder: i18n.global.t('questions.askQuestion'),
                 },
                 {
                     id: 'statement',
                     type: 'html-inline',
-                    label: t('questions.instruction'),
+                    label: i18n.global.t('questions.instruction'),
                     value: '',
-                    placeholder: t('questions.instructionPlaceholder'),
+                    placeholder: i18n.global.t('questions.instructionPlaceholder'),
                 },
             ],
         },
         {
-            name: t('questions.proposedChoices'),
+            name: i18n.global.t('questions.proposedChoices'),
             inputs: [
                 {
                     id: 'categories',
-                    label: t('forms.node.choice'),
+                    label: i18n.global.t('forms.node.choice'),
                     type: 'repeat',
                     value: [],
                     inputs: [
@@ -472,7 +474,7 @@ export const listForm: Form = {
                             id: '',
                             type: 'text',
                             label: '',
-                            placeholder: t('questions.typeResponse'),
+                            placeholder: i18n.global.t('questions.typeResponse'),
                             value: '',
                         },
                     ],
@@ -480,35 +482,35 @@ export const listForm: Form = {
             ],
         },
         {
-            name: t('questions.cards'),
+            name: i18n.global.t('questions.cards'),
             inputs: [
                 {
                     id: 'responses',
-                    label: t('questions.card'),
+                    label: i18n.global.t('questions.card'),
                     type: 'repeat',
                     value: [],
                     inputs: [
                         {
                             id: 'label',
                             type: 'text',
-                            label: t('questions.response'),
-                            placeholder: t('questions.typeProposition'),
+                            label: i18n.global.t('questions.response'),
+                            placeholder: i18n.global.t('questions.typeProposition'),
                             value: '',
                         },
                         {
                             id: 'feedback',
                             type: 'textarea',
-                            label: t('questions.explanation'),
-                            placeholder: t('questions.typeExplanation'),
+                            label: i18n.global.t('questions.explanation'),
+                            placeholder: i18n.global.t('questions.typeExplanation'),
                             value: '',
                             collapsible: true,
-                            collapsibleLabel: t('questions.addExplanation'),
+                            collapsibleLabel: i18n.global.t('questions.addExplanation'),
                         },
                         {
                             id: 'value',
                             type: 'hidden',
                             label: '',
-                            placeholder: t('forms.type'),
+                            placeholder: i18n.global.t('forms.type'),
                             value: '',
                         },
                         {
@@ -525,64 +527,64 @@ export const listForm: Form = {
             ],
         },
         {
-            name: t('questions.explanation'),
+            name: i18n.global.t('questions.explanation'),
             inputs: [
                 {
                     id: 'explanation',
                     type: 'html',
                     label: '',
                     value: '',
-                    placeholder: t('questions.typeExplanation'),
+                    placeholder: i18n.global.t('questions.typeExplanation'),
                 },
             ],
         },
     ],
-};
+}));
 
-export const customQuestionForm: Form = {
+export const customQuestionForm: ComputedRef<Form> = computed(() => ({
     type: 'custom',
-    name: t('questions.types.custom'),
+    name: i18n.global.t('questions.types.custom'),
     icon: 'icon-terminal',
     displayFieldIndex: true,
-    buttons: contentButtons,
+    buttons: contentButtons.value,
     fields: [
         {
-            name: t('questions.configuration'),
+            name: i18n.global.t('questions.configuration'),
             inputs: [
                 {
                     id: 'score',
                     type: 'score',
-                    label: t('questions.score'),
+                    label: i18n.global.t('questions.score'),
                     value: 0,
                 },
             ],
         },
         {
-            name: t('questions.question'),
+            name: i18n.global.t('questions.question'),
             inputs: [
                 {
                     id: 'label',
                     type: 'textarea',
-                    label: t('questions.question'),
+                    label: i18n.global.t('questions.question'),
                     value: '',
-                    placeholder: t('questions.askQuestion'),
+                    placeholder: i18n.global.t('questions.askQuestion'),
                 },
                 {
                     id: 'statement',
                     type: 'html-inline',
-                    label: t('questions.instruction'),
+                    label: i18n.global.t('questions.instruction'),
                     value: '',
-                    placeholder: t('questions.instructionPlaceholder'),
+                    placeholder: i18n.global.t('questions.instructionPlaceholder'),
                 },
             ],
         },
         {
-            name: t('questions.template.title'),
+            name: i18n.global.t('questions.template.title'),
             inputs: [
                 {
                     id: 'template',
                     type: 'select',
-                    label: t('questions.template.select'),
+                    label: i18n.global.t('questions.template.select'),
                     value: '',
                     options: [],
                     linkedOptions: 'plugins.*.template',
@@ -590,26 +592,26 @@ export const customQuestionForm: Form = {
             ],
         },
         {
-            name: t('questions.template.data'),
+            name: i18n.global.t('questions.template.data'),
             inputs: [
                 {
                     type: 'repeat',
                     id: 'data',
-                    label: t('questions.template.data'),
+                    label: i18n.global.t('questions.template.data'),
                     value: [],
                     inputs: [
                         {
                             id: 'key',
                             type: 'text',
-                            label: t('questions.template.key'),
-                            placeholder: t('questions.template.key'),
+                            label: i18n.global.t('questions.template.key'),
+                            placeholder: i18n.global.t('questions.template.key'),
                             value: '',
                         },
                         {
                             id: 'value',
                             type: 'textarea',
-                            label: t('questions.template.value'),
-                            placeholder: t('questions.template.value'),
+                            label: i18n.global.t('questions.template.value'),
+                            placeholder: i18n.global.t('questions.template.value'),
                             value: '',
                         },
                     ],
@@ -617,29 +619,36 @@ export const customQuestionForm: Form = {
             ],
         },
         {
-            name: t('questions.response'),
+            name: i18n.global.t('questions.response'),
             inputs: [
                 {
                     id: 'correctResponse',
-                    label: t('questions.response'),
+                    label: i18n.global.t('questions.response'),
                     type: 'text',
                     value: '',
                 },
             ],
         },
         {
-            name: t('questions.explanation'),
+            name: i18n.global.t('questions.explanation'),
             inputs: [
                 {
                     id: 'explanation',
                     type: 'html',
                     label: '',
                     value: '',
-                    placeholder: t('questions.typeExplanation'),
+                    placeholder: i18n.global.t('questions.typeExplanation'),
                 },
             ],
         },
     ],
-};
+}));
 
-export const questionForms: Form[] = [qcmForm, swipeForm, reorderForm, dragDropForm, listForm, customQuestionForm];
+export const questionForms: ComputedRef<Form[]> = computed(() => [
+    qcmForm.value,
+    swipeForm.value,
+    reorderForm.value,
+    dragDropForm.value,
+    listForm.value,
+    customQuestionForm.value,
+]);
diff --git a/src/shared/data/sideBar.data.ts b/src/shared/data/sideBar.data.ts
index 440e4859..2f7cd879 100644
--- a/src/shared/data/sideBar.data.ts
+++ b/src/shared/data/sideBar.data.ts
@@ -1,111 +1,110 @@
 import { SideAction } from '@/src/shared/interfaces';
 import { i18n } from '@/src/i18n/config';
+import { computed, ComputedRef } from 'vue';
 
-const { t } = i18n.global;
-
-export const questions: SideAction[] = [
+export const questions: ComputedRef<SideAction[]> = computed(() => [
     {
         icon: 'icon-qcm',
         type: 'choice',
-        label: t('questions.types.qcm'),
+        label: i18n.global.t('questions.types.qcm'),
     },
     {
         icon: 'icon-dragdrop',
         type: 'drag-and-drop',
-        label: t('questions.types.dragDrop'),
+        label: i18n.global.t('questions.types.dragDrop'),
     },
     {
         icon: 'icon-reorder',
         type: 'reorder',
-        label: t('questions.types.reorder'),
+        label: i18n.global.t('questions.types.reorder'),
     },
     {
         icon: 'icon-swipe',
         type: 'swipe',
-        label: t('questions.types.swipe'),
+        label: i18n.global.t('questions.types.swipe'),
     },
     {
         icon: 'icon-liste',
         type: 'dropdown-list',
-        label: t('questions.types.dropdownList'),
+        label: i18n.global.t('questions.types.dropdownList'),
     },
     {
         icon: 'icon-terminal',
         type: 'custom',
-        label: t('questions.types.custom'),
-    }
-];
+        label: i18n.global.t('questions.types.custom'),
+    },
+]);
 
-const contents: SideAction[] = [
+const contents: ComputedRef<SideAction[]> = computed(() => [
     {
         icon: 'icon-texte',
         type: 'text',
-        label: t('sidebar.content.text'),
-        tooltip: t('sidebar.content.textTooltip'),
+        label: i18n.global.t('sidebar.content.text'),
+        tooltip: i18n.global.t('sidebar.content.textTooltip'),
     },
     {
         icon: 'icon-video',
         type: 'video',
-        label: t('sidebar.content.video'),
-        tooltip: t('sidebar.content.videoTooltip'),
+        label: i18n.global.t('sidebar.content.video'),
+        tooltip: i18n.global.t('sidebar.content.videoTooltip'),
     },
     {
         icon: 'icon-audio',
         type: 'audio',
-        label: t('sidebar.content.audio'),
-        tooltip: t('sidebar.content.audioTooltip'),
+        label: i18n.global.t('sidebar.content.audio'),
+        tooltip: i18n.global.t('sidebar.content.audioTooltip'),
     },
-];
+]);
 
-export const standardActions = [...questions, ...contents];
+export const standardActions = computed(() => [...questions.value, ...contents.value]);
 
-export const standardPages: SideAction[] = [
+export const standardPages: ComputedRef<SideAction[]> = computed(() => [
     {
         icon: 'icon-texte',
         type: 'text',
-        label: t('sidebar.content.text'),
-        tooltip: t('sidebar.content.textTooltip'),
+        label: i18n.global.t('sidebar.content.text'),
+        tooltip: i18n.global.t('sidebar.content.textTooltip'),
     },
     {
         icon: 'icon-video',
         type: 'video',
-        label: t('sidebar.content.video'),
-        tooltip: t('sidebar.content.videoTooltip'),
+        label: i18n.global.t('sidebar.content.video'),
+        tooltip: i18n.global.t('sidebar.content.videoTooltip'),
     },
     {
         icon: 'icon-audio',
         type: 'audio',
-        label: t('sidebar.content.audio'),
-        tooltip: t('sidebar.content.audioTooltip'),
+        label: i18n.global.t('sidebar.content.audio'),
+        tooltip: i18n.global.t('sidebar.content.audioTooltip'),
     },
     {
         icon: 'icon-question',
         type: 'question',
-        label: t('sidebar.pages.question'),
-        tooltip: t('sidebar.pages.questionTooltip'),
+        label: i18n.global.t('sidebar.pages.question'),
+        tooltip: i18n.global.t('sidebar.pages.questionTooltip'),
     },
     {
         icon: 'icon-condition',
         type: 'condition',
-        label: t('sidebar.pages.conditions'),
-        tooltip: t('sidebar.pages.conditionsTooltip'),
+        label: i18n.global.t('sidebar.pages.conditions'),
+        tooltip: i18n.global.t('sidebar.pages.conditionsTooltip'),
     },
     {
         icon: 'icon-condition-legacy',
         type: 'legacy-condition',
-        label: t('sidebar.pages.conditionsLegacy'),
-        tooltip: t('sidebar.pages.conditionsTooltip'),
+        label: i18n.global.t('sidebar.pages.conditionsLegacy'),
+        tooltip: i18n.global.t('sidebar.pages.conditionsTooltip'),
     },
     {
         icon: 'icon-modele',
         type: 'model',
-        label: t('sidebar.pages.model'),
-        tooltip: t('sidebar.pages.modelTooltip'),
+        label: i18n.global.t('sidebar.pages.model'),
+        tooltip: i18n.global.t('sidebar.pages.modelTooltip'),
     },
     {
         icon: 'icon-badge',
         type: 'badge',
-        label: t('sidebar.pages.badge'),
-        tooltip: t('sidebar.pages.badgeTooltip'),
+        label: i18n.global.t('sidebar.pages.badge'),
+        tooltip: i18n.global.t('sidebar.pages.badgeTooltip'),
     },
-];
+]);
diff --git a/src/shared/services/graph/badge.service.ts b/src/shared/services/graph/badge.service.ts
index 568e4cc6..b3264469 100644
--- a/src/shared/services/graph/badge.service.ts
+++ b/src/shared/services/graph/badge.service.ts
@@ -7,27 +7,26 @@ import { elementVerbs, verbs } from '@/src/shared/data';
 import { generateContentId, graphService } from '@/src/shared/services';
 import { Operators } from '@epoc/epoc-types/dist/v2';
 import { i18n } from '@/src/i18n/config';
-
-const { t } = i18n.global;
+import { computed, ComputedRef } from 'vue';
 
 const { findNode } = useVueFlow('main');
 
-export function getVerbs(type: ElementType): Verbs {
+export function getVerbs(type: ElementType): ComputedRef<Verbs> {
     if (!type || !elementVerbs[type]) return;
 
     const verbsKeys = elementVerbs[type];
-    const res: Verbs = {};
+    const res: ComputedRef<Verbs> = computed(() => ({}));
 
     for (const key of verbsKeys) {
-        res[key] = verbs[key];
+        res.value[key] = verbs.value[key];
     }
 
     return res;
 }
 
 export function getValueType(verbKey: string): 'number' | 'boolean' {
-    if (!verbs[verbKey]) return;
-    return verbs[verbKey].valueType;
+    if (!verbs.value[verbKey]) return;
+    return verbs.value[verbKey].valueType;
 }
 
 // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -67,66 +66,66 @@ export function createRule(entry: Condition[]): Rule {
     return { and: rules };
 }
 
-const phraseType = {
-    video: t('badge.phrase.type.video'),
-    chapter: t('badge.phrase.type.chapter'),
-    page: t('badge.phrase.type.page'),
-    html: t('badge.phrase.type.html'),
-    audio: t('badge.phrase.type.audio'),
-    activity: t('badge.phrase.type.activity'),
-    question: t('badge.phrase.type.question'),
-};
+const phraseType = computed(() => ({
+    video: i18n.global.t('badge.phrase.type.video'),
+    chapter: i18n.global.t('badge.phrase.type.chapter'),
+    page: i18n.global.t('badge.phrase.type.page'),
+    html: i18n.global.t('badge.phrase.type.html'),
+    audio: i18n.global.t('badge.phrase.type.audio'),
+    activity: i18n.global.t('badge.phrase.type.activity'),
+    question: i18n.global.t('badge.phrase.type.question'),
+}));
 
-const phraseVerb = {
+const phraseVerb = computed(() => ({
     started: {
-        true: t('badge.phrase.verb.started.true'),
-        false: t('badge.phrase.verb.started.false'),
+        true: i18n.global.t('badge.phrase.verb.started.true'),
+        false: i18n.global.t('badge.phrase.verb.started.false'),
     },
     completed: {
-        true: t('badge.phrase.verb.completed.true'),
-        false: t('badge.phrase.verb.completed.false'),
+        true: i18n.global.t('badge.phrase.verb.completed.true'),
+        false: i18n.global.t('badge.phrase.verb.completed.false'),
     },
     viewed: {
-        true: t('badge.phrase.verb.viewed.true'),
-        false: t('badge.phrase.verb.viewed.false'),
+        true: i18n.global.t('badge.phrase.verb.viewed.true'),
+        false: i18n.global.t('badge.phrase.verb.viewed.false'),
     },
     read: {
-        true: t('badge.phrase.verb.read.true'),
-        false: t('badge.phrase.verb.read.false'),
+        true: i18n.global.t('badge.phrase.verb.read.true'),
+        false: i18n.global.t('badge.phrase.verb.read.false'),
     },
     played: {
-        true: t('badge.phrase.verb.played.true'),
-        false: t('badge.phrase.verb.played.false'),
+        true: i18n.global.t('badge.phrase.verb.played.true'),
+        false: i18n.global.t('badge.phrase.verb.played.false'),
     },
     watched: {
-        true: t('badge.phrase.verb.watched.true'),
-        false: t('badge.phrase.verb.watched.false'),
+        true: i18n.global.t('badge.phrase.verb.watched.true'),
+        false: i18n.global.t('badge.phrase.verb.watched.false'),
     },
     listened: {
-        true: t('badge.phrase.verb.listened.true'),
-        false: t('badge.phrase.verb.listened.false'),
+        true: i18n.global.t('badge.phrase.verb.listened.true'),
+        false: i18n.global.t('badge.phrase.verb.listened.false'),
     },
     attempted: {
-        true: t('badge.phrase.verb.attempted.true'),
-        false: t('badge.phrase.verb.attempted.false'),
+        true: i18n.global.t('badge.phrase.verb.attempted.true'),
+        false: i18n.global.t('badge.phrase.verb.attempted.false'),
     },
     passed: {
-        true: t('badge.phrase.verb.passed.true'),
-        false: t('badge.phrase.verb.passed.false'),
+        true: i18n.global.t('badge.phrase.verb.passed.true'),
+        false: i18n.global.t('badge.phrase.verb.passed.false'),
     },
-    scored: t('badge.phrase.verb.scored'),
-};
+    scored: i18n.global.t('badge.phrase.verb.scored'),
+}));
 
 export function createPhrase(condition: Condition, elementType: ElementType) {
     const { verb, value } = condition;
     let firstPart: string;
     if (verb === 'scored') {
-        firstPart = t('badge.phrase.scored', { value, verb: phraseVerb[verb] });
+        firstPart = i18n.global.t('badge.phrase.scored', { value, verb: phraseVerb[verb] });
     } else {
-        firstPart = `${phraseVerb[verb][value]}`;
+        firstPart = `${phraseVerb.value[verb][value]}`;
     }
 
-    return `${firstPart} ${phraseType[elementType]}`;
+    return `${firstPart} ${phraseType.value[elementType]}`;
 }
 
 export function getConnectedBadges(contentId: string): Badge[] {
diff --git a/src/shared/services/graph/content.service.ts b/src/shared/services/graph/content.service.ts
index 288cb409..ac562fd1 100644
--- a/src/shared/services/graph/content.service.ts
+++ b/src/shared/services/graph/content.service.ts
@@ -92,7 +92,9 @@ export function unselectAllContents(): void {
 }
 
 export function getContentDefaultValues(type: string) {
-    const form = [...forms.questionForms, ...forms.elementForms, ...forms.nodeForms].find((f) => f.type === type);
+    const form = [...forms.questionForms.value, ...forms.elementForms.value, ...forms.nodeForms.value].find(
+        (f) => f.type === type,
+    );
 
     return form.fields.reduce((acc, field) => {
         // eslint-disable-next-line @typescript-eslint/no-explicit-any
diff --git a/src/shared/services/graph/element.service.ts b/src/shared/services/graph/element.service.ts
index 58ef472d..426be115 100644
--- a/src/shared/services/graph/element.service.ts
+++ b/src/shared/services/graph/element.service.ts
@@ -94,6 +94,8 @@ export function getElementType(contentId: string): ElementType {
         return getContentType(contentId);
     }
 
+    console.log('type', node.type);
+
     return node.type as ElementType;
 }
 
diff --git a/src/shared/services/import.service.ts b/src/shared/services/import.service.ts
index 46fe0e72..5e8e279d 100644
--- a/src/shared/services/import.service.ts
+++ b/src/shared/services/import.service.ts
@@ -17,15 +17,15 @@ import { saveState } from '@/src/shared/services/undoRedo.service';
 import { useEditorStore, useGraphStore } from '@/src/shared/stores';
 
 const mapType = {
-    video: standardPages.find((s) => s.type === 'video'),
-    html: standardPages.find((s) => s.type === 'text'),
-    audio: standardPages.find((s) => s.type === 'audio'),
-    'multiple-choice': questions.find((s) => s.type === 'choice'),
-    choice: questions.find((s) => s.type === 'choice'),
-    'drag-and-drop': questions.find((s) => s.type === 'drag-and-drop'),
-    'dropdown-list': questions.find((s) => s.type === 'dropdown-list'),
-    swipe: questions.find((s) => s.type === 'swipe'),
-    reorder: questions.find((s) => s.type === 'reorder'),
+    video: standardPages.value.find((s) => s.type === 'video'),
+    html: standardPages.value.find((s) => s.type === 'text'),
+    audio: standardPages.value.find((s) => s.type === 'audio'),
+    'multiple-choice': questions.value.find((s) => s.type === 'choice'),
+    choice: questions.value.find((s) => s.type === 'choice'),
+    'drag-and-drop': questions.value.find((s) => s.type === 'drag-and-drop'),
+    'dropdown-list': questions.value.find((s) => s.type === 'dropdown-list'),
+    swipe: questions.value.find((s) => s.type === 'swipe'),
+    reorder: questions.value.find((s) => s.type === 'reorder'),
 };
 
 export function createGraphEpocFromData(epoc: EpocV1) {
@@ -70,7 +70,7 @@ export function createGraphEpocFromData(epoc: EpocV1) {
                 const contentElement = newQuestion(epoc, id, qid);
                 contentElements.push(contentElement);
             } else if (content.type === 'choice') {
-                const action = standardPages.find((s) => s.type === 'legacy-condition');
+                const action = standardPages.value.find((s) => s.type === 'legacy-condition');
                 const choiceResolver = (content as ChoiceCondition).conditionResolver;
                 const contentElement = {
                     id: generateId(),
diff --git a/src/shared/stores/editorStore.ts b/src/shared/stores/editorStore.ts
index e5f69b2d..2d91bc25 100644
--- a/src/shared/stores/editorStore.ts
+++ b/src/shared/stores/editorStore.ts
@@ -100,8 +100,8 @@ export const useEditorStore = defineStore('editor', {
 
         // Data
         pageModels: [],
-        questions: questions,
-        standardPages: standardPages,
+        questions: questions.value,
+        standardPages: standardPages.value,
 
         // Modal
         conditionModal: false,
@@ -135,7 +135,7 @@ export const useEditorStore = defineStore('editor', {
 
         sideMenuOpen(): boolean {
             return this.modelMenu || this.badgeMenu;
-        }
+        },
     },
 
     actions: {
@@ -163,7 +163,7 @@ export const useEditorStore = defineStore('editor', {
             this.openedElementId = null;
 
             setTimeout(() => {
-                this.formPanel.form = formsModel.find((form) => form.type === 'badge');
+                this.formPanel.form = formsModel.value.find((form) => form.type === 'badge');
             });
 
             if (scrollPosY) this.scrollFormPanel(scrollPosY);
@@ -183,7 +183,7 @@ export const useEditorStore = defineStore('editor', {
             //? To be sure the view is notified of closing / reopening
             this.formPanel.form = null;
             setTimeout(() => {
-                this.formPanel.form = formsModel.find((form) => form.type === formType);
+                this.formPanel.form = formsModel.value.find((form) => form.type === formType);
             });
 
             if (scrollPosY) this.scrollFormPanel(scrollPosY);
@@ -259,9 +259,9 @@ export const useEditorStore = defineStore('editor', {
         },
 
         toggleSideMenu(type: SideMenu) {
-            for(const key in sideMenus) {
-                this[sideMenus[key]] = (key === type) ? !this[sideMenus[key]] : false;
+            for (const key in sideMenus) {
+                this[sideMenus[key]] = key === type ? !this[sideMenus[key]] : false;
             }
-        }
+        },
     },
 });
-- 
GitLab


From b7efd3dcb66b686adecc8568ee52a434d8a3f094 Mon Sep 17 00:00:00 2001
From: VIAUD Nathan <nathan.viaud@inria.fr>
Date: Fri, 21 Mar 2025 13:41:37 +0100
Subject: [PATCH 13/18] WIP: translate

---
 .../sideBar/components/SideActions.vue        | 29 ++++++++++---------
 src/i18n/en.ts                                |  2 +-
 src/i18n/fr.ts                                |  3 +-
 src/shared/data/forms/contentForm.data.ts     |  2 +-
 4 files changed, 19 insertions(+), 17 deletions(-)

diff --git a/src/features/sideBar/components/SideActions.vue b/src/features/sideBar/components/SideActions.vue
index bb26f0a5..ce3bcdb2 100644
--- a/src/features/sideBar/components/SideActions.vue
+++ b/src/features/sideBar/components/SideActions.vue
@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import { SideAction } from '@/src/shared/interfaces';
-import { ref } from 'vue';
+import { ref, computed } from 'vue';
 import ContentButton from '@/src/components/ContentButton.vue';
 import { useEditorStore } from '@/src/shared/stores';
 import { moveGuard } from '@/src/shared/utils/draggable';
@@ -9,15 +9,18 @@ import SettingsModal from '@/src/features/settings/SettingsModal.vue';
 
 const editorStore = useEditorStore();
 
-const standardContent = editorStore.standardPages.filter(({ type }) => {
-    const filteredPages = ['legacy-condition', 'condition', 'question', 'model', 'badge'];
-    const prodFilteredPages = env.isDev ? [] : [];
-    return ![...filteredPages, ...prodFilteredPages].includes(type);
-});
-const questionContent = editorStore.standardPages.find(({ type }) => type === 'question');
-const conditionContent = editorStore.standardPages.find(({ type }) => type === 'condition');
-const modelContent = editorStore.standardPages.find(({ type }) => type === 'model');
-const badgeContent = editorStore.standardPages.find(({ type }) => type === 'badge');
+const standardContent = computed(() =>
+    editorStore.standardPages.filter(({ type }) => {
+        const filteredPages = ['legacy-condition', 'condition', 'question', 'model', 'badge'];
+        const prodFilteredPages = env.isDev ? [] : [];
+        return ![...filteredPages, ...prodFilteredPages].includes(type);
+    }),
+);
+
+const questionContent = computed(() => editorStore.standardPages.find(({ type }) => type === 'question'));
+const conditionContent = computed(() => editorStore.standardPages.find(({ type }) => type === 'condition'));
+const modelContent = computed(() => editorStore.standardPages.find(({ type }) => type === 'model'));
+const badgeContent = computed(() => editorStore.standardPages.find(({ type }) => type === 'badge'));
 
 const dragging = ref(false);
 
@@ -56,7 +59,6 @@ function showTemplateMenu() {
 function showBadgeMenu() {
     editorStore.toggleSideMenu('badge');
 }
-
 </script>
 
 <template>
@@ -220,7 +222,9 @@ hr {
     transition: all 0.15s ease-in-out;
 }
 
-.questions-list, .actions-list, .contents-list {
+.questions-list,
+.actions-list,
+.contents-list {
     display: flex;
     flex-direction: column;
     gap: 1rem;
@@ -273,5 +277,4 @@ hr {
 .model-menu {
     flex: 1;
 }
-
 </style>
diff --git a/src/i18n/en.ts b/src/i18n/en.ts
index f022404d..68d26a3e 100644
--- a/src/i18n/en.ts
+++ b/src/i18n/en.ts
@@ -146,7 +146,7 @@ export const enMessages = {
             video: {
                 label: 'Video',
                 placeholder: 'Add a video',
-                hint: 'Recommended format:',
+                hint: 'Recommended format: {format}',
             },
             audio: {
                 label: 'Audio track',
diff --git a/src/i18n/fr.ts b/src/i18n/fr.ts
index fddafbd6..4bf4bcbe 100644
--- a/src/i18n/fr.ts
+++ b/src/i18n/fr.ts
@@ -131,7 +131,6 @@ export const frMessages = {
         lastSave: 'Dernière sauvegarde :',
         new: 'Nouvel ePoc',
     },
-
     forms: {
         type: 'Saisissez...',
         badge: {
@@ -147,7 +146,7 @@ export const frMessages = {
             video: {
                 label: 'Vidéo',
                 placeholder: 'Ajouter une vidéo',
-                hint: 'Format recommandé :',
+                hint: 'Format recommandé : {format}',
             },
             audio: {
                 label: 'Piste audio',
diff --git a/src/shared/data/forms/contentForm.data.ts b/src/shared/data/forms/contentForm.data.ts
index 83e62ac4..ecc3b24c 100644
--- a/src/shared/data/forms/contentForm.data.ts
+++ b/src/shared/data/forms/contentForm.data.ts
@@ -41,7 +41,7 @@ export const videoForm: ComputedRef<Form> = computed(() => {
                         placeholder: i18n.global.t('forms.content.video.placeholder'),
                         value: '',
                         accept: '.mp4',
-                        hint: i18n.global.t('forms.content.video.hint'),
+                        hint: i18n.global.t('forms.content.video.hint', { format: '16:9 (720x480)' }),
                     },
                     {
                         id: 'summary',
-- 
GitLab


From 99933b67377f3722229236448138a736eae6cf1d Mon Sep 17 00:00:00 2001
From: VIAUD Nathan <nathan.viaud@inria.fr>
Date: Fri, 21 Mar 2025 14:09:16 +0100
Subject: [PATCH 14/18] WIP translate: setup settings store

---
 src/App.vue                             | 10 ++++++++++
 src/features/ePocFlow/ePocFlow.vue      |  9 ++++++---
 src/features/settings/SettingsModal.vue |  8 +++++---
 src/main.ts                             |  3 ++-
 src/router.ts                           |  4 +++-
 src/shared/stores/settingsStore.ts      |  3 +++
 src/views/EditorPage.vue                |  8 ++++++++
 src/views/LandingPage.vue               |  9 +++++++--
 8 files changed, 44 insertions(+), 10 deletions(-)

diff --git a/src/App.vue b/src/App.vue
index 7dcb7355..28c7eef7 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -1,7 +1,17 @@
 <script setup lang="ts">
+import { onMounted } from 'vue';
+import { useSettingsStore } from './shared/stores';
+
 // remove this when implementing dark mode
 const root = document.querySelector(':root');
 root.setAttribute('color-scheme', 'light');
+
+onMounted(() => {
+    // const settingsStore = useSettingsStore();
+    // if (!settingsStore.initialized) {
+    //     settingsStore.init();
+    // }
+});
 </script>
 
 <template>
diff --git a/src/features/ePocFlow/ePocFlow.vue b/src/features/ePocFlow/ePocFlow.vue
index 81eb6a6e..5d3a268c 100644
--- a/src/features/ePocFlow/ePocFlow.vue
+++ b/src/features/ePocFlow/ePocFlow.vue
@@ -25,7 +25,8 @@ import {
     addPage,
     createPageFromContent,
     graphCopy,
-    getSelectedNodes, handleChapterDrag
+    getSelectedNodes,
+    handleChapterDrag,
 } from '@/src/shared/services/graph';
 import { saveState, saveGivenState, getCurrentState } from '@/src/shared/services/undoRedo.service';
 import { closeAllPanels, closeFormPanel, graphService, getSelectedEdges } from '@/src/shared/services';
@@ -120,7 +121,7 @@ function connect(event: Connection) {
     }
 
     const otherEdge = getConnectedEdges([targetNode], edges.value).find(
-        (edge) => edge.target === targetNode.id && edge.source !== sourceNode.id,
+        (edge) => edge.target === targetNode.id && edge.source !== sourceNode.id
     );
 
     if (otherEdge) {
@@ -188,7 +189,9 @@ function onContextMenu(event: MouseEvent) {
 function onSelectionContextMenu() {
     const selectedNodes = getSelectedNodes();
     const selectedEdges = getSelectedEdges();
-    graphService.openContextMenu('selection', { selection: JSON.stringify({ pages: selectedNodes, edges: selectedEdges }) });
+    graphService.openContextMenu('selection', {
+        selection: JSON.stringify({ pages: selectedNodes, edges: selectedEdges }),
+    });
 }
 
 function onPaneReady() {
diff --git a/src/features/settings/SettingsModal.vue b/src/features/settings/SettingsModal.vue
index 19209bfd..b119d3ac 100644
--- a/src/features/settings/SettingsModal.vue
+++ b/src/features/settings/SettingsModal.vue
@@ -11,9 +11,11 @@ const modal = ref(null);
 const settingsStore = useSettingsStore();
 const spellcheck = ref(false);
 
-onMounted(() => {
-    settingsStore.init();
-});
+// onMounted(() => {
+//     if (!settingsStore.init) {
+//         settingsStore.init();
+//     }
+// });
 
 function save() {
     settingsStore.setSettings(spellcheck.value);
diff --git a/src/main.ts b/src/main.ts
index 17fe8774..c5f78095 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -9,8 +9,9 @@ import draggable from 'vuedraggable';
 import { i18n } from './i18n/config';
 
 const app = createApp(App);
+const pinia = createPinia();
+app.use(pinia);
 app.use(router);
-app.use(createPinia());
 app.use(VueTippy);
 app.use(i18n);
 app.component('VueDraggable', draggable);
diff --git a/src/router.ts b/src/router.ts
index a06482a0..c263ba9d 100644
--- a/src/router.ts
+++ b/src/router.ts
@@ -1,6 +1,6 @@
 import { createRouter, createWebHashHistory } from 'vue-router';
 
-export const router = createRouter({
+const router = createRouter({
     history: createWebHashHistory(),
     routes: [
         {
@@ -17,3 +17,5 @@ export const router = createRouter({
         },
     ],
 });
+
+export { router };
diff --git a/src/shared/stores/settingsStore.ts b/src/shared/stores/settingsStore.ts
index 31ec72d6..8936e422 100644
--- a/src/shared/stores/settingsStore.ts
+++ b/src/shared/stores/settingsStore.ts
@@ -5,16 +5,19 @@ import { i18n } from '@/src/i18n/config';
 
 interface SettingsState {
     settings?: Settings;
+    initialized: boolean;
 }
 
 export const useSettingsStore = defineStore('settings', {
     state: (): SettingsState => ({
         settings: undefined,
+        initialized: false,
     }),
 
     actions: {
         init() {
             editorService.fetchSettings();
+            this.initialized = true;
         },
 
         initSettings(settings: Settings) {
diff --git a/src/views/EditorPage.vue b/src/views/EditorPage.vue
index 13fe3fb1..42563c86 100644
--- a/src/views/EditorPage.vue
+++ b/src/views/EditorPage.vue
@@ -14,6 +14,8 @@ import { setupContextMenu } from '../shared/services/contextMenu.service';
 import { computed } from 'vue';
 import ModelMenu from '@/src/features/sideBar/components/ModelMenu.vue';
 import BadgeMenu from '@/src/features/sideBar/components/BadgeMenu.vue';
+import { onMounted } from 'vue';
+import { useSettingsStore } from '@/src/shared/stores';
 
 const editorStore = useEditorStore();
 
@@ -86,6 +88,12 @@ function onRemoveCursor() {
 
 const editorDisplay = computed(() => (editorStore.selectNodeMode ? 'editor-flex' : 'editor-grid'));
 const sideMenuOpen = computed(() => (editorStore.sideMenuOpen ? 'side-menu-open' : ''));
+
+//? For some reason, this code doesn't work in App.vue
+const settingsStore = useSettingsStore();
+if (!settingsStore.initialized) {
+    settingsStore.init();
+}
 </script>
 
 <template>
diff --git a/src/views/LandingPage.vue b/src/views/LandingPage.vue
index 0bd10472..cd34a15f 100644
--- a/src/views/LandingPage.vue
+++ b/src/views/LandingPage.vue
@@ -1,14 +1,19 @@
 <script setup lang="ts">
-import { useEditorStore } from '@/src/shared/stores';
+import { useEditorStore, useSettingsStore } from '@/src/shared/stores';
 import { editorService } from '@/src/shared/services';
 import { ePocProject } from '@/src/shared/interfaces';
 import ChoiceModal from '@/src/components/ChoiceModal.vue';
 import { createGraphFromImport } from '@/src/shared/services/import.service';
+import { onMounted } from 'vue';
 
 const editorStore = useEditorStore();
-
 editorService.setup();
 
+const settingsStore = useSettingsStore();
+if (!settingsStore.initialized) {
+    settingsStore.init();
+}
+
 function pickProject() {
     editorService.pickEpocProject();
 }
-- 
GitLab


From 0d8d29a73825a61e9b4ecb0885e4a19f7dc27e72 Mon Sep 17 00:00:00 2001
From: VIAUD Nathan <nathan.viaud@inria.fr>
Date: Fri, 21 Mar 2025 14:19:52 +0100
Subject: [PATCH 15/18] WIP: translate: fix since

---
 src/features/topBar/TopBar.vue | 15 +++------------
 1 file changed, 3 insertions(+), 12 deletions(-)

diff --git a/src/features/topBar/TopBar.vue b/src/features/topBar/TopBar.vue
index cb18614f..a76829f4 100644
--- a/src/features/topBar/TopBar.vue
+++ b/src/features/topBar/TopBar.vue
@@ -13,12 +13,6 @@ const undoRedoStore = useUndoRedoStore();
 
 const { zoomTo, fitView, onViewportChangeEnd } = useVueFlow('main');
 
-editorStore.$subscribe(() => {
-    savedSince.value = since(editorStore.currentProject.modified);
-});
-
-const savedSince = ref(since(editorStore.currentProject.modified));
-
 const zoom = ref(1);
 const zoomString = computed(() => (zoom.value === 0 ? t('header.adjust') : `${Math.round(zoom.value * 100)}%`));
 
@@ -37,7 +31,8 @@ function updateZoom(val: number) {
     }
 }
 
-function since(date: string) {
+const savedSince = computed(() => {
+    const date = editorStore.currentProject.modified;
     if (!date) return t('header.never');
     const milliseconds = Math.abs(Date.now() - new Date(date).getTime());
     const secs = Math.floor(Math.abs(milliseconds) / 1000);
@@ -61,11 +56,7 @@ function since(date: string) {
             : 'less than a minute') + ' ago'
         );
     }
-}
-
-setInterval(() => {
-    savedSince.value = since(editorStore.currentProject.modified);
-}, 60000);
+});
 
 function separateFilePath(filepath: string) {
     const file = filepath.split('/').pop();
-- 
GitLab


From 40aab653b62c168ee22cdbecc43db68c8aea4fd5 Mon Sep 17 00:00:00 2001
From: VIAUD Nathan <nathan.viaud@inria.fr>
Date: Mon, 24 Mar 2025 16:40:42 +0100
Subject: [PATCH 16/18] WIP: translate electron menu

---
 electron/components/window.js     | 112 ++++++++++++++++++++--------
 electron/i18n/en/translation.json |  44 +++++++++++
 electron/i18n/fr/translation.json |  44 +++++++++++
 electron/i18n/i18next.config.js   |  18 +++++
 package-lock.json                 | 120 +++++++++++++++++++++++-------
 package.json                      |   4 +-
 6 files changed, 284 insertions(+), 58 deletions(-)
 create mode 100644 electron/i18n/en/translation.json
 create mode 100644 electron/i18n/fr/translation.json
 create mode 100644 electron/i18n/i18next.config.js

diff --git a/electron/components/window.js b/electron/components/window.js
index 48dc1d05..8a98b070 100644
--- a/electron/components/window.js
+++ b/electron/components/window.js
@@ -13,10 +13,19 @@ const {
     createPreview,
     createGlobalPreview,
 } = require('./file');
+const i18n = require('../i18n/i18next.config.js');
 
 const Store = require('electron-store');
 const electronStore = new Store();
 
+i18n.on('initialized', () => {
+    console.log('i18next initialized successfully');
+});
+
+i18n.on('failedLoading', (lng, ns, msg) => {
+    console.error('i18next failed to load:', msg);
+});
+
 /**
  * Create the app main window
  * @returns {Electron.CrossProcessExports.BrowserWindow}
@@ -104,7 +113,7 @@ const setupWindow = function (window, filepath) {
     if (filepath) {
         window.webContents.send(
             'epocProjectPicked',
-            JSON.stringify({ name: null, modified: null, filepath: filepath, workdir: null }),
+            JSON.stringify({ name: null, modified: null, filepath: filepath, workdir: null })
         );
     }
 };
@@ -124,44 +133,62 @@ const createNewWindow = () => {
 const setupMenu = () => {
     const mainMenuTemplate = [
         {
-            label: 'App',
+            label: i18n.t('menu.app.label'),
             submenu: [
-                { label: 'À propos', role: 'about' },
+                { label: i18n.t('menu.app.about'), role: 'about' },
                 {
-                    label: 'Quitter',
+                    label: i18n.t('menu.app.quit'),
                     accelerator: 'CmdOrCtrl+Q',
                     click: function () {
                         app.quit();
                     },
                 },
+                { type: 'separator' },
+                {
+                    label: i18n.t('menu.app.lang'),
+                    submenu: [
+                        {
+                            label: 'English',
+                            type: 'radio',
+                            checked: i18n.language === 'en',
+                            click: () => changeLanguage('en'),
+                        },
+                        {
+                            label: 'Français',
+                            type: 'radio',
+                            checked: i18n.language === 'fr',
+                            click: () => changeLanguage('fr'),
+                        },
+                    ],
+                },
             ],
         },
         {
-            label: 'Fichier',
+            label: i18n.t('menu.file.label'),
             submenu: [
                 {
-                    label: 'Nouveau',
+                    label: i18n.t('menu.file.new'),
                     accelerator: 'CmdOrCtrl+N',
                     click: function () {
                         sendToFrontend(BrowserWindow.getFocusedWindow(), 'epocProjectNew');
                     },
                 },
                 {
-                    label: 'Nouvelle fenêtre',
+                    label: i18n.t('menu.file.newWindow'),
                     click: function () {
                         ipcMain.emit('newWindow');
                     },
                 },
                 { type: 'separator' },
                 {
-                    label: 'Ouvrir',
+                    label: i18n.t('menu.file.open'),
                     accelerator: 'CmdOrCtrl+O',
                     click: function () {
                         sendToFrontend(BrowserWindow.getFocusedWindow(), 'epocProjectPicked', pickEpocProject());
                     },
                 },
                 {
-                    label: 'Ouvrir dans une nouvelle fenêtre',
+                    label: i18n.t('menu.file.openInNewWindow'),
                     click: () => {
                         const newWindow = createNewWindow();
                         const project = pickEpocProject();
@@ -171,7 +198,7 @@ const setupMenu = () => {
                     },
                 },
                 {
-                    label: 'Projet récents',
+                    label: i18n.t('menu.file.latest'),
                     submenu: [
                         ...getRecentFiles().map((project) => {
                             return {
@@ -184,7 +211,7 @@ const setupMenu = () => {
                     ],
                 },
                 {
-                    label: 'Importer un fichier .epoc',
+                    label: i18n.t('menu.file.import'),
                     click: async function () {
                         sendToFrontend(BrowserWindow.getFocusedWindow(), 'epocImportPicked');
                         const project = await pickEpocToImport();
@@ -222,7 +249,7 @@ const setupMenu = () => {
                 { type: 'separator' },
                 {
                     id: 'save',
-                    label: 'Sauvegarder',
+                    label: i18n.t('menu.file.save'),
                     accelerator: 'CmdOrCtrl+S',
                     enabled: !!(
                         store.state.projects[BrowserWindow.getFocusedWindow()?.id] &&
@@ -240,7 +267,7 @@ const setupMenu = () => {
                 },
                 {
                     id: 'saveAs',
-                    label: 'Sauvegarder sous...',
+                    label: i18n.t('menu.file.saveAs'),
                     accelerator: 'Shift+CmdOrCtrl+S',
                     enabled: !!(
                         store.state.projects[BrowserWindow.getFocusedWindow()?.id] &&
@@ -249,7 +276,7 @@ const setupMenu = () => {
                     click: async function () {
                         sendToFrontend(BrowserWindow.getFocusedWindow(), 'epocProjectSaving');
                         const result = await saveAsEpocProject(
-                            store.state.projects[BrowserWindow.getFocusedWindow().id],
+                            store.state.projects[BrowserWindow.getFocusedWindow().id]
                         );
                         if (result) {
                             updateSavedProject(BrowserWindow.getFocusedWindow().webContents, result);
@@ -261,40 +288,40 @@ const setupMenu = () => {
             ],
         },
         {
-            label: 'Édition',
+            label: i18n.t('menu.edit.label'),
             submenu: [
                 {
-                    label: 'Annuler',
+                    label: i18n.t('menu.edit.undo'),
                     accelerator: 'CmdOrCtrl+Z',
                     click: function () {
                         sendToFrontend(BrowserWindow.getFocusedWindow(), 'undo');
                     },
                 },
                 {
-                    label: 'Rétablir',
+                    label: i18n.t('menu.edit.redo'),
                     accelerator: process.platform === 'darwin' ? 'Shift+CmdOrCtrl+Z' : 'CmdOrCtrl+Y',
                     click: function () {
                         sendToFrontend(BrowserWindow.getFocusedWindow(), 'redo');
                     },
                 },
                 { type: 'separator' },
-                { label: 'Couper', accelerator: 'CmdOrCtrl+X', role: 'cut' },
-                { label: 'Copier', accelerator: 'CmdOrCtrl+C', role: 'copy' },
-                { label: 'Coller', accelerator: 'CmdOrCtrl+V', role: 'paste' },
-                { label: 'Tout sélectionner', accelerator: 'CmdOrCtrl+A', role: 'selectAll' },
+                { label: i18n.t('menu.edit.cut'), accelerator: 'CmdOrCtrl+X', role: 'cut' },
+                { label: i18n.t('menu.edit.copy'), accelerator: 'CmdOrCtrl+C', role: 'copy' },
+                { label: i18n.t('menu.edit.paste'), accelerator: 'CmdOrCtrl+V', role: 'paste' },
+                { label: i18n.t('menu.edit.selectAll'), accelerator: 'CmdOrCtrl+A', role: 'selectAll' },
             ],
         },
         {
-            label: 'Aperçu',
+            label: i18n.t('menu.preview.label'),
             submenu: [
                 {
-                    label: "Lancer l'aperçu",
+                    label: i18n.t('menu.preview.start'),
                     click: function () {
                         createPreview(store.state.projects[BrowserWindow.getFocusedWindow().id].workdir);
                     },
                 },
                 {
-                    label: "Lancer l'aperçu global",
+                    label: i18n.t('menu.preview.global'),
                     click: function () {
                         createGlobalPreview(store.state.projects[BrowserWindow.getFocusedWindow().id].workdir);
                     },
@@ -302,31 +329,33 @@ const setupMenu = () => {
             ],
         },
         {
-            label: 'Aide',
+            label: i18n.t('menu.help.label'),
             submenu: [
                 {
-                    label: 'Documentation',
+                    label: i18n.t('menu.help.documentation'),
                     click: async function () {
                         await shell.openExternal('https://epoc.inria.fr/guide/user/getting-started/');
                     },
                 },
                 {
-                    label: 'Signaler un problème',
+                    label: i18n.t('menu.help.reportIssue'),
                     click: async function () {
                         const isDev = process.env.IS_DEV === 'true';
 
-                        const emailSubject = 'Aide éditeur';
+                        const emailSubject = i18n.t('menu.help.mailSubject');
                         const emailRecipient = 'ill-ePoc-contact@inria.fr';
                         let emailBody = '';
                         if (isDev) {
                             const appVersion = app.getVersion();
                             emailBody = encodeURIComponent(
-                                `Version: ${appVersion}\n---\n\nDécrivez votre problème ci-dessous:\n\n`,
+                                `Version: ${appVersion}\n---\n\n${i18n.t('menu.help.mailBody')}\n\n`
                             );
                         } else {
                             const appInfo = require('../../dist/appInfo.json');
                             emailBody = encodeURIComponent(
-                                `Version: ${appInfo.version}\nBuild: ${appInfo.buildNumber}\n ---\n\nDécrivez votre problème ci-dessous:\n\n`,
+                                `Version: ${appInfo.version}\nBuild: ${appInfo.buildNumber}\n ---\n\n${i18n.t(
+                                    'menu.help.mailBody'
+                                )}\n\n`
                             );
                         }
 
@@ -337,14 +366,14 @@ const setupMenu = () => {
                 },
                 { type: 'separator' },
                 {
-                    label: 'Outils de développement',
+                    label: i18n.t('menu.devTools'),
                     accelerator: 'CmdOrCtrl+D',
                     click: function () {
                         BrowserWindow.getFocusedWindow().webContents.toggleDevTools();
                     },
                 },
                 {
-                    label: 'Recharger',
+                    label: i18n.t('menu.reload'),
                     accelerator: 'CmdOrCtrl+R',
                     click: function () {
                         BrowserWindow.getFocusedWindow().webContents.reload();
@@ -373,3 +402,22 @@ module.exports = {
     setupWindow,
     createNewWindow,
 };
+
+const changeLanguage = (lng) => {
+    i18n.changeLanguage(lng, (err) => {
+        if (err) {
+            console.error('Error changing language:', err);
+            return;
+        }
+        // Save language preference
+        electronStore.set('language', lng);
+        // Refresh the menu
+        setupMenu();
+    });
+};
+
+// Initialize language from store
+const storedLanguage = electronStore.get('language');
+if (storedLanguage) {
+    i18n.changeLanguage(storedLanguage);
+}
diff --git a/electron/i18n/en/translation.json b/electron/i18n/en/translation.json
new file mode 100644
index 00000000..985c60eb
--- /dev/null
+++ b/electron/i18n/en/translation.json
@@ -0,0 +1,44 @@
+{
+    "menu": {
+        "app": {
+            "label": "App",
+            "about": "About",
+            "quit": "Quit",
+            "lang": "Language"
+        },
+        "file": {
+            "label": "File",
+            "new": "New",
+            "newWindow": "New Window",
+            "open": "Open",
+            "openInNewWindow": "Open in New Window",
+            "latest": "Recent Projects",
+            "import": "Import .epoc File",
+            "save": "Save",
+            "saveAs": "Save As..."
+        },
+        "edit": {
+            "label": "Edit",
+            "undo": "Undo",
+            "redo": "Redo",
+            "cut": "Cut",
+            "copy": "Copy",
+            "paste": "Paste",
+            "selectAll": "Select All"
+        },
+        "preview": {
+            "label": "Preview",
+            "start": "Start Preview",
+            "global": "Start Global Preview"
+        },
+        "help": {
+            "label": "Help",
+            "documentation": "Documentation",
+            "reportIssue": "Report Issue",
+            "mailSubject": "Editor Help",
+            "mailBody": "Describe your issue below:"
+        },
+        "devTools": "Developer Tools",
+        "reload": "Reload"
+    }
+}
diff --git a/electron/i18n/fr/translation.json b/electron/i18n/fr/translation.json
new file mode 100644
index 00000000..7068ba96
--- /dev/null
+++ b/electron/i18n/fr/translation.json
@@ -0,0 +1,44 @@
+{
+    "menu": {
+        "app": {
+            "label:": "App",
+            "about": "À propos",
+            "quit": "Quitter",
+            "lang": "Langage"
+        },
+        "file": {
+            "label": "Fichier",
+            "new": "Nouveau",
+            "newWindow": "Nouvelle fenêtre",
+            "open": "Ouvrir",
+            "openInNewWindow": "Ouvrir dans une nouvelle fenêtre",
+            "latest": "Projets récents",
+            "import": "Importer un fichier .epoc",
+            "save": "Sauvegarder",
+            "saveAs": "Sauvegarder sous..."
+        },
+        "edit": {
+            "label": "Édition",
+            "undo": "Annuler",
+            "redo": "Rétablir",
+            "cut": "Couper",
+            "copy": "Copier",
+            "paste": "Coller",
+            "selectAll": "Sélectionner tout"
+        },
+        "preview": {
+            "label": "Aperçu",
+            "start": "Lancer l'aperçu",
+            "global": "Lancer l'aperçu global"
+        },
+        "help": {
+            "label": "Aide",
+            "documentation": "Documentation",
+            "reportIssue": "Signaler un problème",
+            "mailSubject": "Aide éditeur",
+            "mailBody": "Décrivez votre problème ci-dessous:"
+        },
+        "devTools": "Outils de développement",
+        "reload": "Recharger"
+    }
+}
diff --git a/electron/i18n/i18next.config.js b/electron/i18n/i18next.config.js
new file mode 100644
index 00000000..1c2367d2
--- /dev/null
+++ b/electron/i18n/i18next.config.js
@@ -0,0 +1,18 @@
+const i18n = require('i18next');
+const Backend = require('i18next-node-fs-backend');
+const path = require('path');
+
+i18n.use(Backend).init({
+    backend: {
+        loadPath: path.join(__dirname, './{{lng}}/{{ns}}.json'),
+    },
+    fallbackLng: 'fr',
+    lng: 'fr',
+    supportedLngs: ['en', 'fr'], // Add supported languages
+    interpolation: {
+        escapeValue: false,
+    },
+    debug: process.env.NODE_ENV === 'development',
+});
+
+module.exports = i18n;
diff --git a/package-lock.json b/package-lock.json
index 7179623f..8a8c1c50 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,6 +22,8 @@
                 "expect": "^29.7.0",
                 "express": "^4.18.2",
                 "glob": "^8.1.0",
+                "i18next": "^24.2.3",
+                "i18next-node-fs-backend": "^2.1.3",
                 "pinia": "^2.0.28",
                 "sass": "^1.56.2",
                 "serve-static": "^1.15.0",
@@ -52,7 +54,7 @@
                 "eslint-plugin-vue": "^9.8.0",
                 "happy-dom": "^16.7.2",
                 "ts-node": "^10.9.1",
-                "typescript": "^4.9.4",
+                "typescript": "^5.8.2",
                 "vite": "^4.0.0",
                 "vitest": "^0.25.8",
                 "vue-tsc": "^1.0.11",
@@ -179,10 +181,9 @@
             }
         },
         "node_modules/@babel/runtime": {
-            "version": "7.25.6",
-            "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.25.6.tgz",
-            "integrity": "sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==",
-            "dev": true,
+            "version": "7.26.10",
+            "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.10.tgz",
+            "integrity": "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw==",
             "license": "MIT",
             "dependencies": {
                 "regenerator-runtime": "^0.14.0"
@@ -8855,20 +8856,6 @@
                 "node": ">=16 || 14 >=14.17"
             }
         },
-        "node_modules/config-file-ts/node_modules/typescript": {
-            "version": "5.7.3",
-            "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
-            "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
-            "dev": true,
-            "license": "Apache-2.0",
-            "bin": {
-                "tsc": "bin/tsc",
-                "tsserver": "bin/tsserver"
-            },
-            "engines": {
-                "node": ">=14.17"
-            }
-        },
         "node_modules/console-control-strings": {
             "version": "1.1.0",
             "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
@@ -11374,7 +11361,6 @@
             "version": "4.0.1",
             "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
             "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
-            "dev": true,
             "license": "BSD-2-Clause",
             "bin": {
                 "esparse": "bin/esparse.js",
@@ -13314,6 +13300,91 @@
                 "ms": "^2.0.0"
             }
         },
+        "node_modules/i18next": {
+            "version": "24.2.3",
+            "resolved": "https://registry.npmjs.org/i18next/-/i18next-24.2.3.tgz",
+            "integrity": "sha512-lfbf80OzkocvX7nmZtu7nSTNbrTYR52sLWxPtlXX1zAhVw8WEnFk4puUkCR4B1dNQwbSpEHHHemcZu//7EcB7A==",
+            "funding": [
+                {
+                    "type": "individual",
+                    "url": "https://locize.com"
+                },
+                {
+                    "type": "individual",
+                    "url": "https://locize.com/i18next.html"
+                },
+                {
+                    "type": "individual",
+                    "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project"
+                }
+            ],
+            "license": "MIT",
+            "dependencies": {
+                "@babel/runtime": "^7.26.10"
+            },
+            "peerDependencies": {
+                "typescript": "^5"
+            },
+            "peerDependenciesMeta": {
+                "typescript": {
+                    "optional": true
+                }
+            }
+        },
+        "node_modules/i18next-node-fs-backend": {
+            "version": "2.1.3",
+            "resolved": "https://registry.npmjs.org/i18next-node-fs-backend/-/i18next-node-fs-backend-2.1.3.tgz",
+            "integrity": "sha512-CreMFiVl3ChlMc5ys/e0QfuLFOZyFcL40Jj6jaKD6DxZ/GCUMxPI9BpU43QMWUgC7r+PClpxg2cGXAl0CjG04g==",
+            "deprecated": "replaced by i18next-fs-backend",
+            "license": "MIT",
+            "dependencies": {
+                "js-yaml": "3.13.1",
+                "json5": "2.0.0"
+            }
+        },
+        "node_modules/i18next-node-fs-backend/node_modules/argparse": {
+            "version": "1.0.10",
+            "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
+            "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
+            "license": "MIT",
+            "dependencies": {
+                "sprintf-js": "~1.0.2"
+            }
+        },
+        "node_modules/i18next-node-fs-backend/node_modules/js-yaml": {
+            "version": "3.13.1",
+            "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz",
+            "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==",
+            "license": "MIT",
+            "dependencies": {
+                "argparse": "^1.0.7",
+                "esprima": "^4.0.0"
+            },
+            "bin": {
+                "js-yaml": "bin/js-yaml.js"
+            }
+        },
+        "node_modules/i18next-node-fs-backend/node_modules/json5": {
+            "version": "2.0.0",
+            "resolved": "https://registry.npmjs.org/json5/-/json5-2.0.0.tgz",
+            "integrity": "sha512-0EdQvHuLm7yJ7lyG5dp7Q3X2ku++BG5ZHaJ5FTnaXpKqDrw4pMxel5Bt3oAYMthnrthFBdnZ1FcsXTPyrQlV0w==",
+            "license": "MIT",
+            "dependencies": {
+                "minimist": "^1.2.0"
+            },
+            "bin": {
+                "json5": "lib/cli.js"
+            },
+            "engines": {
+                "node": ">=6"
+            }
+        },
+        "node_modules/i18next-node-fs-backend/node_modules/sprintf-js": {
+            "version": "1.0.3",
+            "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
+            "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
+            "license": "BSD-3-Clause"
+        },
         "node_modules/iconv-corefoundation": {
             "version": "1.1.7",
             "resolved": "https://registry.npmjs.org/iconv-corefoundation/-/iconv-corefoundation-1.1.7.tgz",
@@ -17323,7 +17394,6 @@
             "version": "0.14.1",
             "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
             "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
-            "dev": true,
             "license": "MIT"
         },
         "node_modules/require-directory": {
@@ -19212,9 +19282,9 @@
             }
         },
         "node_modules/typescript": {
-            "version": "4.9.5",
-            "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz",
-            "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
+            "version": "5.8.2",
+            "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.2.tgz",
+            "integrity": "sha512-aJn6wq13/afZp/jT9QZmwEjDqqvSGp1VT5GVg+f/t6/oVyrgXM6BY1h9BRh/O5p3PlUPAe+WuiEZOmb/49RqoQ==",
             "devOptional": true,
             "license": "Apache-2.0",
             "bin": {
@@ -19222,7 +19292,7 @@
                 "tsserver": "bin/tsserver"
             },
             "engines": {
-                "node": ">=4.2.0"
+                "node": ">=14.17"
             }
         },
         "node_modules/ua-parser-js": {
diff --git a/package.json b/package.json
index 87a336f7..5377aeb5 100644
--- a/package.json
+++ b/package.json
@@ -39,6 +39,8 @@
         "expect": "^29.7.0",
         "express": "^4.18.2",
         "glob": "^8.1.0",
+        "i18next": "^24.2.3",
+        "i18next-node-fs-backend": "^2.1.3",
         "pinia": "^2.0.28",
         "sass": "^1.56.2",
         "serve-static": "^1.15.0",
@@ -69,7 +71,7 @@
         "eslint-plugin-vue": "^9.8.0",
         "happy-dom": "^16.7.2",
         "ts-node": "^10.9.1",
-        "typescript": "^4.9.4",
+        "typescript": "^5.8.2",
         "vite": "^4.0.0",
         "vitest": "^0.25.8",
         "vue-tsc": "^1.0.11",
-- 
GitLab


From 2f2b990ad3acda9cb41d90ffc5cbb07cb7ed4dbc Mon Sep 17 00:00:00 2001
From: VIAUD Nathan <nathan.viaud@inria.fr>
Date: Thu, 27 Mar 2025 10:31:45 +0100
Subject: [PATCH 17/18] feat: electron translation

---
 electron/components/contextMenu.js      | 83 +++++++++++++------------
 electron/i18n/en/translation.json       | 23 +++++++
 electron/i18n/fr/translation.json       | 25 ++++++++
 src/i18n/en.ts                          |  1 +
 src/i18n/fr.ts                          |  1 +
 src/shared/data/forms/badgeForm.data.ts |  2 +-
 6 files changed, 93 insertions(+), 42 deletions(-)

diff --git a/electron/components/contextMenu.js b/electron/components/contextMenu.js
index 8dccce26..77b7edaa 100644
--- a/electron/components/contextMenu.js
+++ b/electron/components/contextMenu.js
@@ -1,4 +1,5 @@
 const { BrowserWindow } = require('electron');
+const i18n = require('../i18n/i18next.config');
 
 const isDev = process.env.IS_DEV === 'true';
 
@@ -8,11 +9,11 @@ function getTemplateFromContext(callback, data) {
     };
     const standardActions = [
         {
-            label: 'Undo',
+            label: i18n.t('menu.edit.undo'),
             click: () => onClick('undo'),
         },
         {
-            label: 'Redo',
+            label: i18n.t('menu.edit.redo'),
             click: () => onClick('redo'),
         },
     ];
@@ -22,58 +23,58 @@ function getTemplateFromContext(callback, data) {
     if (data.context === 'flow') {
         menu.push(
             {
-                label: 'Ajouter',
+                label: i18n.t('context.add'),
                 submenu: getPagesFromContext(onClick, { position: data.position }, 'addPage', data.context),
             },
             {
-                label: 'Coller ici',
+                label: i18n.t('context.pasteHere'),
                 click: () => onClick('paste', { position: data.position }),
             },
         );
     } else if (data.context === 'page' || data.context === 'activity' || data.context === 'pageWithQuestion') {
         if (isDev) {
             menu.push({
-                label: 'Ajouter',
+                label: i18n.t('context.add'),
                 submenu: getContentFromContext(onClick, { id: data.id }, data.context),
             });
         }
         menu.push(
             {
-                label: 'Insérer après',
+                label: i18n.t('context.insertAfter'),
                 submenu: getPagesFromContext(onClick, { id: data.id }, 'insertAfter', data.context),
             },
             {
-                label: 'Insérer avant',
+                label: i18n.t('context.insertBefore'),
                 submenu: getPagesFromContext(onClick, { id: data.id }, 'insertBefore', data.context),
             },
             {
-                label: 'Dupliquer',
+                label: i18n.t('context.duplicate'),
                 click: () => onClick('duplicatePage', { id: data.id }),
             },
             {
-                label: 'Supprimer',
+                label: i18n.t('context.delete'),
                 click: () => onClick('deleteNode', { id: data.id }),
             },
             {
                 type: 'separator',
             },
             {
-                label: 'Copier',
+                label: i18n.t('menu.edit.copy'),
                 click: () => onClick('copy', { id: data.id }),
             },
             {
-                label: 'Intervertir avec le suivant',
+                label: i18n.t('context.swapNext'),
                 click: () => onClick('swapNodeWithNext', { id: data.id }),
             },
             {
-                label: 'Intervertir avec le précédent',
+                label: i18n.t('context.swapPrevious'),
                 click: () => onClick('swapNodeWithPrevious', { id: data.id }),
             },
         );
     } else if (data.context === 'content') {
         menu.push(
             {
-                label: 'Supprimer',
+                label: i18n.t('context.delete'),
                 click: () => onClick('deleteContent', { pageId: data.pageId, id: data.id }),
             },
             // {
@@ -84,43 +85,43 @@ function getTemplateFromContext(callback, data) {
     } else if (data.context === 'chapter') {
         menu.push(
             {
-                label: 'Insérer à la fin',
+                label: i18n.t('context.insertEnd'),
                 submenu: getPagesFromContext(onClick, { id: data.id }, 'insertAtEnd', data.context),
             },
             {
-                label: 'Insérer au début',
+                label: i18n.t('context.insertStart'),
                 submenu: getPagesFromContext(onClick, { id: data.id }, 'insertAtStart', data.context),
             },
             {
-                label: 'Intervertir avec le précédent',
+                label: i18n.t('context.swapPrevious'),
                 click: () => onClick('swapChapterWithPrevious', { id: data.id }),
             },
             {
-                label: 'Intervertir avec le suivant',
+                label: i18n.t('context.swapNext'),
                 click: () => onClick('swapChapterWithNext', { id: data.id }),
             },
             {
-                label: 'Supprimer',
+                label: i18n.t('context.delete'),
                 click: () => onClick('deleteNode', { id: data.id }),
             },
             {
-                label: 'Copier le chapitre',
+                label: i18n.t('context.copyChapter'),
                 click: () => onClick('copyChapter', { id: data.id }),
             },
         );
     } else if (data.context === 'epoc') {
         menu.push({
-            label: 'Ajouter un nouveau chapitre',
+            label: i18n.t('context.addChapter'),
             click: () => onClick('addChapter'),
         });
     } else if (data.context === 'selection') {
         menu.push(
             {
-                label: 'Supprimer',
+                label: i18n.t('context.delete'),
                 click: () => onClick('deleteSelection', { selection: data.selection }),
             },
             {
-                label: 'Copier',
+                label: i18n.t('menu.edit.copy'),
                 click: () => onClick('copySelection', { selection: data.selection }),
             },
         );
@@ -134,42 +135,42 @@ function getTemplateFromContext(callback, data) {
 function getPagesFromContext(onClick, data, event, context) {
     const contents = [
         {
-            label: 'Ajouter une page Texte',
+            label: i18n.t('context.addText'),
             click: () => onClick(event, { type: 'text', ...data }),
         },
         {
-            label: 'Ajouter une page Vidéo',
+            label: i18n.t('context.addVideo'),
             click: () => onClick(event, { type: 'video', ...data }),
         },
         {
-            label: 'Ajouter une page Audio',
+            label: i18n.t('context.addAudio'),
             click: () => onClick(event, { type: 'audio', ...data }),
         },
     ];
 
     const questions = [
         {
-            label: 'Ajouter une évaluation QCM',
+            label: i18n.t('context.addChoice'),
             click: () => onClick(event, { type: 'choice', ...data }),
         },
         {
-            label: 'Ajouter une évaluation Drag & Drop',
+            label: i18n.t('context.addDragAndDrop'),
             click: () => onClick(event, { type: 'drag-and-drop', ...data }),
         },
         {
-            label: 'Ajouter une évaluation Reorder',
+            label: i18n.t('context.addReorder'),
             click: () => onClick(event, { type: 'reorder', ...data }),
         },
         {
-            label: 'Ajouter une évaluation Swipe',
+            label: i18n.t('context.addSwipe'),
             click: () => onClick(event, { type: 'swipe', ...data }),
         },
         {
-            label: 'Ajouter une évaluation Liste Déroulante',
+            label: i18n.t('context.addDropdown'),
             click: () => onClick(event, { type: 'dropdown-list', ...data }),
         },
         {
-            label: 'Ajouter une évaluation personnalisée',
+            label: i18n.t('context.addCustom'),
             click: () => onClick(event, { type: 'custom', ...data }),
         },
     ];
@@ -180,7 +181,7 @@ function getPagesFromContext(onClick, data, event, context) {
         const addChapter = [
             { type: 'separator' },
             {
-                label: 'Ajouter un chapitre',
+                label: i18n.t('context.addChapter'),
                 click: () => onClick('addChapter'),
             },
         ];
@@ -193,42 +194,42 @@ function getPagesFromContext(onClick, data, event, context) {
 function getContentFromContext(onClick, data, context) {
     const questions = [
         {
-            label: 'Ajouter une question QCM',
+            label: i18n.t('context.addQuestion'),
             click: () => onClick('addContent', { type: 'choice', ...data }),
         },
         {
-            label: 'Ajouter une question Drag & Drop',
+            label: i18n.t('context.addDragAndDrop'),
             click: () => onClick('addContent', { type: 'drag-and-drop', ...data }),
         },
         {
-            label: 'Ajouter une question Reorder',
+            label: i18n.t('context.addReorder'),
             click: () => onClick('addContent', { type: 'reorder', ...data }),
         },
         {
-            label: 'Ajouter une question Swipe',
+            label: i18n.t('context.addSwipe'),
             click: () => onClick('addContent', { type: 'swipe', ...data }),
         },
         {
-            label: 'Ajouter une question Liste Déroulante',
+            label: i18n.t('context.addDropdownList'),
             click: () => onClick('addContent', { type: 'dropdown-list', ...data }),
         },
         {
-            label: 'Ajouter une question personnalisée',
+            label: i18n.t('context.addCustom'),
             click: () => onClick('addContent', { type: 'custom', ...data }),
         },
     ];
 
     const contents = [
         {
-            label: 'Ajouter un contenu Texte',
+            label: i18n.t('context.addText'),
             click: () => onClick('addContent', { type: 'text', ...data }),
         },
         {
-            label: 'Ajouter un contenu Vidéo',
+            label: i18n.t('context.addVideo'),
             click: () => onClick('addContent', { type: 'video', ...data }),
         },
         {
-            label: 'Ajouter un contenu Audio',
+            label: i18n.t('context.addAudio'),
             click: () => onClick('addContent', { type: 'audio', ...data }),
         },
     ];
diff --git a/electron/i18n/en/translation.json b/electron/i18n/en/translation.json
index 985c60eb..8fa5cbbc 100644
--- a/electron/i18n/en/translation.json
+++ b/electron/i18n/en/translation.json
@@ -40,5 +40,28 @@
         },
         "devTools": "Developer Tools",
         "reload": "Reload"
+    },
+    "context": {
+        "add": "Add",
+        "pasteHere": "Paste here",
+        "insertAfter": "Insert after",
+        "insertBefore": "Insert before",
+        "delete": "Delete",
+        "duplicate": "Duplicate",
+        "swapNext": "Swap with next",
+        "swapPrevious": "Swap with previous",
+        "insertEnd": "Insert at end",
+        "insertStart": "Insert at start",
+        "copyChapter": "Copy chapter",
+        "addChapter": "Add new chapter",
+        "addText": "Add text page",
+        "addVideo": "Add video page",
+        "addAudio": "Add audio page",
+        "addChoice": "Add multiple choice assessment",
+        "addDragAndDrop": "Add drag and drop assessment",
+        "addReorder": "Add reorder assessment",
+        "addSwipe": "Add swipe assessment",
+        "addDropdown": "Add dropdown assessment",
+        "addCustom": "Add custom assessment"
     }
 }
diff --git a/electron/i18n/fr/translation.json b/electron/i18n/fr/translation.json
index 7068ba96..2e282b42 100644
--- a/electron/i18n/fr/translation.json
+++ b/electron/i18n/fr/translation.json
@@ -40,5 +40,30 @@
         },
         "devTools": "Outils de développement",
         "reload": "Recharger"
+    },
+    "context": {
+        "add": "Ajouter",
+        "pasteHere": "Coller ici",
+        "insertAfter": "Insérer après",
+        "insertBefore": "Insérer avant",
+        "delete": "Supprimer",
+        "duplicate": "Dupliquer",
+        "swapNext": "Échanger avec le suivant",
+        "swapPrevious": "Échanger avec le précédent",
+        "insertEnd": "Insérer à la fin",
+        "insertStart": "Insérer au début",
+        "copyChapter": "Copier le chapitre",
+        "addChapter": "Ajouter un nouveau chapitre",
+
+        "addText": "Ajouter une page texte",
+        "addVideo": "Ajouter une page vidéo",
+        "addAudio": "Ajouter une page audio",
+
+        "addChoice": "Ajouter une évaluation QCM",
+        "addDragAndDrop": "Ajouter une évaluation Drag & Drop",
+        "addReorder": "Ajouter une évaluation Reorder",
+        "addSwipe": "Ajouter une évaluation Swipe",
+        "addDropdown": "Ajouter une évaluation Liste Déroulante",
+        "addCustom": "Ajouter une évaluation personnalisée"
     }
 }
diff --git a/src/i18n/en.ts b/src/i18n/en.ts
index 68d26a3e..bc0cbf86 100644
--- a/src/i18n/en.ts
+++ b/src/i18n/en.ts
@@ -134,6 +134,7 @@ export const enMessages = {
     forms: {
         type: 'Type here...',
         badge: {
+            text: 'Badge settings',
             updateIcon: 'Update icon',
             obtention: 'Badge earning conditions',
             presentation: 'Badge presentation',
diff --git a/src/i18n/fr.ts b/src/i18n/fr.ts
index 4bf4bcbe..c3bd7caa 100644
--- a/src/i18n/fr.ts
+++ b/src/i18n/fr.ts
@@ -134,6 +134,7 @@ export const frMessages = {
     forms: {
         type: 'Saisissez...',
         badge: {
+            text: 'Paramètres du badge',
             updateIcon: "Modifier l'icône",
             obtention: "Conditions d'obtention du badge",
             presentation: 'Présentation du badge',
diff --git a/src/shared/data/forms/badgeForm.data.ts b/src/shared/data/forms/badgeForm.data.ts
index 3573e693..11ef0802 100644
--- a/src/shared/data/forms/badgeForm.data.ts
+++ b/src/shared/data/forms/badgeForm.data.ts
@@ -6,7 +6,7 @@ import { i18n } from '@/src/i18n/config';
 export const customBadgeForm: ComputedRef<Form> = computed(() => {
     return {
         type: 'badge',
-        name: 'Paramètres du badge',
+        name: i18n.global.t('forms.badge.text'),
         icon: 'icon-badge',
         buttons: badgeButtons.value,
         fields: [
-- 
GitLab


From ad5e6f396eaa3a5c703c3d061475d623bf87bd1c Mon Sep 17 00:00:00 2001
From: VIAUD Nathan <nathan.viaud@inria.fr>
Date: Mon, 31 Mar 2025 14:35:28 +0200
Subject: [PATCH 18/18] feat: translation

---
 electron/components/contextMenu.js           |  94 ++---
 electron/components/ipc.js                   |  16 +-
 electron/components/utils.js                 |  36 ++
 electron/components/window.js                | 117 ++----
 electron/electron.js                         |   4 +-
 electron/i18n/en/translation.json            |  67 ----
 electron/i18n/fr/translation.json            |  69 ----
 electron/i18n/i18next.config.js              |  18 -
 i18n/config.ts                               |  14 +
 i18n/en/translation.json                     | 390 ++++++++++++++++++
 i18n/fr/translation.json                     | 392 +++++++++++++++++++
 src/features/settings/SettingsModal.vue      |  20 +-
 src/i18n/config.ts                           |  12 -
 src/i18n/en.ts                               | 324 ---------------
 src/i18n/fr.ts                               | 325 ---------------
 src/main.ts                                  |   2 +-
 src/shared/data/badge.data.ts                |   2 +-
 src/shared/data/forms/badgeForm.data.ts      |   2 +-
 src/shared/data/forms/contentForm.data.ts    |   2 +-
 src/shared/data/forms/formButtons.data.ts    |   2 +-
 src/shared/data/forms/nodeForm.data.ts       |   2 +-
 src/shared/data/forms/questionsForm.data.ts  |   2 +-
 src/shared/data/sideBar.data.ts              |   2 +-
 src/shared/services/graph/badge.service.ts   |   2 +-
 src/shared/services/graph/chapter.service.ts |   2 +-
 src/shared/stores/settingsStore.ts           |   2 +-
 26 files changed, 951 insertions(+), 969 deletions(-)
 delete mode 100644 electron/i18n/en/translation.json
 delete mode 100644 electron/i18n/fr/translation.json
 delete mode 100644 electron/i18n/i18next.config.js
 create mode 100644 i18n/config.ts
 create mode 100644 i18n/en/translation.json
 create mode 100644 i18n/fr/translation.json
 delete mode 100644 src/i18n/config.ts
 delete mode 100644 src/i18n/en.ts
 delete mode 100644 src/i18n/fr.ts

diff --git a/electron/components/contextMenu.js b/electron/components/contextMenu.js
index 77b7edaa..055612fb 100644
--- a/electron/components/contextMenu.js
+++ b/electron/components/contextMenu.js
@@ -1,5 +1,5 @@
 const { BrowserWindow } = require('electron');
-const i18n = require('../i18n/i18next.config');
+const { t } = require('./utils');
 
 const isDev = process.env.IS_DEV === 'true';
 
@@ -9,11 +9,11 @@ function getTemplateFromContext(callback, data) {
     };
     const standardActions = [
         {
-            label: i18n.t('menu.edit.undo'),
+            label: t('menu.edit.undo'),
             click: () => onClick('undo'),
         },
         {
-            label: i18n.t('menu.edit.redo'),
+            label: t('menu.edit.redo'),
             click: () => onClick('redo'),
         },
     ];
@@ -23,60 +23,60 @@ function getTemplateFromContext(callback, data) {
     if (data.context === 'flow') {
         menu.push(
             {
-                label: i18n.t('context.add'),
+                label: t('context.add'),
                 submenu: getPagesFromContext(onClick, { position: data.position }, 'addPage', data.context),
             },
             {
-                label: i18n.t('context.pasteHere'),
+                label: t('context.pasteHere'),
                 click: () => onClick('paste', { position: data.position }),
-            },
+            }
         );
     } else if (data.context === 'page' || data.context === 'activity' || data.context === 'pageWithQuestion') {
         if (isDev) {
             menu.push({
-                label: i18n.t('context.add'),
+                label: t('context.add'),
                 submenu: getContentFromContext(onClick, { id: data.id }, data.context),
             });
         }
         menu.push(
             {
-                label: i18n.t('context.insertAfter'),
+                label: t('context.insertAfter'),
                 submenu: getPagesFromContext(onClick, { id: data.id }, 'insertAfter', data.context),
             },
             {
-                label: i18n.t('context.insertBefore'),
+                label: t('context.insertBefore'),
                 submenu: getPagesFromContext(onClick, { id: data.id }, 'insertBefore', data.context),
             },
             {
-                label: i18n.t('context.duplicate'),
+                label: t('context.duplicate'),
                 click: () => onClick('duplicatePage', { id: data.id }),
             },
             {
-                label: i18n.t('context.delete'),
+                label: t('context.delete'),
                 click: () => onClick('deleteNode', { id: data.id }),
             },
             {
                 type: 'separator',
             },
             {
-                label: i18n.t('menu.edit.copy'),
+                label: t('menu.edit.copy'),
                 click: () => onClick('copy', { id: data.id }),
             },
             {
-                label: i18n.t('context.swapNext'),
+                label: t('context.swapNext'),
                 click: () => onClick('swapNodeWithNext', { id: data.id }),
             },
             {
-                label: i18n.t('context.swapPrevious'),
+                label: t('context.swapPrevious'),
                 click: () => onClick('swapNodeWithPrevious', { id: data.id }),
-            },
+            }
         );
     } else if (data.context === 'content') {
         menu.push(
             {
-                label: i18n.t('context.delete'),
+                label: t('context.delete'),
                 click: () => onClick('deleteContent', { pageId: data.pageId, id: data.id }),
-            },
+            }
             // {
             //     label: 'Dupliquer',
             //     click: () => onClick('duplicateContent', { pageId: data.pageId, id: data.id })
@@ -85,45 +85,45 @@ function getTemplateFromContext(callback, data) {
     } else if (data.context === 'chapter') {
         menu.push(
             {
-                label: i18n.t('context.insertEnd'),
+                label: t('context.insertEnd'),
                 submenu: getPagesFromContext(onClick, { id: data.id }, 'insertAtEnd', data.context),
             },
             {
-                label: i18n.t('context.insertStart'),
+                label: t('context.insertStart'),
                 submenu: getPagesFromContext(onClick, { id: data.id }, 'insertAtStart', data.context),
             },
             {
-                label: i18n.t('context.swapPrevious'),
+                label: t('context.swapPrevious'),
                 click: () => onClick('swapChapterWithPrevious', { id: data.id }),
             },
             {
-                label: i18n.t('context.swapNext'),
+                label: t('context.swapNext'),
                 click: () => onClick('swapChapterWithNext', { id: data.id }),
             },
             {
-                label: i18n.t('context.delete'),
+                label: t('context.delete'),
                 click: () => onClick('deleteNode', { id: data.id }),
             },
             {
-                label: i18n.t('context.copyChapter'),
+                label: t('context.copyChapter'),
                 click: () => onClick('copyChapter', { id: data.id }),
-            },
+            }
         );
     } else if (data.context === 'epoc') {
         menu.push({
-            label: i18n.t('context.addChapter'),
+            label: t('context.addChapter'),
             click: () => onClick('addChapter'),
         });
     } else if (data.context === 'selection') {
         menu.push(
             {
-                label: i18n.t('context.delete'),
+                label: t('context.delete'),
                 click: () => onClick('deleteSelection', { selection: data.selection }),
             },
             {
-                label: i18n.t('menu.edit.copy'),
+                label: t('menu.edit.copy'),
                 click: () => onClick('copySelection', { selection: data.selection }),
-            },
+            }
         );
     }
 
@@ -135,42 +135,42 @@ function getTemplateFromContext(callback, data) {
 function getPagesFromContext(onClick, data, event, context) {
     const contents = [
         {
-            label: i18n.t('context.addText'),
+            label: t('context.addText'),
             click: () => onClick(event, { type: 'text', ...data }),
         },
         {
-            label: i18n.t('context.addVideo'),
+            label: t('context.addVideo'),
             click: () => onClick(event, { type: 'video', ...data }),
         },
         {
-            label: i18n.t('context.addAudio'),
+            label: t('context.addAudio'),
             click: () => onClick(event, { type: 'audio', ...data }),
         },
     ];
 
     const questions = [
         {
-            label: i18n.t('context.addChoice'),
+            label: t('context.addChoice'),
             click: () => onClick(event, { type: 'choice', ...data }),
         },
         {
-            label: i18n.t('context.addDragAndDrop'),
+            label: t('context.addDragAndDrop'),
             click: () => onClick(event, { type: 'drag-and-drop', ...data }),
         },
         {
-            label: i18n.t('context.addReorder'),
+            label: t('context.addReorder'),
             click: () => onClick(event, { type: 'reorder', ...data }),
         },
         {
-            label: i18n.t('context.addSwipe'),
+            label: t('context.addSwipe'),
             click: () => onClick(event, { type: 'swipe', ...data }),
         },
         {
-            label: i18n.t('context.addDropdown'),
+            label: t('context.addDropdown'),
             click: () => onClick(event, { type: 'dropdown-list', ...data }),
         },
         {
-            label: i18n.t('context.addCustom'),
+            label: t('context.addCustom'),
             click: () => onClick(event, { type: 'custom', ...data }),
         },
     ];
@@ -181,7 +181,7 @@ function getPagesFromContext(onClick, data, event, context) {
         const addChapter = [
             { type: 'separator' },
             {
-                label: i18n.t('context.addChapter'),
+                label: t('context.addChapter'),
                 click: () => onClick('addChapter'),
             },
         ];
@@ -194,42 +194,42 @@ function getPagesFromContext(onClick, data, event, context) {
 function getContentFromContext(onClick, data, context) {
     const questions = [
         {
-            label: i18n.t('context.addQuestion'),
+            label: t('context.addQuestion'),
             click: () => onClick('addContent', { type: 'choice', ...data }),
         },
         {
-            label: i18n.t('context.addDragAndDrop'),
+            label: t('context.addDragAndDrop'),
             click: () => onClick('addContent', { type: 'drag-and-drop', ...data }),
         },
         {
-            label: i18n.t('context.addReorder'),
+            label: t('context.addReorder'),
             click: () => onClick('addContent', { type: 'reorder', ...data }),
         },
         {
-            label: i18n.t('context.addSwipe'),
+            label: t('context.addSwipe'),
             click: () => onClick('addContent', { type: 'swipe', ...data }),
         },
         {
-            label: i18n.t('context.addDropdownList'),
+            label: t('context.addDropdownList'),
             click: () => onClick('addContent', { type: 'dropdown-list', ...data }),
         },
         {
-            label: i18n.t('context.addCustom'),
+            label: t('context.addCustom'),
             click: () => onClick('addContent', { type: 'custom', ...data }),
         },
     ];
 
     const contents = [
         {
-            label: i18n.t('context.addText'),
+            label: t('context.addText'),
             click: () => onClick('addContent', { type: 'text', ...data }),
         },
         {
-            label: i18n.t('context.addVideo'),
+            label: t('context.addVideo'),
             click: () => onClick('addContent', { type: 'video', ...data }),
         },
         {
-            label: i18n.t('context.addAudio'),
+            label: t('context.addAudio'),
             click: () => onClick('addContent', { type: 'audio', ...data }),
         },
     ];
diff --git a/electron/components/ipc.js b/electron/components/ipc.js
index 35188809..f72a91fc 100644
--- a/electron/components/ipc.js
+++ b/electron/components/ipc.js
@@ -32,7 +32,7 @@ const copyData = {
  * Setup ipc listeners that are received from renderer process
  * @param targetWindow
  */
-const setupIpcListener = function (targetWindow) {
+const setupIpcListener = function (targetWindow, setupMenu) {
     function ipcGuard(handler) {
         return (event, ...args) => {
             if (targetWindow.isDestroyed() || event.sender !== targetWindow.webContents) return;
@@ -251,11 +251,19 @@ const setupIpcListener = function (targetWindow) {
 
     ipcMain.on(
         'setSettings',
-        ipcGuard(async (event, data) => {
+        ipcGuard(async (_event, data) => {
+            const { spellcheck, locale } = electronStore.get('settings');
             electronStore.set('settings', data);
 
-            targetWindow.webContents.session.setSpellCheckerEnabled(electronStore.get('settings.spellcheck'));
-            targetWindow.webContents.reload();
+            if (data.spellcheck !== spellcheck) {
+                targetWindow.webContents.session.setSpellCheckerEnabled(spellcheck);
+                targetWindow.webContents.reload();
+            }
+
+            if (data.locale !== locale) {
+                setupMenu();
+                targetWindow.webContents.reload();
+            }
         }),
     );
 };
diff --git a/electron/components/utils.js b/electron/components/utils.js
index 1c8d0ab9..bc843f7f 100644
--- a/electron/components/utils.js
+++ b/electron/components/utils.js
@@ -1,3 +1,39 @@
+const translations = require('../../i18n/en/translation.json');
+const Store = require('electron-store');
+const electronStore = new Store();
+
+/**
+ * Get translation for a given key
+ * @param {string} key - Translation key (e.g., 'menu.app.label')
+ * @returns {string} - Translated string or key if not found
+ */
+module.exports.t = function (key) {
+    const lang = electronStore.get('settings.locale') || 'en';
+    const translationFile = require(`../../i18n/${lang}/translation.json`);
+
+    // Split key by dots and traverse the translation object
+    const keys = key.split('.');
+    let result = translationFile;
+
+    for (const k of keys) {
+        if (result && Object.prototype.hasOwnProperty.call(result, k)) {
+            result = result[k];
+        } else {
+            // If key not found, try English as fallback
+            result = translations;
+            for (const k of keys) {
+                if (result && Object.prototype.hasOwnProperty.call(result, k)) {
+                    result = result[k];
+                } else {
+                    return key; // Return the key itself if translation not found
+                }
+            }
+            return result;
+        }
+    }
+    return result;
+};
+
 /**
  * Wait for all promise to have resolve
  * @param promises
diff --git a/electron/components/window.js b/electron/components/window.js
index 8a98b070..c5557c73 100644
--- a/electron/components/window.js
+++ b/electron/components/window.js
@@ -13,19 +13,11 @@ const {
     createPreview,
     createGlobalPreview,
 } = require('./file');
-const i18n = require('../i18n/i18next.config.js');
+const { t } = require('./utils');
 
 const Store = require('electron-store');
 const electronStore = new Store();
 
-i18n.on('initialized', () => {
-    console.log('i18next initialized successfully');
-});
-
-i18n.on('failedLoading', (lng, ns, msg) => {
-    console.error('i18next failed to load:', msg);
-});
-
 /**
  * Create the app main window
  * @returns {Electron.CrossProcessExports.BrowserWindow}
@@ -113,14 +105,14 @@ const setupWindow = function (window, filepath) {
     if (filepath) {
         window.webContents.send(
             'epocProjectPicked',
-            JSON.stringify({ name: null, modified: null, filepath: filepath, workdir: null })
+            JSON.stringify({ name: null, modified: null, filepath: filepath, workdir: null }),
         );
     }
 };
 
 const createNewWindow = () => {
     const newWindow = createMainWindow();
-    setupIpcListener(newWindow);
+    setupIpcListener(newWindow, setupMenu);
     setupWindow(newWindow);
 
     newWindow.show();
@@ -133,62 +125,45 @@ const createNewWindow = () => {
 const setupMenu = () => {
     const mainMenuTemplate = [
         {
-            label: i18n.t('menu.app.label'),
+            label: t('menu.app.label'),
             submenu: [
-                { label: i18n.t('menu.app.about'), role: 'about' },
+                { label: t('menu.app.about'), role: 'about' },
                 {
-                    label: i18n.t('menu.app.quit'),
+                    label: t('menu.app.quit'),
                     accelerator: 'CmdOrCtrl+Q',
                     click: function () {
                         app.quit();
                     },
                 },
                 { type: 'separator' },
-                {
-                    label: i18n.t('menu.app.lang'),
-                    submenu: [
-                        {
-                            label: 'English',
-                            type: 'radio',
-                            checked: i18n.language === 'en',
-                            click: () => changeLanguage('en'),
-                        },
-                        {
-                            label: 'Français',
-                            type: 'radio',
-                            checked: i18n.language === 'fr',
-                            click: () => changeLanguage('fr'),
-                        },
-                    ],
-                },
             ],
         },
         {
-            label: i18n.t('menu.file.label'),
+            label: t('menu.file.label'),
             submenu: [
                 {
-                    label: i18n.t('menu.file.new'),
+                    label: t('menu.file.new'),
                     accelerator: 'CmdOrCtrl+N',
                     click: function () {
                         sendToFrontend(BrowserWindow.getFocusedWindow(), 'epocProjectNew');
                     },
                 },
                 {
-                    label: i18n.t('menu.file.newWindow'),
+                    label: t('menu.file.newWindow'),
                     click: function () {
                         ipcMain.emit('newWindow');
                     },
                 },
                 { type: 'separator' },
                 {
-                    label: i18n.t('menu.file.open'),
+                    label: t('menu.file.open'),
                     accelerator: 'CmdOrCtrl+O',
                     click: function () {
                         sendToFrontend(BrowserWindow.getFocusedWindow(), 'epocProjectPicked', pickEpocProject());
                     },
                 },
                 {
-                    label: i18n.t('menu.file.openInNewWindow'),
+                    label: t('menu.file.openInNewWindow'),
                     click: () => {
                         const newWindow = createNewWindow();
                         const project = pickEpocProject();
@@ -198,7 +173,7 @@ const setupMenu = () => {
                     },
                 },
                 {
-                    label: i18n.t('menu.file.latest'),
+                    label: t('menu.file.latest'),
                     submenu: [
                         ...getRecentFiles().map((project) => {
                             return {
@@ -211,7 +186,7 @@ const setupMenu = () => {
                     ],
                 },
                 {
-                    label: i18n.t('menu.file.import'),
+                    label: t('menu.file.import'),
                     click: async function () {
                         sendToFrontend(BrowserWindow.getFocusedWindow(), 'epocImportPicked');
                         const project = await pickEpocToImport();
@@ -249,7 +224,7 @@ const setupMenu = () => {
                 { type: 'separator' },
                 {
                     id: 'save',
-                    label: i18n.t('menu.file.save'),
+                    label: t('menu.file.save'),
                     accelerator: 'CmdOrCtrl+S',
                     enabled: !!(
                         store.state.projects[BrowserWindow.getFocusedWindow()?.id] &&
@@ -267,7 +242,7 @@ const setupMenu = () => {
                 },
                 {
                     id: 'saveAs',
-                    label: i18n.t('menu.file.saveAs'),
+                    label: t('menu.file.saveAs'),
                     accelerator: 'Shift+CmdOrCtrl+S',
                     enabled: !!(
                         store.state.projects[BrowserWindow.getFocusedWindow()?.id] &&
@@ -276,7 +251,7 @@ const setupMenu = () => {
                     click: async function () {
                         sendToFrontend(BrowserWindow.getFocusedWindow(), 'epocProjectSaving');
                         const result = await saveAsEpocProject(
-                            store.state.projects[BrowserWindow.getFocusedWindow().id]
+                            store.state.projects[BrowserWindow.getFocusedWindow().id],
                         );
                         if (result) {
                             updateSavedProject(BrowserWindow.getFocusedWindow().webContents, result);
@@ -288,40 +263,40 @@ const setupMenu = () => {
             ],
         },
         {
-            label: i18n.t('menu.edit.label'),
+            label: t('menu.edit.label'),
             submenu: [
                 {
-                    label: i18n.t('menu.edit.undo'),
+                    label: t('menu.edit.undo'),
                     accelerator: 'CmdOrCtrl+Z',
                     click: function () {
                         sendToFrontend(BrowserWindow.getFocusedWindow(), 'undo');
                     },
                 },
                 {
-                    label: i18n.t('menu.edit.redo'),
+                    label: t('menu.edit.redo'),
                     accelerator: process.platform === 'darwin' ? 'Shift+CmdOrCtrl+Z' : 'CmdOrCtrl+Y',
                     click: function () {
                         sendToFrontend(BrowserWindow.getFocusedWindow(), 'redo');
                     },
                 },
                 { type: 'separator' },
-                { label: i18n.t('menu.edit.cut'), accelerator: 'CmdOrCtrl+X', role: 'cut' },
-                { label: i18n.t('menu.edit.copy'), accelerator: 'CmdOrCtrl+C', role: 'copy' },
-                { label: i18n.t('menu.edit.paste'), accelerator: 'CmdOrCtrl+V', role: 'paste' },
-                { label: i18n.t('menu.edit.selectAll'), accelerator: 'CmdOrCtrl+A', role: 'selectAll' },
+                { label: t('menu.edit.cut'), accelerator: 'CmdOrCtrl+X', role: 'cut' },
+                { label: t('menu.edit.copy'), accelerator: 'CmdOrCtrl+C', role: 'copy' },
+                { label: t('menu.edit.paste'), accelerator: 'CmdOrCtrl+V', role: 'paste' },
+                { label: t('menu.edit.selectAll'), accelerator: 'CmdOrCtrl+A', role: 'selectAll' },
             ],
         },
         {
-            label: i18n.t('menu.preview.label'),
+            label: t('menu.preview.label'),
             submenu: [
                 {
-                    label: i18n.t('menu.preview.start'),
+                    label: t('menu.preview.start'),
                     click: function () {
                         createPreview(store.state.projects[BrowserWindow.getFocusedWindow().id].workdir);
                     },
                 },
                 {
-                    label: i18n.t('menu.preview.global'),
+                    label: t('menu.preview.global'),
                     click: function () {
                         createGlobalPreview(store.state.projects[BrowserWindow.getFocusedWindow().id].workdir);
                     },
@@ -329,33 +304,33 @@ const setupMenu = () => {
             ],
         },
         {
-            label: i18n.t('menu.help.label'),
+            label: t('menu.help.label'),
             submenu: [
                 {
-                    label: i18n.t('menu.help.documentation'),
+                    label: t('menu.help.documentation'),
                     click: async function () {
                         await shell.openExternal('https://epoc.inria.fr/guide/user/getting-started/');
                     },
                 },
                 {
-                    label: i18n.t('menu.help.reportIssue'),
+                    label: t('menu.help.reportIssue'),
                     click: async function () {
                         const isDev = process.env.IS_DEV === 'true';
 
-                        const emailSubject = i18n.t('menu.help.mailSubject');
+                        const emailSubject = t('menu.help.mailSubject');
                         const emailRecipient = 'ill-ePoc-contact@inria.fr';
                         let emailBody = '';
                         if (isDev) {
                             const appVersion = app.getVersion();
                             emailBody = encodeURIComponent(
-                                `Version: ${appVersion}\n---\n\n${i18n.t('menu.help.mailBody')}\n\n`
+                                `Version: ${appVersion}\n---\n\n${t('menu.help.mailBody')}\n\n`,
                             );
                         } else {
                             const appInfo = require('../../dist/appInfo.json');
                             emailBody = encodeURIComponent(
-                                `Version: ${appInfo.version}\nBuild: ${appInfo.buildNumber}\n ---\n\n${i18n.t(
-                                    'menu.help.mailBody'
-                                )}\n\n`
+                                `Version: ${appInfo.version}\nBuild: ${appInfo.buildNumber}\n ---\n\n${t(
+                                    'menu.help.mailBody',
+                                )}\n\n`,
                             );
                         }
 
@@ -366,14 +341,14 @@ const setupMenu = () => {
                 },
                 { type: 'separator' },
                 {
-                    label: i18n.t('menu.devTools'),
+                    label: t('menu.devTools'),
                     accelerator: 'CmdOrCtrl+D',
                     click: function () {
                         BrowserWindow.getFocusedWindow().webContents.toggleDevTools();
                     },
                 },
                 {
-                    label: i18n.t('menu.reload'),
+                    label: t('menu.reload'),
                     accelerator: 'CmdOrCtrl+R',
                     click: function () {
                         BrowserWindow.getFocusedWindow().webContents.reload();
@@ -401,23 +376,5 @@ module.exports = {
     createMainWindow,
     setupWindow,
     createNewWindow,
+    setupMenu,
 };
-
-const changeLanguage = (lng) => {
-    i18n.changeLanguage(lng, (err) => {
-        if (err) {
-            console.error('Error changing language:', err);
-            return;
-        }
-        // Save language preference
-        electronStore.set('language', lng);
-        // Refresh the menu
-        setupMenu();
-    });
-};
-
-// Initialize language from store
-const storedLanguage = electronStore.get('language');
-if (storedLanguage) {
-    i18n.changeLanguage(storedLanguage);
-}
diff --git a/electron/electron.js b/electron/electron.js
index 3b37581e..1bd8b675 100644
--- a/electron/electron.js
+++ b/electron/electron.js
@@ -1,7 +1,7 @@
 /* eslint-disable no-undef */
 /* eslint-disable @typescript-eslint/no-var-requires */
 const { app, BrowserWindow, ipcMain } = require('electron');
-const { createMainWindow, setupWindow, createNewWindow } = require('./components/window');
+const { createMainWindow, setupWindow, createNewWindow, setupMenu } = require('./components/window');
 const { createSplashWindow } = require('./components/splash');
 const { setupIpcListener } = require('./components/ipc');
 const { waitEvent, waitAll, wait } = require('./components/utils');
@@ -40,7 +40,7 @@ app.whenReady().then(() => {
     mainWindow = createMainWindow();
     if (!headless) splashWindow = createSplashWindow();
 
-    setupIpcListener(mainWindow);
+    setupIpcListener(mainWindow, setupMenu);
 
     // Display splash screen for minimum 2s then display main window
     waitAll([waitEvent(mainWindow, 'ready-to-show'), wait(200)]).then(async () => {
diff --git a/electron/i18n/en/translation.json b/electron/i18n/en/translation.json
deleted file mode 100644
index 8fa5cbbc..00000000
--- a/electron/i18n/en/translation.json
+++ /dev/null
@@ -1,67 +0,0 @@
-{
-    "menu": {
-        "app": {
-            "label": "App",
-            "about": "About",
-            "quit": "Quit",
-            "lang": "Language"
-        },
-        "file": {
-            "label": "File",
-            "new": "New",
-            "newWindow": "New Window",
-            "open": "Open",
-            "openInNewWindow": "Open in New Window",
-            "latest": "Recent Projects",
-            "import": "Import .epoc File",
-            "save": "Save",
-            "saveAs": "Save As..."
-        },
-        "edit": {
-            "label": "Edit",
-            "undo": "Undo",
-            "redo": "Redo",
-            "cut": "Cut",
-            "copy": "Copy",
-            "paste": "Paste",
-            "selectAll": "Select All"
-        },
-        "preview": {
-            "label": "Preview",
-            "start": "Start Preview",
-            "global": "Start Global Preview"
-        },
-        "help": {
-            "label": "Help",
-            "documentation": "Documentation",
-            "reportIssue": "Report Issue",
-            "mailSubject": "Editor Help",
-            "mailBody": "Describe your issue below:"
-        },
-        "devTools": "Developer Tools",
-        "reload": "Reload"
-    },
-    "context": {
-        "add": "Add",
-        "pasteHere": "Paste here",
-        "insertAfter": "Insert after",
-        "insertBefore": "Insert before",
-        "delete": "Delete",
-        "duplicate": "Duplicate",
-        "swapNext": "Swap with next",
-        "swapPrevious": "Swap with previous",
-        "insertEnd": "Insert at end",
-        "insertStart": "Insert at start",
-        "copyChapter": "Copy chapter",
-        "addChapter": "Add new chapter",
-        "addText": "Add text page",
-        "addVideo": "Add video page",
-        "addAudio": "Add audio page",
-        "addChoice": "Add multiple choice assessment",
-        "addDragAndDrop": "Add drag and drop assessment",
-        "addReorder": "Add reorder assessment",
-        "addSwipe": "Add swipe assessment",
-        "addDropdown": "Add dropdown assessment",
-        "addCustom": "Add custom assessment"
-    }
-}
diff --git a/electron/i18n/fr/translation.json b/electron/i18n/fr/translation.json
deleted file mode 100644
index 2e282b42..00000000
--- a/electron/i18n/fr/translation.json
+++ /dev/null
@@ -1,69 +0,0 @@
-{
-    "menu": {
-        "app": {
-            "label:": "App",
-            "about": "À propos",
-            "quit": "Quitter",
-            "lang": "Langage"
-        },
-        "file": {
-            "label": "Fichier",
-            "new": "Nouveau",
-            "newWindow": "Nouvelle fenêtre",
-            "open": "Ouvrir",
-            "openInNewWindow": "Ouvrir dans une nouvelle fenêtre",
-            "latest": "Projets récents",
-            "import": "Importer un fichier .epoc",
-            "save": "Sauvegarder",
-            "saveAs": "Sauvegarder sous..."
-        },
-        "edit": {
-            "label": "Édition",
-            "undo": "Annuler",
-            "redo": "Rétablir",
-            "cut": "Couper",
-            "copy": "Copier",
-            "paste": "Coller",
-            "selectAll": "Sélectionner tout"
-        },
-        "preview": {
-            "label": "Aperçu",
-            "start": "Lancer l'aperçu",
-            "global": "Lancer l'aperçu global"
-        },
-        "help": {
-            "label": "Aide",
-            "documentation": "Documentation",
-            "reportIssue": "Signaler un problème",
-            "mailSubject": "Aide éditeur",
-            "mailBody": "Décrivez votre problème ci-dessous:"
-        },
-        "devTools": "Outils de développement",
-        "reload": "Recharger"
-    },
-    "context": {
-        "add": "Ajouter",
-        "pasteHere": "Coller ici",
-        "insertAfter": "Insérer après",
-        "insertBefore": "Insérer avant",
-        "delete": "Supprimer",
-        "duplicate": "Dupliquer",
-        "swapNext": "Échanger avec le suivant",
-        "swapPrevious": "Échanger avec le précédent",
-        "insertEnd": "Insérer à la fin",
-        "insertStart": "Insérer au début",
-        "copyChapter": "Copier le chapitre",
-        "addChapter": "Ajouter un nouveau chapitre",
-
-        "addText": "Ajouter une page texte",
-        "addVideo": "Ajouter une page vidéo",
-        "addAudio": "Ajouter une page audio",
-
-        "addChoice": "Ajouter une évaluation QCM",
-        "addDragAndDrop": "Ajouter une évaluation Drag & Drop",
-        "addReorder": "Ajouter une évaluation Reorder",
-        "addSwipe": "Ajouter une évaluation Swipe",
-        "addDropdown": "Ajouter une évaluation Liste Déroulante",
-        "addCustom": "Ajouter une évaluation personnalisée"
-    }
-}
diff --git a/electron/i18n/i18next.config.js b/electron/i18n/i18next.config.js
deleted file mode 100644
index 1c2367d2..00000000
--- a/electron/i18n/i18next.config.js
+++ /dev/null
@@ -1,18 +0,0 @@
-const i18n = require('i18next');
-const Backend = require('i18next-node-fs-backend');
-const path = require('path');
-
-i18n.use(Backend).init({
-    backend: {
-        loadPath: path.join(__dirname, './{{lng}}/{{ns}}.json'),
-    },
-    fallbackLng: 'fr',
-    lng: 'fr',
-    supportedLngs: ['en', 'fr'], // Add supported languages
-    interpolation: {
-        escapeValue: false,
-    },
-    debug: process.env.NODE_ENV === 'development',
-});
-
-module.exports = i18n;
diff --git a/i18n/config.ts b/i18n/config.ts
new file mode 100644
index 00000000..ac4e3334
--- /dev/null
+++ b/i18n/config.ts
@@ -0,0 +1,14 @@
+import { createI18n } from 'vue-i18n';
+// @ts-expect-error // json files not resolved
+import fr from './fr/translation.json';
+// @ts-expect-error // json files not resolved
+import en from './en/translation.json';
+
+export const i18n = createI18n({
+    locale: 'en',
+    fallbackLocale: 'en',
+    messages: {
+        fr,
+        en,
+    },
+});
diff --git a/i18n/en/translation.json b/i18n/en/translation.json
new file mode 100644
index 00000000..bbbd0a79
--- /dev/null
+++ b/i18n/en/translation.json
@@ -0,0 +1,390 @@
+{
+    "menu": {
+        "app": {
+            "label": "App",
+            "about": "About",
+            "quit": "Quit",
+            "lang": "Language"
+        },
+        "file": {
+            "label": "File",
+            "new": "New",
+            "newWindow": "New Window",
+            "open": "Open",
+            "openInNewWindow": "Open in New Window",
+            "latest": "Recent Projects",
+            "import": "Import .epoc File",
+            "save": "Save",
+            "saveAs": "Save As..."
+        },
+        "edit": {
+            "label": "Edit",
+            "undo": "Undo",
+            "redo": "Redo",
+            "cut": "Cut",
+            "copy": "Copy",
+            "paste": "Paste",
+            "selectAll": "Select All"
+        },
+        "preview": {
+            "label": "Preview",
+            "start": "Start Preview",
+            "global": "Start Global Preview"
+        },
+        "help": {
+            "label": "Help",
+            "documentation": "Documentation",
+            "reportIssue": "Report Issue",
+            "mailSubject": "Editor Help",
+            "mailBody": "Describe your issue below:"
+        },
+        "devTools": "Developer Tools",
+        "reload": "Reload"
+    },
+    "context": {
+        "add": "Add",
+        "pasteHere": "Paste here",
+        "insertAfter": "Insert after",
+        "insertBefore": "Insert before",
+        "delete": "Delete",
+        "duplicate": "Duplicate",
+        "swapNext": "Swap with next",
+        "swapPrevious": "Swap with previous",
+        "insertEnd": "Insert at end",
+        "insertStart": "Insert at start",
+        "copyChapter": "Copy chapter",
+        "addChapter": "Add new chapter",
+        "addText": "Add text page",
+        "addVideo": "Add video page",
+        "addAudio": "Add audio page",
+        "addChoice": "Add multiple choice assessment",
+        "addDragAndDrop": "Add drag and drop assessment",
+        "addReorder": "Add reorder assessment",
+        "addSwipe": "Add swipe assessment",
+        "addDropdown": "Add dropdown assessment",
+        "addCustom": "Add custom assessment"
+    },
+
+    "global": {
+        "open": "Open",
+        "cancel": "Cancel",
+        "accept": "Accept",
+        "element": "Element",
+        "pleaseSelect": "Please select",
+        "and": "and",
+        "condition": "Condition",
+        "save": "Save",
+        "true": "True",
+        "false": "False",
+        "right": "right",
+        "left": "left",
+        "add": "Add",
+        "validate": "Confirm",
+        "delete": "Delete",
+        "conditions": "Conditions",
+        "label": "Label",
+        "chapter": "Chapter",
+        "objective": "Objective",
+        "name": "Name",
+        "file": "File"
+    },
+    "validation": {
+        "yes": "YES, DELETE",
+        "no": "NO, DO NOT DELETE",
+        "confirm": "Are you sure you want to delete this element?"
+    },
+    "landing": {
+        "published": "This ePoc is a published version, you need to import it before editing here",
+        "loadingPath": "Loading {path}",
+        "loadingEpoc": "Loading ePoc",
+        "open": "Open existing project",
+        "create": "Create new project",
+        "recents": "Recent files"
+    },
+    "editor": {
+        "select": "Click on the content element affected by the condition to select it"
+    },
+    "badge": {
+        "supported": "Supported file: SVG",
+        "select": "Select a file",
+        "conditions": "Badge earning conditions",
+        "add": "Add a condition",
+        "selectIcon": "Select an icon",
+        "defaultIcons": "Default icons",
+        "customIcons": "Custom icons",
+        "phrase": {
+            "type": {
+                "video": "the video",
+                "chapter": "the chapter",
+                "page": "the page",
+                "html": "the text",
+                "audio": "the audio",
+                "activity": "the assessment",
+                "question": "the question"
+            },
+            "verb": {
+                "started": {
+                    "true": "Have started",
+                    "false": "Have not started"
+                },
+                "completed": {
+                    "true": "Have completed",
+                    "false": "Have not completed"
+                },
+                "viewed": {
+                    "true": "Have viewed",
+                    "false": "Have not viewed"
+                },
+                "read": {
+                    "true": "Have read",
+                    "false": "Have not read"
+                },
+                "played": {
+                    "true": "Have played",
+                    "false": "Have not played"
+                },
+                "watched": {
+                    "true": "Have watched",
+                    "false": "Have not watched"
+                },
+                "listened": {
+                    "true": "Have listened to",
+                    "false": "Have not listened to"
+                },
+                "attempted": {
+                    "true": "Have attempted",
+                    "false": "Have not attempted"
+                },
+                "passed": {
+                    "true": "Have passed",
+                    "false": "Have failed"
+                },
+                "scored": "Have scored at least"
+            },
+            "scored": "{verb} {value} on"
+        }
+    },
+    "inputs": {
+        "manageConditions": "Configure conditions",
+        "updateIcon": "Update icon",
+        "leftChoice": "Left choice",
+        "rightChoice": "Right choice",
+        "accordion": "Expand/Collapse",
+        "accordionDescription": "Expand/Collapse with title and content"
+    },
+    "toast": {
+        "modelSaved": "Template saved 👌",
+        "modelExists": "Template already exists 🤔"
+    },
+    "settings": {
+        "title": "Settings",
+        "spellcheck": "Enable spell checking",
+        "lang": "Language"
+    },
+    "models": {
+        "title": "Page templates",
+        "empty": "No page template has been created",
+        "dragdrop": "Drag/drop to add a template",
+        "model": "Template"
+    },
+    "header": {
+        "undo": "Undo",
+        "redo": "Redo",
+        "preview": "Preview",
+        "publish": "Publish",
+        "adjust": "Adjust",
+        "never": "never",
+        "lastSave": "Last save:",
+        "new": "New ePoc"
+    },
+    "forms": {
+        "type": "Type here...",
+        "badge": {
+            "text": "Badge settings",
+            "updateIcon": "Update icon",
+            "obtention": "Badge earning conditions",
+            "presentation": "Badge presentation",
+            "presentationPlaceholder": "Enter badge presentation",
+            "icon": "Badge icon"
+        },
+        "content": {
+            "text": "Content",
+            "summary": "Summary",
+            "video": {
+                "label": "Video",
+                "placeholder": "Add a video",
+                "hint": "Recommended format: {format}"
+            },
+            "audio": {
+                "label": "Audio track",
+                "placeholder": "Add an audio track",
+                "hint": "Recommended format: MP3"
+            },
+            "subtitle": {
+                "label": "Language name",
+                "code": "Language code",
+                "placeholder": "Add subtitles",
+                "hint": "Accepted extensions: {extensions}"
+            },
+            "transcription": {
+                "label": "Transcription",
+                "placeholder": "Add a transcription",
+                "hint": "Accepted extensions: {extensions} <br>For users who do not wish or are unable to watch the video"
+            },
+            "thumbnail": {
+                "label": "Thumbnail",
+                "placeholder": "Add a thumbnail",
+                "hint": "Recommended format: same as video"
+            }
+        },
+        "buttons": {
+            "addBadge": "Add badge",
+            "saveModel": "Save template",
+            "duplicateEvaluation": "Duplicate assessment",
+            "duplicatePage": "Duplicate page",
+            "duplicateElement": "Duplicate element",
+            "backToPage": "Back to page",
+            "backToEpoc": "Back to ePoc"
+        },
+        "node": {
+            "conditionPlaceholder": "Enter condition {condition}...",
+            "choice": "Choice",
+            "course": "Course {course}",
+            "conditional": "Conditional content",
+            "title": "Title",
+            "subtitle": "Subtitle",
+            "duration": "Duration (in minutes)",
+            "objectives": "Learning objectives",
+            "about": "About the ePoc",
+            "cover": {
+                "title": "Cover image",
+                "placeholder": "Add a cover image",
+                "hint": "Recommended format: square (180x180)<br> Image visible in the ePoc list"
+            },
+            "teaser": {
+                "title": "Video teaser",
+                "placeholder": "Add a teaser",
+                "hint": "Recommended format: 16:9<br> Video visible on the ePoc presentation page"
+            },
+            "thumbnail": {
+                "title": "Video thumbnail",
+                "hint": "Recommended format: same as video <br> Image visible on the ePoc presentation page"
+            },
+            "presentation": "Presentation",
+            "edition": "Edition",
+            "author": {
+                "title": "Author | Authors",
+                "placeholder": "Jane Doe",
+                "image": {
+                    "title": "Author image",
+                    "placeholder": "Add an image",
+                    "hint": "Recommended format: square (100x100)<br> Image visible on the ePoc presentation page"
+                },
+                "position": {
+                    "title": "Position",
+                    "placeholder": "Researcher at Inria",
+                    "hint": "Profession, position, affiliation..."
+                },
+                "biography": "Short author biography"
+            },
+            "certificateBadge": "Number of badges required for certification",
+            "certificateScore": "Score required for certification",
+            "certificateScoreHint": "Not considered if the number of badges required for certification is greater than 0",
+            "chapterLabel": "Chapter label",
+            "chapterDuration": "Chapter duration (in minutes)",
+            "plugin": {
+                "title": "Plugin | Plugins",
+                "script": "Script file",
+                "scriptPlaceholder": "Add a script file",
+                "template": "Plugin HTML template",
+                "templatePlaceholder": "Add HTML template"
+            },
+            "licence": {
+                "title": "License",
+                "hint": "Name of your ePoc content license",
+                "url": "URL",
+                "urlPlaceholder": "https://creativecommons.org/licenses/by/4.0/deed",
+                "urlHint": "Full text of the chosen license"
+            },
+            "page": {
+                "title": "Page",
+                "hidden": "Hidden in table of contents",
+                "conditional": "Only displays under certain conditions",
+                "conditionalHint": "Option used for conditional display",
+                "components": "Components"
+            },
+            "activity": "Assessment"
+        }
+    },
+    "questions": {
+        "configuration": "Assessment configuration",
+        "question": "Question",
+        "askQuestion": "Ask the question",
+        "instruction": "Instruction",
+        "instructionPlaceholder": "Instruction to answer the question",
+        "explanation": "Explanation",
+        "typeExplanation": "Enter an explanation",
+        "addExplanation": "Add an explanation",
+        "response": "Answer",
+        "responses": "Answers",
+        "typeResponse": "Enter an answer",
+        "correctResponse": "Correct answer",
+        "categories": "Proposed answer categories",
+        "category": "Category",
+        "typeCategory": "Enter a category title",
+        "proposedChoices": "Proposed choice categories",
+        "proposedResponses": "Proposed answers",
+        "cards": "Cards",
+        "card": "Card",
+        "typeProposition": "Enter a proposition",
+        "template": {
+            "title": "Template",
+            "select": "Select a template",
+            "data": "Data",
+            "key": "Key",
+            "value": "Value"
+        },
+        "types": {
+            "qcm": "MCQ",
+            "dragDrop": "Drag & Drop",
+            "reorder": "Reorder",
+            "swipe": "Swipe",
+            "dropdownList": "Dropdown list",
+            "custom": "Custom question"
+        },
+        "score": "Score"
+    },
+    "sidebar": {
+        "content": {
+            "text": "Text",
+            "video": "Video",
+            "audio": "Audio",
+            "textTooltip": "Drag/drop to add text",
+            "videoTooltip": "Drag/drop to add video",
+            "audioTooltip": "Drag/drop to add audio"
+        },
+        "pages": {
+            "question": "Question",
+            "conditions": "Conditions",
+            "conditionsLegacy": "Conditions (legacy)",
+            "model": "Template",
+            "badge": "Badge",
+            "questionTooltip": "Click to add a question",
+            "conditionsTooltip": "Drag/drop to add a condition",
+            "modelTooltip": "Click to open template menu",
+            "badgeTooltip": "Click to open badge menu"
+        }
+    },
+    "verbs": {
+        "started": "Started",
+        "completed": "Completed",
+        "viewed": "Viewed",
+        "read": "Read",
+        "played": "Played",
+        "watched": "Watched",
+        "listened": "Listened",
+        "attempted": "Attempted",
+        "scored": "Scored",
+        "passed": "Passed"
+    }
+}
diff --git a/i18n/fr/translation.json b/i18n/fr/translation.json
new file mode 100644
index 00000000..70a625ac
--- /dev/null
+++ b/i18n/fr/translation.json
@@ -0,0 +1,392 @@
+{
+    "menu": {
+        "app": {
+            "label:": "App",
+            "about": "À propos",
+            "quit": "Quitter",
+            "lang": "Langage"
+        },
+        "file": {
+            "label": "Fichier",
+            "new": "Nouveau",
+            "newWindow": "Nouvelle fenêtre",
+            "open": "Ouvrir",
+            "openInNewWindow": "Ouvrir dans une nouvelle fenêtre",
+            "latest": "Projets récents",
+            "import": "Importer un fichier .epoc",
+            "save": "Sauvegarder",
+            "saveAs": "Sauvegarder sous..."
+        },
+        "edit": {
+            "label": "Édition",
+            "undo": "Annuler",
+            "redo": "Rétablir",
+            "cut": "Couper",
+            "copy": "Copier",
+            "paste": "Coller",
+            "selectAll": "Sélectionner tout"
+        },
+        "preview": {
+            "label": "Aperçu",
+            "start": "Lancer l'aperçu",
+            "global": "Lancer l'aperçu global"
+        },
+        "help": {
+            "label": "Aide",
+            "documentation": "Documentation",
+            "reportIssue": "Signaler un problème",
+            "mailSubject": "Aide éditeur",
+            "mailBody": "Décrivez votre problème ci-dessous:"
+        },
+        "devTools": "Outils de développement",
+        "reload": "Recharger"
+    },
+    "context": {
+        "add": "Ajouter",
+        "pasteHere": "Coller ici",
+        "insertAfter": "Insérer après",
+        "insertBefore": "Insérer avant",
+        "delete": "Supprimer",
+        "duplicate": "Dupliquer",
+        "swapNext": "Échanger avec le suivant",
+        "swapPrevious": "Échanger avec le précédent",
+        "insertEnd": "Insérer à la fin",
+        "insertStart": "Insérer au début",
+        "copyChapter": "Copier le chapitre",
+        "addChapter": "Ajouter un nouveau chapitre",
+
+        "addText": "Ajouter une page texte",
+        "addVideo": "Ajouter une page vidéo",
+        "addAudio": "Ajouter une page audio",
+
+        "addChoice": "Ajouter une évaluation QCM",
+        "addDragAndDrop": "Ajouter une évaluation Drag & Drop",
+        "addReorder": "Ajouter une évaluation Reorder",
+        "addSwipe": "Ajouter une évaluation Swipe",
+        "addDropdown": "Ajouter une évaluation Liste Déroulante",
+        "addCustom": "Ajouter une évaluation personnalisée"
+    },
+
+    "global": {
+        "open": "Ouvrir",
+        "cancel": "Annuler",
+        "accept": "Accepter",
+        "element": "Élément",
+        "pleaseSelect": "Veuillez sélectionner",
+        "and": "et",
+        "condition": "Condition",
+        "save": "Sauvegarder",
+        "true": "Vrai",
+        "false": "Faux",
+        "right": "droite",
+        "left": "gauche",
+        "add": "Ajouter",
+        "validate": "Valider",
+        "delete": "Supprimer",
+        "conditions": "Conditions",
+        "label": "Label",
+        "chapter": "Chapitre",
+        "objective": "Objectif",
+        "name": "Nom",
+        "file": "Fichier"
+    },
+    "validation": {
+        "yes": "OUI, SUPPRIMER",
+        "no": "NON, NE PAS SUPPRIMER",
+        "confirm": "Êtes-vous sûr de vouloir supprimer cet élément ?"
+    },
+    "landing": {
+        "published": "Cet ePoc est une version publiée, vous devez l'importer avant de pouvoir l'éditer ici",
+        "loadingPath": "Chargement de {path}",
+        "loadingEpoc": "Chargement de l'ePoc",
+        "open": "Ouvrir un projet existant",
+        "create": "Créer un nouveau projet",
+        "recents": "Fichiers récents"
+    },
+    "editor": {
+        "select": "Cliquer sur l'élément de contenu concerné par la condition pour la sélectionner"
+    },
+    "badge": {
+        "supported": "Fichier supporté: SVG",
+        "select": "Sélectionner un fichier",
+        "conditions": "Conditions d'obtention du badge",
+        "add": "Ajouter une condition",
+        "selectIcon": "Sélectionner une icône",
+        "defaultIcons": "Icônes par défaut",
+        "customIcons": "Icônes personnalisées",
+        "phrase": {
+            "type": {
+                "video": "la vidéo",
+                "chapter": "le chapitre",
+                "page": "la page",
+                "html": "le texte",
+                "audio": "l'audio",
+                "activity": "l'évaluation",
+                "question": "la question"
+            },
+            "verb": {
+                "started": {
+                    "true": "Avoir commencé",
+                    "false": "Ne pas avoir pas commencé"
+                },
+                "completed": {
+                    "true": "Avoir terminé",
+                    "false": "Ne pas avoir terminé"
+                },
+                "viewed": {
+                    "true": "Avoir vu",
+                    "false": "Ne pas avoir vu"
+                },
+                "read": {
+                    "true": "Avoir lu",
+                    "false": "Ne pas avoir lu"
+                },
+                "played": {
+                    "true": "Avoir lancé",
+                    "false": "Ne pas avoir lancé"
+                },
+                "watched": {
+                    "true": "Avoir regardé",
+                    "false": "Ne pas avoir regardé"
+                },
+                "listened": {
+                    "true": "Avoir écouté",
+                    "false": "Ne pas avoir écouté"
+                },
+                "attempted": {
+                    "true": "Avoir tenté",
+                    "false": "Ne pas avoir tenté"
+                },
+                "passed": {
+                    "true": "Avoir réussi",
+                    "false": "Avoir échoué"
+                },
+                "scored": "Avoir obtenu un score d'au moins"
+            },
+            "scored": "{verb} {value} à"
+        }
+    },
+    "inputs": {
+        "manageConditions": "Configurer les conditions",
+        "updateIcon": "Modifier l'icône",
+        "leftChoice": "Choix gauche",
+        "rightChoice": "Choix droit",
+        "accordion": "Plier/déplier",
+        "accordionDescription": "Plier/déplier avec titre et contenu"
+    },
+    "toast": {
+        "modelSaved": "Modèle sauvegardé 👌",
+        "modelExists": "Le modèle existe déjà 🤔"
+    },
+    "settings": {
+        "title": "Paramètres",
+        "spellcheck": "Activer la vérification orthographique",
+        "lang": "Langue"
+    },
+    "models": {
+        "title": "Modèles de page",
+        "empty": "Aucun modèle de page n'as été créé",
+        "dragdrop": "Glisser/déposer pour ajouter un modèle",
+        "model": "Modèle"
+    },
+    "header": {
+        "undo": "Annuler",
+        "redo": "Rétablir",
+        "preview": "Aperçu",
+        "publish": "Publier",
+        "adjust": "Ajuster",
+        "never": "jamais",
+        "lastSave": "Dernière sauvegarde :",
+        "new": "Nouvel ePoc"
+    },
+    "forms": {
+        "type": "Saisissez...",
+        "badge": {
+            "text": "Paramètres du badge",
+            "updateIcon": "Modifier l'icône",
+            "obtention": "Conditions d'obtention du badge",
+            "presentation": "Présentation du badge",
+            "presentationPlaceholder": "Saisissez une présentation du badge",
+            "icon": "Icône du badge"
+        },
+        "content": {
+            "text": "Contenu",
+            "summary": "Résumé",
+            "video": {
+                "label": "Vidéo",
+                "placeholder": "Ajouter une vidéo",
+                "hint": "Format recommandé : {format}"
+            },
+            "audio": {
+                "label": "Piste audio",
+                "placeholder": "Ajouter une piste audio",
+                "hint": "Format recommandé : MP3"
+            },
+            "subtitle": {
+                "label": "Nom de la langue",
+                "code": "Code de la langue",
+                "placeholder": "Ajouter des sous-titre",
+                "hint": "Extensions acceptées : {extensions}"
+            },
+            "transcription": {
+                "label": "Transcription",
+                "placeholder": "Ajouter une transcription",
+                "hint": "Extensions acceptées : {extensions} <br>Pour les utilisateurs qui ne souhaitent pas ou ne sont pas en capacité d'écouter la vidéo"
+            },
+            "thumbnail": {
+                "label": "Vignette",
+                "placeholder": "Ajouter une vignette",
+                "hint": "Format recommandé : idem à la vidéo"
+            }
+        },
+        "buttons": {
+            "addBadge": "Ajouter un badge",
+            "saveModel": "Sauvegarder le modèle",
+            "duplicateEvaluation": "Dupliquer l'évaluation",
+            "duplicatePage": "Dupliquer la page",
+            "duplicateElement": "Dupliquer l'élement",
+            "backToPage": "Revenir à la page",
+            "backToEpoc": "Revenir à l'ePoc"
+        },
+        "node": {
+            "conditionPlaceholder": "Saisissez la condition {condition}...",
+            "choice": "Choix",
+            "course": "Cours {course}",
+            "conditional": "Contenus conditionnels",
+            "title": "Titre",
+            "subtitle": "Sous-titre",
+            "duration": "Durée (en minutes)",
+            "objectives": "Objectifs pédagogiques",
+            "about": "À propos de l'ePoc",
+            "cover": {
+                "title": "Image de couverture",
+                "placeholder": "Ajouter une image de couverture",
+                "hint": "Format recommandé : carré (180x180)<br> Image visible dans la liste des ePocs"
+            },
+            "teaser": {
+                "title": "Teaser vidéo",
+                "placeholder": "Ajouter un teaser",
+                "hint": "Format recommandé : 16:9<br> Vidéo visible dans la page de présentation de l'ePoc"
+            },
+            "thumbnail": {
+                "title": "Vignette de la vidéo",
+                "hint": "Format recommandé : idem que la vidéo <br> Image visible dans la page de présentation de l'ePoc"
+            },
+            "presentation": "Présentation",
+            "edition": "Édition",
+            "author": {
+                "title": "Auteur | Auteurs",
+                "placeholder": "Jeanne Dupont",
+                "image": {
+                    "title": "Image de l'auteur",
+                    "placeholder": "Ajouter une image",
+                    "hint": "Format recommandé : carré (100x100)<br> Image visible dans la page de présentation de l'ePoc"
+                },
+                "position": {
+                    "title": "Fonction",
+                    "placeholder": "Chercheur à l'Inria",
+                    "hint": "Profession, fonction, affiliation…"
+                },
+                "biography": "Courte biographie de l'auteur"
+            },
+            "certificateBadge": "Nombre de badge pour obtenir l'attestation",
+            "certificateScore": "Score pour obtenir l'attestation",
+            "certificateScoreHint": "N'est pas pris en compte si le nombre de badge pour obtenir l'attestation est supérieur à 0",
+            "chapterLabel": "Label des chapitres",
+            "chapterDuration": "Durée des chapitres (en minutes)",
+            "plugin": {
+                "title": "Plugin | Plugins",
+                "script": "Fichier de script",
+                "scriptPlaceholder": "Ajouter un fichier de script",
+                "template": "Template html du plugin",
+                "templatePlaceholder": "Ajouter un template html"
+            },
+            "licence": {
+                "title": "Licence",
+                "hint": "Nom de la licence de votre contenu ePoc",
+                "url": "URL",
+                "urlPlaceholder": "https://creativecommons.org/licenses/by/4.0/deed",
+                "urlHint": "Text complet de la licence choisie"
+            },
+            "page": {
+                "title": "Page",
+                "hidden": "Caché dans la table des matières",
+                "conditional": "Ne s'affiche qu'à certaines conditions",
+                "conditionalHint": "Option utilisée pour l'affichage conditionnel",
+                "components": "Composants"
+            },
+            "activity": "Évaluation"
+        }
+    },
+    "questions": {
+        "configuration": "Configuration de l'évaluation",
+        "question": "Question",
+        "askQuestion": "Posez la question",
+        "instruction": "Consigne",
+        "instructionPlaceholder": "Instruction pour répondre à la question",
+        "explanation": "Explication",
+        "typeExplanation": "Saisissez une explication",
+        "addExplanation": "Ajouter une explication",
+        "response": "Réponse",
+        "responses": "Réponses",
+        "typeResponse": "Saisissez une réponse",
+        "correctResponse": "Bonne réponse",
+        "categories": "Catégories de réponses proposées",
+        "category": "Catégorie",
+        "typeCategory": "Saisissez un intitulé catégorie",
+        "proposedChoices": "Catégories de choix proposées",
+        "proposedResponses": "Réponses proposées",
+        "cards": "Cartes",
+        "card": "Carte",
+        "typeProposition": "Saisissez une proposition",
+        "template": {
+            "title": "Template",
+            "select": "Selectionnez un template",
+            "data": "Données",
+            "key": "Clé",
+            "value": "Valeur"
+        },
+        "types": {
+            "qcm": "QCM",
+            "dragDrop": "Drag & Drop",
+            "reorder": "Reorder",
+            "swipe": "Swipe",
+            "dropdownList": "Liste déroulante",
+            "custom": "Question personnalisée"
+        },
+        "score": "Score"
+    },
+    "sidebar": {
+        "content": {
+            "text": "Texte",
+            "video": "Vidéo",
+            "audio": "Audio",
+            "textTooltip": "Glisser/déposer pour ajouter un texte",
+            "videoTooltip": "Glisser/déposer pour ajouter une vidéo",
+            "audioTooltip": "Glisser/déposer pour ajouter un audio"
+        },
+        "pages": {
+            "question": "Question",
+            "conditions": "Conditions",
+            "conditionsLegacy": "Conditions (legacy)",
+            "model": "Modèle",
+            "badge": "Badge",
+            "questionTooltip": "Cliquer pour ajouter une question",
+            "conditionsTooltip": "Glisser/déposer pour ajouter une condition",
+            "modelTooltip": "Cliquer pour ouvrir le menu modèle",
+            "badgeTooltip": "Cliquer pour ouvrir le menu badge"
+        }
+    },
+    "verbs": {
+        "started": "Commencé",
+        "completed": "Terminé",
+        "viewed": "Vu",
+        "read": "Lu",
+        "played": "Joué",
+        "watched": "Regardé",
+        "listened": "Écouté",
+        "attempted": "Tenté",
+        "scored": "Obtenu un score de",
+        "passed": "Réussi"
+    }
+}
diff --git a/src/features/settings/SettingsModal.vue b/src/features/settings/SettingsModal.vue
index b119d3ac..05f29b4b 100644
--- a/src/features/settings/SettingsModal.vue
+++ b/src/features/settings/SettingsModal.vue
@@ -1,7 +1,7 @@
 <script setup lang="ts">
 import Modal from '@/src/components/LayoutModal.vue';
 import SettingsInput from './SettingsInput.vue';
-import { ref, onMounted, watch } from 'vue';
+import { ref, watch } from 'vue';
 import { useSettingsStore } from '@/src/shared/stores';
 import { useI18n } from 'vue-i18n';
 
@@ -10,14 +10,10 @@ const { locale } = useI18n();
 const modal = ref(null);
 const settingsStore = useSettingsStore();
 const spellcheck = ref(false);
-
-// onMounted(() => {
-//     if (!settingsStore.init) {
-//         settingsStore.init();
-//     }
-// });
+const localeTmp = ref();
 
 function save() {
+    locale.value = localeTmp.value;
     settingsStore.setSettings(spellcheck.value);
     modal.value.close();
 }
@@ -29,7 +25,11 @@ function open() {
 watch(
     () => modal.value?.isOpen,
     (isOpen) => {
-        if (isOpen) spellcheck.value = settingsStore?.settings?.spellcheck;
+        // set values here to make sure the settings were fetched from the store
+        if (isOpen) {
+            spellcheck.value = settingsStore?.settings?.spellcheck;
+            localeTmp.value = locale.value;
+        }
     },
 );
 
@@ -47,7 +47,7 @@ defineExpose({
         <div class="settings">
             <SettingsInput v-model="spellcheck" type="toggle" :label="$t('settings.spellcheck')" />
             <SettingsInput
-                v-model="locale"
+                v-model="localeTmp"
                 type="select"
                 :label="$t('settings.lang')"
                 :options="[
@@ -59,7 +59,7 @@ defineExpose({
 
         <template #footer>
             <button class="btn-choice cancel" @click="modal.close">{{ $t('global.cancel') }}</button>
-            <button class="btn-choice save" @click="save">{{ $t('global.validate') }}</button>
+            <button class="btn-choice save" @click="save">{{ $t('global.save') }}</button>
         </template>
     </Modal>
 </template>
diff --git a/src/i18n/config.ts b/src/i18n/config.ts
deleted file mode 100644
index e547d5c5..00000000
--- a/src/i18n/config.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-import { createI18n } from 'vue-i18n';
-import { frMessages } from './fr';
-import { enMessages } from './en';
-
-export const i18n = createI18n({
-    locale: 'en',
-    fallbackLocale: 'en',
-    messages: {
-        fr: frMessages,
-        en: enMessages,
-    },
-});
diff --git a/src/i18n/en.ts b/src/i18n/en.ts
deleted file mode 100644
index bc0cbf86..00000000
--- a/src/i18n/en.ts
+++ /dev/null
@@ -1,324 +0,0 @@
-export const enMessages = {
-    global: {
-        open: 'Open',
-        cancel: 'Cancel',
-        accept: 'Accept',
-        element: 'Element',
-        pleaseSelect: 'Please select',
-        and: 'and',
-        condition: 'Condition',
-        save: 'Save',
-        true: 'True',
-        false: 'False',
-        right: 'right',
-        left: 'left',
-        add: 'Add',
-        validate: 'Confirm',
-        delete: 'Delete',
-        conditions: 'Conditions',
-        label: 'Label',
-        chapter: 'Chapter',
-        objective: 'Objective',
-        name: 'Name',
-        file: 'File',
-    },
-    validation: {
-        yes: 'YES, DELETE',
-        no: 'NO, DO NOT DELETE',
-        confirm: 'Are you sure you want to delete this element?',
-    },
-    landing: {
-        published: 'This ePoc is a published version, you need to import it before editing here',
-        loadingPath: 'Loading {path}',
-        loadingEpoc: 'Loading ePoc',
-        open: 'Open existing project',
-        create: 'Create new project',
-        recents: 'Recent files',
-    },
-    editor: {
-        select: 'Click on the content element affected by the condition to select it',
-    },
-    badge: {
-        supported: 'Supported file: SVG',
-        select: 'Select a file',
-        conditions: 'Badge earning conditions',
-        add: 'Add a condition',
-        selectIcon: 'Select an icon',
-        defaultIcons: 'Default icons',
-        customIcons: 'Custom icons',
-        phrase: {
-            type: {
-                video: 'the video',
-                chapter: 'the chapter',
-                page: 'the page',
-                html: 'the text',
-                audio: 'the audio',
-                activity: 'the assessment',
-                question: 'the question',
-            },
-            verb: {
-                started: {
-                    true: 'Have started',
-                    false: 'Have not started',
-                },
-                completed: {
-                    true: 'Have completed',
-                    false: 'Have not completed',
-                },
-                viewed: {
-                    true: 'Have viewed',
-                    false: 'Have not viewed',
-                },
-                read: {
-                    true: 'Have read',
-                    false: 'Have not read',
-                },
-                played: {
-                    true: 'Have played',
-                    false: 'Have not played',
-                },
-                watched: {
-                    true: 'Have watched',
-                    false: 'Have not watched',
-                },
-                listened: {
-                    true: 'Have listened to',
-                    false: 'Have not listened to',
-                },
-                attempted: {
-                    true: 'Have attempted',
-                    false: 'Have not attempted',
-                },
-                passed: {
-                    true: 'Have passed',
-                    false: 'Have failed',
-                },
-                scored: 'Have scored at least',
-            },
-            scored: '{verb} {value} on',
-        },
-    },
-    inputs: {
-        manageConditions: 'Configure conditions',
-        updateIcon: 'Update icon',
-        leftChoice: 'Left choice',
-        rightChoice: 'Right choice',
-        accordion: 'Expand/Collapse',
-        accordionDescription: 'Expand/Collapse with title and content',
-    },
-    toast: {
-        modelSaved: 'Template saved 👌',
-        modelExists: 'Template already exists 🤔',
-    },
-    settings: {
-        title: 'Settings',
-        spellcheck: 'Enable spell checking',
-        lang: 'Language',
-    },
-    models: {
-        title: 'Page templates',
-        empty: 'No page template has been created',
-        dragdrop: 'Drag/drop to add a template',
-        model: 'Template',
-    },
-    header: {
-        undo: 'Undo',
-        redo: 'Redo',
-        preview: 'Preview',
-        publish: 'Publish',
-        adjust: 'Adjust',
-        never: 'never',
-        lastSave: 'Last save:',
-        new: 'New ePoc',
-    },
-    forms: {
-        type: 'Type here...',
-        badge: {
-            text: 'Badge settings',
-            updateIcon: 'Update icon',
-            obtention: 'Badge earning conditions',
-            presentation: 'Badge presentation',
-            presentationPlaceholder: 'Enter badge presentation',
-            icon: 'Badge icon',
-        },
-        content: {
-            text: 'Content',
-            summary: 'Summary',
-            video: {
-                label: 'Video',
-                placeholder: 'Add a video',
-                hint: 'Recommended format: {format}',
-            },
-            audio: {
-                label: 'Audio track',
-                placeholder: 'Add an audio track',
-                hint: 'Recommended format: MP3',
-            },
-            subtitle: {
-                label: 'Language name',
-                code: 'Language code',
-                placeholder: 'Add subtitles',
-                hint: 'Accepted extensions: {extensions}',
-            },
-            transcription: {
-                label: 'Transcription',
-                placeholder: 'Add a transcription',
-                hint: 'Accepted extensions: {extensions} <br>For users who do not wish or are unable to watch the video',
-            },
-            thumbnail: {
-                label: 'Thumbnail',
-                placeholder: 'Add a thumbnail',
-                hint: 'Recommended format: same as video',
-            },
-        },
-        buttons: {
-            addBadge: 'Add badge',
-            saveModel: 'Save template',
-            duplicateEvaluation: 'Duplicate assessment',
-            duplicatePage: 'Duplicate page',
-            duplicateElement: 'Duplicate element',
-            backToPage: 'Back to page',
-            backToEpoc: 'Back to ePoc',
-        },
-        node: {
-            conditionPlaceholder: 'Enter condition {condition}...',
-            choice: 'Choice',
-            course: 'Course {course}',
-            conditional: 'Conditional content',
-            title: 'Title',
-            subtitle: 'Subtitle',
-            duration: 'Duration (in minutes)',
-            objectives: 'Learning objectives',
-            about: 'About the ePoc',
-            cover: {
-                title: 'Cover image',
-                placeholder: 'Add a cover image',
-                hint: 'Recommended format: square (180x180)<br> Image visible in the ePoc list',
-            },
-            teaser: {
-                title: 'Video teaser',
-                placeholder: 'Add a teaser',
-                hint: 'Recommended format: 16:9<br> Video visible on the ePoc presentation page',
-            },
-            thumbnail: {
-                title: 'Video thumbnail',
-                hint: 'Recommended format: same as video <br> Image visible on the ePoc presentation page',
-            },
-            presentation: 'Presentation',
-            edition: 'Edition',
-            author: {
-                title: 'Author | Authors',
-                placeholder: 'Jane Doe',
-                image: {
-                    title: 'Author image',
-                    placeholder: 'Add an image',
-                    hint: 'Recommended format: square (100x100)<br> Image visible on the ePoc presentation page',
-                },
-                position: {
-                    title: 'Position',
-                    placeholder: 'Researcher at Inria',
-                    hint: 'Profession, position, affiliation...',
-                },
-                biography: 'Short author biography',
-            },
-            certificateBadge: 'Number of badges required for certification',
-            certificateScore: 'Score required for certification',
-            certificateScoreHint: 'Not considered if the number of badges required for certification is greater than 0',
-            chapterLabel: 'Chapter label',
-            chapterDuration: 'Chapter duration (in minutes)',
-            plugin: {
-                title: 'Plugin | Plugins',
-                script: 'Script file',
-                scriptPlaceholder: 'Add a script file',
-                template: 'Plugin HTML template',
-                templatePlaceholder: 'Add HTML template',
-            },
-            licence: {
-                title: 'License',
-                hint: 'Name of your ePoc content license',
-                url: 'URL',
-                urlPlaceholder: 'https://creativecommons.org/licenses/by/4.0/deed',
-                urlHint: 'Full text of the chosen license',
-            },
-            page: {
-                title: 'Page',
-                hidden: 'Hidden in table of contents',
-                conditional: 'Only displays under certain conditions',
-                conditionalHint: 'Option used for conditional display',
-                components: 'Components',
-            },
-            activity: 'Assessment',
-        },
-    },
-    questions: {
-        configuration: 'Assessment configuration',
-        question: 'Question',
-        askQuestion: 'Ask the question',
-        instruction: 'Instruction',
-        instructionPlaceholder: 'Instruction to answer the question',
-        explanation: 'Explanation',
-        typeExplanation: 'Enter an explanation',
-        addExplanation: 'Add an explanation',
-        response: 'Answer',
-        responses: 'Answers',
-        typeResponse: 'Enter an answer',
-        correctResponse: 'Correct answer',
-        categories: 'Proposed answer categories',
-        category: 'Category',
-        typeCategory: 'Enter a category title',
-        proposedChoices: 'Proposed choice categories',
-        proposedResponses: 'Proposed answers',
-        cards: 'Cards',
-        card: 'Card',
-        typeProposition: 'Enter a proposition',
-        template: {
-            title: 'Template',
-            select: 'Select a template',
-            data: 'Data',
-            key: 'Key',
-            value: 'Value',
-        },
-        types: {
-            qcm: 'MCQ',
-            dragDrop: 'Drag & Drop',
-            reorder: 'Reorder',
-            swipe: 'Swipe',
-            dropdownList: 'Dropdown list',
-            custom: 'Custom question',
-        },
-        score: 'Score',
-    },
-    sidebar: {
-        content: {
-            text: 'Text',
-            video: 'Video',
-            audio: 'Audio',
-            textTooltip: 'Drag/drop to add text',
-            videoTooltip: 'Drag/drop to add video',
-            audioTooltip: 'Drag/drop to add audio',
-        },
-        pages: {
-            question: 'Question',
-            conditions: 'Conditions',
-            conditionsLegacy: 'Conditions (legacy)',
-            model: 'Template',
-            badge: 'Badge',
-            questionTooltip: 'Click to add a question',
-            conditionsTooltip: 'Drag/drop to add a condition',
-            modelTooltip: 'Click to open template menu',
-            badgeTooltip: 'Click to open badge menu',
-        },
-    },
-    verbs: {
-        started: 'Started',
-        completed: 'Completed',
-        viewed: 'Viewed',
-        read: 'Read',
-        played: 'Played',
-        watched: 'Watched',
-        listened: 'Listened',
-        attempted: 'Attempted',
-        scored: 'Scored',
-        passed: 'Passed',
-    },
-};
diff --git a/src/i18n/fr.ts b/src/i18n/fr.ts
deleted file mode 100644
index c3bd7caa..00000000
--- a/src/i18n/fr.ts
+++ /dev/null
@@ -1,325 +0,0 @@
-export const frMessages = {
-    global: {
-        open: 'Ouvrir',
-        cancel: 'Annuler',
-        accept: 'Accepter',
-        element: 'Élément',
-        pleaseSelect: 'Veuillez sélectionner',
-        and: 'et',
-        condition: 'Condition',
-        save: 'Sauvegarder',
-        true: 'Vrai',
-        false: 'Faux',
-        right: 'droite',
-        left: 'gauche',
-        add: 'Ajouter',
-        validate: 'Valider',
-        delete: 'Supprimer',
-        conditions: 'Conditions',
-        label: 'Label',
-        chapter: 'Chapitre',
-        objective: 'Objectif',
-        name: 'Nom',
-        file: 'Fichier',
-    },
-    validation: {
-        yes: 'OUI, SUPPRIMER',
-        no: 'NON, NE PAS SUPPRIMER',
-        confirm: 'Êtes-vous sûr de vouloir supprimer cet élément ?',
-    },
-    landing: {
-        published: "Cet ePoc est une version publiée, vous devez l'importer avant de pouvoir l'éditer ici",
-        loadingPath: 'Chargement de {path}',
-        loadingEpoc: "Chargement de l'ePoc",
-        open: 'Ouvrir un projet existant',
-        create: 'Créer un nouveau projet',
-        recents: 'Fichiers récents',
-    },
-    editor: {
-        select: "Cliquer sur l'élément de contenu concerné par la condition pour la sélectionner",
-    },
-    badge: {
-        supported: 'Fichier supporté: SVG',
-        select: 'Sélectionner un fichier',
-        conditions: "Conditions d'obtention du badge",
-        add: 'Ajouter une condition',
-        selectIcon: 'Sélectionner une icône',
-        defaultIcons: 'Icônes par défaut',
-        customIcons: 'Icônes personnalisées',
-        phrase: {
-            type: {
-                video: 'la vidéo',
-                chapter: 'le chapitre',
-                page: 'la page',
-                html: 'le texte',
-                audio: "l'audio",
-                activity: "l'évaluation",
-                question: 'la question',
-            },
-            verb: {
-                started: {
-                    true: 'Avoir commencé',
-                    false: 'Ne pas avoir pas commencé',
-                },
-                completed: {
-                    true: 'Avoir terminé',
-                    false: 'Ne pas avoir terminé',
-                },
-                viewed: {
-                    true: 'Avoir vu',
-                    false: 'Ne pas avoir vu',
-                },
-                read: {
-                    true: 'Avoir lu',
-                    false: 'Ne pas avoir lu',
-                },
-                played: {
-                    true: 'Avoir lancé',
-                    false: 'Ne pas avoir lancé',
-                },
-                watched: {
-                    true: 'Avoir regardé',
-                    false: 'Ne pas avoir regardé',
-                },
-                listened: {
-                    true: 'Avoir écouté',
-                    false: 'Ne pas avoir écouté',
-                },
-                attempted: {
-                    true: 'Avoir tenté',
-                    false: 'Ne pas avoir tenté',
-                },
-                passed: {
-                    true: 'Avoir réussi',
-                    false: 'Avoir échoué',
-                },
-                scored: "Avoir obtenu un score d'au moins",
-            },
-            scored: '{verb} {value} à',
-        },
-    },
-    inputs: {
-        manageConditions: 'Configurer les conditions',
-        updateIcon: "Modifier l'icône",
-        leftChoice: 'Choix gauche',
-        rightChoice: 'Choix droit',
-        accordion: 'Plier/déplier',
-        accordionDescription: 'Plier/déplier avec titre et contenu',
-    },
-    toast: {
-        modelSaved: 'Modèle sauvegardé 👌',
-        modelExists: 'Le modèle existe déjà 🤔',
-    },
-    settings: {
-        title: 'Paramètres',
-        spellcheck: 'Activer la vérification orthographique',
-        lang: 'Langue',
-    },
-    models: {
-        title: 'Modèles de page',
-        empty: "Aucun modèle de page n'as été créé",
-        dragdrop: 'Glisser/déposer pour ajouter un modèle',
-        model: 'Modèle',
-    },
-    header: {
-        undo: 'Annuler',
-        redo: 'Rétablir',
-        preview: 'Aperçu',
-        publish: 'Publier',
-        adjust: 'Ajuster',
-        never: 'jamais',
-        lastSave: 'Dernière sauvegarde :',
-        new: 'Nouvel ePoc',
-    },
-    forms: {
-        type: 'Saisissez...',
-        badge: {
-            text: 'Paramètres du badge',
-            updateIcon: "Modifier l'icône",
-            obtention: "Conditions d'obtention du badge",
-            presentation: 'Présentation du badge',
-            presentationPlaceholder: 'Saisissez une présentation du badge',
-            icon: 'Icône du badge',
-        },
-        content: {
-            text: 'Contenu',
-            summary: 'Résumé',
-            video: {
-                label: 'Vidéo',
-                placeholder: 'Ajouter une vidéo',
-                hint: 'Format recommandé : {format}',
-            },
-            audio: {
-                label: 'Piste audio',
-                placeholder: 'Ajouter une piste audio',
-                hint: 'Format recommandé : MP3',
-            },
-            subtitle: {
-                label: 'Nom de la langue',
-                code: 'Code de la langue',
-                placeholder: 'Ajouter des sous-titre',
-                hint: 'Extensions acceptées : {extensions}',
-            },
-            transcription: {
-                label: 'Transcription',
-                placeholder: 'Ajouter une transcription',
-                hint: "Extensions acceptées : {extensions} <br>Pour les utilisateurs qui ne souhaitent pas ou ne sont pas en capacité d'écouter la vidéo",
-            },
-            thumbnail: {
-                label: 'Vignette',
-                placeholder: 'Ajouter une vignette',
-                hint: 'Format recommandé : idem à la vidéo',
-            },
-        },
-        buttons: {
-            addBadge: 'Ajouter un badge',
-            saveModel: 'Sauvegarder le modèle',
-            duplicateEvaluation: "Dupliquer l'évaluation",
-            duplicatePage: 'Dupliquer la page',
-            duplicateElement: "Dupliquer l'élement",
-            backToPage: 'Revenir à la page',
-            backToEpoc: "Revenir à l'ePoc",
-        },
-        node: {
-            conditionPlaceholder: 'Saisissez la condition {condition}...',
-            choice: 'Choix',
-            course: 'Cours {course}',
-            conditional: 'Contenus conditionnels',
-            title: 'Titre',
-            subtitle: 'Sous-titre',
-            duration: 'Durée (en minutes)',
-            objectives: 'Objectifs pédagogiques',
-            about: "À propos de l'ePoc",
-            cover: {
-                title: 'Image de couverture',
-                placeholder: 'Ajouter une image de couverture',
-                hint: 'Format recommandé : carré (180x180)<br> Image visible dans la liste des ePocs',
-            },
-            teaser: {
-                title: 'Teaser vidéo',
-                placeholder: 'Ajouter un teaser',
-                hint: "Format recommandé : 16:9<br> Vidéo visible dans la page de présentation de l'ePoc",
-            },
-            thumbnail: {
-                title: 'Vignette de la vidéo',
-                hint: "Format recommandé : idem que la vidéo <br> Image visible dans la page de présentation de l'ePoc",
-            },
-            presentation: 'Présentation',
-            edition: 'Édition',
-            author: {
-                title: 'Auteur | Auteurs',
-                placeholder: 'Jeanne Dupont',
-                image: {
-                    title: "Image de l'auteur",
-                    placeholder: 'Ajouter une image',
-                    hint: "Format recommandé : carré (100x100)<br> Image visible dans la page de présentation de l'ePoc",
-                },
-                position: {
-                    title: 'Fonction',
-                    placeholder: "Chercheur à l'Inria",
-                    hint: 'Profession, fonction, affiliation…',
-                },
-                biography: "Courte biographie de l'auteur",
-            },
-            certificateBadge: "Nombre de badge pour obtenir l'attestation",
-            certificateScore: "Score pour obtenir l'attestation",
-            certificateScoreHint:
-                "N'est pas pris en compte si le nombre de badge pour obtenir l'attestation est supérieur à 0",
-            chapterLabel: 'Label des chapitres',
-            chapterDuration: 'Durée des chapitres (en minutes)',
-            plugin: {
-                title: 'Plugin | Plugins',
-                script: 'Fichier de script',
-                scriptPlaceholder: 'Ajouter un fichier de script',
-                template: 'Template html du plugin',
-                templatePlaceholder: 'Ajouter un template html',
-            },
-            licence: {
-                title: 'Licence',
-                hint: 'Nom de la licence de votre contenu ePoc',
-                url: 'URL',
-                urlPlaceholder: 'https://creativecommons.org/licenses/by/4.0/deed',
-                urlHint: 'Text complet de la licence choisie',
-            },
-            page: {
-                title: 'Page',
-                hidden: 'Caché dans la table des matières',
-                conditional: "Ne s'affiche qu'à certaines conditions",
-                conditionalHint: "Option utilisée pour l'affichage conditionnel",
-                components: 'Composants',
-            },
-            activity: 'Évaluation',
-        },
-    },
-    questions: {
-        configuration: "Configuration de l'évaluation",
-        question: 'Question',
-        askQuestion: 'Posez la question',
-        instruction: 'Consigne',
-        instructionPlaceholder: 'Instruction pour répondre à la question',
-        explanation: 'Explication',
-        typeExplanation: 'Saisissez une explication',
-        addExplanation: 'Ajouter une explication',
-        response: 'Réponse',
-        responses: 'Réponses',
-        typeResponse: 'Saisissez une réponse',
-        correctResponse: 'Bonne réponse',
-        categories: 'Catégories de réponses proposées',
-        category: 'Catégorie',
-        typeCategory: 'Saisissez un intitulé catégorie',
-        proposedChoices: 'Catégories de choix proposées',
-        proposedResponses: 'Réponses proposées',
-        cards: 'Cartes',
-        card: 'Carte',
-        typeProposition: 'Saisissez une proposition',
-        template: {
-            title: 'Template',
-            select: 'Selectionnez un template',
-            data: 'Données',
-            key: 'Clé',
-            value: 'Valeur',
-        },
-        types: {
-            qcm: 'QCM',
-            dragDrop: 'Drag & Drop',
-            reorder: 'Reorder',
-            swipe: 'Swipe',
-            dropdownList: 'Liste déroulante',
-            custom: 'Question personnalisée',
-        },
-        score: 'Score',
-    },
-    sidebar: {
-        content: {
-            text: 'Texte',
-            video: 'Vidéo',
-            audio: 'Audio',
-            textTooltip: 'Glisser/déposer pour ajouter un texte',
-            videoTooltip: 'Glisser/déposer pour ajouter une vidéo',
-            audioTooltip: 'Glisser/déposer pour ajouter un audio',
-        },
-        pages: {
-            question: 'Question',
-            conditions: 'Conditions',
-            conditionsLegacy: 'Conditions (legacy)',
-            model: 'Modèle',
-            badge: 'Badge',
-            questionTooltip: 'Cliquer pour ajouter une question',
-            conditionsTooltip: 'Glisser/déposer pour ajouter une condition',
-            modelTooltip: 'Cliquer pour ouvrir le menu modèle',
-            badgeTooltip: 'Cliquer pour ouvrir le menu badge',
-        },
-    },
-    verbs: {
-        started: 'Commencé',
-        completed: 'Terminé',
-        viewed: 'Vu',
-        read: 'Lu',
-        played: 'Joué',
-        watched: 'Regardé',
-        listened: 'Écouté',
-        attempted: 'Tenté',
-        scored: 'Obtenu un score de',
-        passed: 'Réussi',
-    },
-};
diff --git a/src/main.ts b/src/main.ts
index c5f78095..d98ecb5b 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -6,7 +6,7 @@ import { createPinia } from 'pinia';
 import VueTippy from 'vue-tippy';
 import 'tippy.js/dist/tippy.css';
 import draggable from 'vuedraggable';
-import { i18n } from './i18n/config';
+import { i18n } from '@/i18n/config';
 
 const app = createApp(App);
 const pinia = createPinia();
diff --git a/src/shared/data/badge.data.ts b/src/shared/data/badge.data.ts
index efc04b6f..80c2640b 100644
--- a/src/shared/data/badge.data.ts
+++ b/src/shared/data/badge.data.ts
@@ -1,6 +1,6 @@
 import env from '@/src/shared/utils/env';
 import { ElementType, VerbKey, Verbs } from '@/src/shared/interfaces';
-import { i18n } from '@/src/i18n/config';
+import { i18n } from '@/i18n/config';
 import { computed, ComputedRef } from 'vue';
 
 export const iconsPath = env.isDev ? '/img/badge/icon' : 'img/badge/icon';
diff --git a/src/shared/data/forms/badgeForm.data.ts b/src/shared/data/forms/badgeForm.data.ts
index 11ef0802..0c3e55a9 100644
--- a/src/shared/data/forms/badgeForm.data.ts
+++ b/src/shared/data/forms/badgeForm.data.ts
@@ -1,7 +1,7 @@
 import { Form } from '@/src/shared/interfaces';
 import { badgeButtons } from './formButtons.data';
 import { computed, ComputedRef } from 'vue';
-import { i18n } from '@/src/i18n/config';
+import { i18n } from '@/i18n/config';
 
 export const customBadgeForm: ComputedRef<Form> = computed(() => {
     return {
diff --git a/src/shared/data/forms/contentForm.data.ts b/src/shared/data/forms/contentForm.data.ts
index ecc3b24c..0dd25100 100644
--- a/src/shared/data/forms/contentForm.data.ts
+++ b/src/shared/data/forms/contentForm.data.ts
@@ -1,7 +1,7 @@
 import { Form } from '@/src/shared/interfaces';
 import { contentButtons } from './formButtons.data';
 import { computed, ComputedRef } from 'vue';
-import { i18n } from '@/src/i18n/config';
+import { i18n } from '@/i18n/config';
 
 export const textForm: ComputedRef<Form> = computed(() => {
     return {
diff --git a/src/shared/data/forms/formButtons.data.ts b/src/shared/data/forms/formButtons.data.ts
index 65a7a12c..86b4819a 100644
--- a/src/shared/data/forms/formButtons.data.ts
+++ b/src/shared/data/forms/formButtons.data.ts
@@ -1,7 +1,7 @@
 import { FormButton } from '@/src/shared/interfaces';
 import env from '@/src/shared/utils/env';
 import { computed, ComputedRef } from 'vue';
-import { i18n } from '@/src/i18n/config';
+import { i18n } from '@/i18n/config';
 
 export const baseButtons: ComputedRef<FormButton[]> = computed(() => {
     return [
diff --git a/src/shared/data/forms/nodeForm.data.ts b/src/shared/data/forms/nodeForm.data.ts
index 8de1e773..66c8d0a9 100644
--- a/src/shared/data/forms/nodeForm.data.ts
+++ b/src/shared/data/forms/nodeForm.data.ts
@@ -1,7 +1,7 @@
 import { Form } from '@/src/shared/interfaces';
 import { activityButtons, baseButtons, pageButtons } from './formButtons.data';
 import { computed, ComputedRef } from 'vue';
-import { i18n } from '@/src/i18n/config';
+import { i18n } from '@/i18n/config';
 
 export const conditionForm: ComputedRef<Form> = computed(() => ({
     type: 'condition',
diff --git a/src/shared/data/forms/questionsForm.data.ts b/src/shared/data/forms/questionsForm.data.ts
index 0ecc3e08..88741e58 100644
--- a/src/shared/data/forms/questionsForm.data.ts
+++ b/src/shared/data/forms/questionsForm.data.ts
@@ -1,6 +1,6 @@
 import { Form } from '@/src/shared/interfaces';
 import { contentButtons } from './formButtons.data';
-import { i18n } from '@/src/i18n/config';
+import { i18n } from '@/i18n/config';
 import { capitalizeFirstLetter } from '../../utils/string';
 import { ComputedRef, computed } from 'vue';
 
diff --git a/src/shared/data/sideBar.data.ts b/src/shared/data/sideBar.data.ts
index 2f7cd879..e55c5457 100644
--- a/src/shared/data/sideBar.data.ts
+++ b/src/shared/data/sideBar.data.ts
@@ -1,5 +1,5 @@
 import { SideAction } from '@/src/shared/interfaces';
-import { i18n } from '@/src/i18n/config';
+import { i18n } from '@/i18n/config';
 import { computed, ComputedRef } from 'vue';
 
 export const questions: ComputedRef<SideAction[]> = computed(() => [
diff --git a/src/shared/services/graph/badge.service.ts b/src/shared/services/graph/badge.service.ts
index b3264469..0a37bfb0 100644
--- a/src/shared/services/graph/badge.service.ts
+++ b/src/shared/services/graph/badge.service.ts
@@ -6,7 +6,7 @@ import { saveState } from '@/src/shared/services/undoRedo.service';
 import { elementVerbs, verbs } from '@/src/shared/data';
 import { generateContentId, graphService } from '@/src/shared/services';
 import { Operators } from '@epoc/epoc-types/dist/v2';
-import { i18n } from '@/src/i18n/config';
+import { i18n } from '@/i18n/config';
 import { computed, ComputedRef } from 'vue';
 
 const { findNode } = useVueFlow('main');
diff --git a/src/shared/services/graph/chapter.service.ts b/src/shared/services/graph/chapter.service.ts
index e1a013b9..79bb2dc3 100644
--- a/src/shared/services/graph/chapter.service.ts
+++ b/src/shared/services/graph/chapter.service.ts
@@ -2,7 +2,7 @@ import { Node, useVueFlow } from '@vue-flow/core';
 import { Chapter } from '@epoc/epoc-types/src/v1';
 import { generateContentId, generateId, graphService } from '@/src/shared/services';
 const { nodes, findNode, addNodes } = useVueFlow('main');
-import { i18n } from '@/src/i18n/config';
+import { i18n } from '@/i18n/config';
 
 const { t } = i18n.global;
 
diff --git a/src/shared/stores/settingsStore.ts b/src/shared/stores/settingsStore.ts
index 8936e422..ef51377e 100644
--- a/src/shared/stores/settingsStore.ts
+++ b/src/shared/stores/settingsStore.ts
@@ -1,7 +1,7 @@
 import { defineStore } from 'pinia';
 import { editorService } from '@/src/shared/services';
 import { Settings } from '@/src/shared/interfaces';
-import { i18n } from '@/src/i18n/config';
+import { i18n } from '@/i18n/config';
 
 interface SettingsState {
     settings?: Settings;
-- 
GitLab