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