diff --git a/electron/components/contextMenu.js b/electron/components/contextMenu.js index 8dccce26311ad7b2464df2db7c4ea2adf3579c63..055612fb4086525f6c0b36bcb9554c72b983373a 100644 --- a/electron/components/contextMenu.js +++ b/electron/components/contextMenu.js @@ -1,4 +1,5 @@ const { BrowserWindow } = require('electron'); +const { t } = require('./utils'); const isDev = process.env.IS_DEV === 'true'; @@ -8,11 +9,11 @@ function getTemplateFromContext(callback, data) { }; const standardActions = [ { - label: 'Undo', + label: t('menu.edit.undo'), click: () => onClick('undo'), }, { - label: 'Redo', + label: t('menu.edit.redo'), click: () => onClick('redo'), }, ]; @@ -22,60 +23,60 @@ function getTemplateFromContext(callback, data) { if (data.context === 'flow') { menu.push( { - label: 'Ajouter', + label: t('context.add'), submenu: getPagesFromContext(onClick, { position: data.position }, 'addPage', data.context), }, { - label: 'Coller ici', + 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: 'Ajouter', + label: t('context.add'), submenu: getContentFromContext(onClick, { id: data.id }, data.context), }); } menu.push( { - label: 'Insérer après', + label: t('context.insertAfter'), submenu: getPagesFromContext(onClick, { id: data.id }, 'insertAfter', data.context), }, { - label: 'Insérer avant', + label: t('context.insertBefore'), submenu: getPagesFromContext(onClick, { id: data.id }, 'insertBefore', data.context), }, { - label: 'Dupliquer', + label: t('context.duplicate'), click: () => onClick('duplicatePage', { id: data.id }), }, { - label: 'Supprimer', + label: t('context.delete'), click: () => onClick('deleteNode', { id: data.id }), }, { type: 'separator', }, { - label: 'Copier', + label: t('menu.edit.copy'), click: () => onClick('copy', { id: data.id }), }, { - label: 'Intervertir avec le suivant', + label: t('context.swapNext'), click: () => onClick('swapNodeWithNext', { id: data.id }), }, { - label: 'Intervertir avec le précédent', + label: t('context.swapPrevious'), click: () => onClick('swapNodeWithPrevious', { id: data.id }), - }, + } ); } else if (data.context === 'content') { menu.push( { - label: 'Supprimer', + label: t('context.delete'), click: () => onClick('deleteContent', { pageId: data.pageId, id: data.id }), - }, + } // { // label: 'Dupliquer', // click: () => onClick('duplicateContent', { pageId: data.pageId, id: data.id }) @@ -84,45 +85,45 @@ function getTemplateFromContext(callback, data) { } else if (data.context === 'chapter') { menu.push( { - label: 'Insérer à la fin', + label: t('context.insertEnd'), submenu: getPagesFromContext(onClick, { id: data.id }, 'insertAtEnd', data.context), }, { - label: 'Insérer au début', + label: t('context.insertStart'), submenu: getPagesFromContext(onClick, { id: data.id }, 'insertAtStart', data.context), }, { - label: 'Intervertir avec le précédent', + label: t('context.swapPrevious'), click: () => onClick('swapChapterWithPrevious', { id: data.id }), }, { - label: 'Intervertir avec le suivant', + label: t('context.swapNext'), click: () => onClick('swapChapterWithNext', { id: data.id }), }, { - label: 'Supprimer', + label: t('context.delete'), click: () => onClick('deleteNode', { id: data.id }), }, { - label: 'Copier le chapitre', + label: t('context.copyChapter'), click: () => onClick('copyChapter', { id: data.id }), - }, + } ); } else if (data.context === 'epoc') { menu.push({ - label: 'Ajouter un nouveau chapitre', + label: t('context.addChapter'), click: () => onClick('addChapter'), }); } else if (data.context === 'selection') { menu.push( { - label: 'Supprimer', + label: t('context.delete'), click: () => onClick('deleteSelection', { selection: data.selection }), }, { - label: 'Copier', + label: 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: t('context.addText'), click: () => onClick(event, { type: 'text', ...data }), }, { - label: 'Ajouter une page Vidéo', + label: t('context.addVideo'), click: () => onClick(event, { type: 'video', ...data }), }, { - label: 'Ajouter une page Audio', + label: t('context.addAudio'), click: () => onClick(event, { type: 'audio', ...data }), }, ]; const questions = [ { - label: 'Ajouter une évaluation QCM', + label: t('context.addChoice'), click: () => onClick(event, { type: 'choice', ...data }), }, { - label: 'Ajouter une évaluation Drag & Drop', + label: t('context.addDragAndDrop'), click: () => onClick(event, { type: 'drag-and-drop', ...data }), }, { - label: 'Ajouter une évaluation Reorder', + label: t('context.addReorder'), click: () => onClick(event, { type: 'reorder', ...data }), }, { - label: 'Ajouter une évaluation Swipe', + label: t('context.addSwipe'), click: () => onClick(event, { type: 'swipe', ...data }), }, { - label: 'Ajouter une évaluation Liste Déroulante', + label: t('context.addDropdown'), click: () => onClick(event, { type: 'dropdown-list', ...data }), }, { - label: 'Ajouter une évaluation personnalisée', + label: 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: 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: t('context.addQuestion'), click: () => onClick('addContent', { type: 'choice', ...data }), }, { - label: 'Ajouter une question Drag & Drop', + label: t('context.addDragAndDrop'), click: () => onClick('addContent', { type: 'drag-and-drop', ...data }), }, { - label: 'Ajouter une question Reorder', + label: t('context.addReorder'), click: () => onClick('addContent', { type: 'reorder', ...data }), }, { - label: 'Ajouter une question Swipe', + label: t('context.addSwipe'), click: () => onClick('addContent', { type: 'swipe', ...data }), }, { - label: 'Ajouter une question Liste Déroulante', + label: t('context.addDropdownList'), click: () => onClick('addContent', { type: 'dropdown-list', ...data }), }, { - label: 'Ajouter une question personnalisée', + label: t('context.addCustom'), click: () => onClick('addContent', { type: 'custom', ...data }), }, ]; const contents = [ { - label: 'Ajouter un contenu Texte', + label: t('context.addText'), click: () => onClick('addContent', { type: 'text', ...data }), }, { - label: 'Ajouter un contenu Vidéo', + label: t('context.addVideo'), click: () => onClick('addContent', { type: 'video', ...data }), }, { - label: 'Ajouter un contenu Audio', + label: t('context.addAudio'), click: () => onClick('addContent', { type: 'audio', ...data }), }, ]; diff --git a/electron/components/ipc.js b/electron/components/ipc.js index 3518880914d6202ae7373437dde2625c77c8dd62..f72a91fc026b83f561cc2e8a28d4e1f56c3d6485 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 1c8d0ab9ac5bfe95a620195353de6fdf26562aaf..bc843f7f204f5b5aa4217336c149eb4fc600efe1 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 48dc1d050f3e0f7b533ea094d3b7e292b6bb011d..c5557c73e7e3dfb99f7f34ff5ab8ca6484673741 100644 --- a/electron/components/window.js +++ b/electron/components/window.js @@ -13,6 +13,7 @@ const { createPreview, createGlobalPreview, } = require('./file'); +const { t } = require('./utils'); const Store = require('electron-store'); const electronStore = new Store(); @@ -111,7 +112,7 @@ const setupWindow = function (window, filepath) { const createNewWindow = () => { const newWindow = createMainWindow(); - setupIpcListener(newWindow); + setupIpcListener(newWindow, setupMenu); setupWindow(newWindow); newWindow.show(); @@ -124,44 +125,45 @@ const createNewWindow = () => { const setupMenu = () => { const mainMenuTemplate = [ { - label: 'App', + label: t('menu.app.label'), submenu: [ - { label: 'À propos', role: 'about' }, + { label: t('menu.app.about'), role: 'about' }, { - label: 'Quitter', + label: t('menu.app.quit'), accelerator: 'CmdOrCtrl+Q', click: function () { app.quit(); }, }, + { type: 'separator' }, ], }, { - label: 'Fichier', + label: t('menu.file.label'), submenu: [ { - label: 'Nouveau', + label: t('menu.file.new'), accelerator: 'CmdOrCtrl+N', click: function () { sendToFrontend(BrowserWindow.getFocusedWindow(), 'epocProjectNew'); }, }, { - label: 'Nouvelle fenêtre', + label: t('menu.file.newWindow'), click: function () { ipcMain.emit('newWindow'); }, }, { type: 'separator' }, { - label: 'Ouvrir', + label: t('menu.file.open'), accelerator: 'CmdOrCtrl+O', click: function () { sendToFrontend(BrowserWindow.getFocusedWindow(), 'epocProjectPicked', pickEpocProject()); }, }, { - label: 'Ouvrir dans une nouvelle fenêtre', + label: t('menu.file.openInNewWindow'), click: () => { const newWindow = createNewWindow(); const project = pickEpocProject(); @@ -171,7 +173,7 @@ const setupMenu = () => { }, }, { - label: 'Projet récents', + label: t('menu.file.latest'), submenu: [ ...getRecentFiles().map((project) => { return { @@ -184,7 +186,7 @@ const setupMenu = () => { ], }, { - label: 'Importer un fichier .epoc', + label: t('menu.file.import'), click: async function () { sendToFrontend(BrowserWindow.getFocusedWindow(), 'epocImportPicked'); const project = await pickEpocToImport(); @@ -222,7 +224,7 @@ const setupMenu = () => { { type: 'separator' }, { id: 'save', - label: 'Sauvegarder', + label: t('menu.file.save'), accelerator: 'CmdOrCtrl+S', enabled: !!( store.state.projects[BrowserWindow.getFocusedWindow()?.id] && @@ -240,7 +242,7 @@ const setupMenu = () => { }, { id: 'saveAs', - label: 'Sauvegarder sous...', + label: t('menu.file.saveAs'), accelerator: 'Shift+CmdOrCtrl+S', enabled: !!( store.state.projects[BrowserWindow.getFocusedWindow()?.id] && @@ -261,40 +263,40 @@ const setupMenu = () => { ], }, { - label: 'Édition', + label: t('menu.edit.label'), submenu: [ { - label: 'Annuler', + label: t('menu.edit.undo'), accelerator: 'CmdOrCtrl+Z', click: function () { sendToFrontend(BrowserWindow.getFocusedWindow(), 'undo'); }, }, { - label: 'Rétablir', + label: 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: 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: 'Aperçu', + label: t('menu.preview.label'), submenu: [ { - label: "Lancer l'aperçu", + label: t('menu.preview.start'), click: function () { createPreview(store.state.projects[BrowserWindow.getFocusedWindow().id].workdir); }, }, { - label: "Lancer l'aperçu global", + label: t('menu.preview.global'), click: function () { createGlobalPreview(store.state.projects[BrowserWindow.getFocusedWindow().id].workdir); }, @@ -302,31 +304,33 @@ const setupMenu = () => { ], }, { - label: 'Aide', + label: t('menu.help.label'), submenu: [ { - label: 'Documentation', + label: t('menu.help.documentation'), click: async function () { await shell.openExternal('https://epoc.inria.fr/guide/user/getting-started/'); }, }, { - label: 'Signaler un problème', + label: t('menu.help.reportIssue'), click: async function () { const isDev = process.env.IS_DEV === 'true'; - const emailSubject = 'Aide éditeur'; + 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\nDécrivez votre problème ci-dessous:\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\nDécrivez votre problème ci-dessous:\n\n`, + `Version: ${appInfo.version}\nBuild: ${appInfo.buildNumber}\n ---\n\n${t( + 'menu.help.mailBody', + )}\n\n`, ); } @@ -337,14 +341,14 @@ const setupMenu = () => { }, { type: 'separator' }, { - label: 'Outils de développement', + label: t('menu.devTools'), accelerator: 'CmdOrCtrl+D', click: function () { BrowserWindow.getFocusedWindow().webContents.toggleDevTools(); }, }, { - label: 'Recharger', + label: t('menu.reload'), accelerator: 'CmdOrCtrl+R', click: function () { BrowserWindow.getFocusedWindow().webContents.reload(); @@ -372,4 +376,5 @@ module.exports = { createMainWindow, setupWindow, createNewWindow, + setupMenu, }; diff --git a/electron/electron.js b/electron/electron.js index 3b37581e908ac15fc8599b9cd0701eaa82dc9886..1bd8b675646ebc82ad3937df1b6330e7f65ff4df 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/i18n/config.ts b/i18n/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..ac4e33349e84000aae9f23a3c4ec839084de7528 --- /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 0000000000000000000000000000000000000000..bbbd0a79bd903e045e110699dde079c2b3e3f10d --- /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 0000000000000000000000000000000000000000..70a625ac68f7abcab5b4bec0839f14857d08139a --- /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/package-lock.json b/package-lock.json index 8d2dcd747b30d1632d3c0c3d79cf18231e44dfe5..8a8c1c5087ca8c7553ccfdb264d066298df88a4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,11 +22,14 @@ "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", "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" @@ -51,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", @@ -178,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" @@ -1298,6 +1300,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", @@ -8810,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", @@ -11329,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", @@ -13269,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", @@ -17278,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": { @@ -19167,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": { @@ -19177,7 +19292,7 @@ "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/ua-parser-js": { @@ -19659,6 +19774,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 079178727de73f6c6f6bab6e5fee0acf720c43eb..5377aeb50a74e9c425ab9649430e87a9a17d303d 100644 --- a/package.json +++ b/package.json @@ -39,11 +39,14 @@ "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", "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" @@ -68,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", diff --git a/src/App.vue b/src/App.vue index 7dcb73559e9abdefc829b58a6c17d6fae2612387..28c7eef7b80e4cc5787166be6332454a9ff5e283 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/components/ChoiceModal.vue b/src/components/ChoiceModal.vue index 0202008aa448f6ed7d706a0579e43b05baa96d1d..b5a4dcd92c81073ac045599a5ac5b64cac3c6e12 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 11a0db7e67722c5181942e9601a88bad7194ac3f..7495f96072ec06b1cc7873520d8cb501d79671e7 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/components/ui/UiSelect.vue b/src/components/ui/UiSelect.vue new file mode 100644 index 0000000000000000000000000000000000000000..4d110cd2d3a439df6c20c4cdcb0050ad26ec811e --- /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/badge/components/BadgePreview.vue b/src/features/badge/components/BadgePreview.vue index 401aad1fb0fac4e082df1b542cbef0dcb0f3c40b..09352c64eb183a39ae0978184ffa3f97d6f094e7 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 76108b235c98f3c44bbce20f17290e689bd76705..30ebfdc2915c7d388a8bf959c874e2d73b2d8bff 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 f9eca07a965d0be93bf0420c0ad2af6284f93115..e57b6b9b53c290f9242dc265a12e24dd7cf0dd51 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> @@ -63,7 +69,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 +87,7 @@ function handleVerbChange(value: string) { class="grid-item" /> <div class="select"> - Condition + {{ $t('global.condition') }} <select id="condition" :disabled="verbDisabled" @@ -89,8 +95,8 @@ function handleVerbChange(value: string) { :value="currentCondition.verb" @change="handleVerbChange(($event.target as HTMLSelectElement).value)" > - <option value="">Veuillez choisir</option> - <option v-for="(description, verb) in getVerbs(elementType)" :key="verb" :value="verb"> + <option value="">{{ $t('global.pleaseSelect') }}</option> + <option v-for="(description, verb) in verbs" :key="verb" :value="verb"> {{ description.label }} </option> </select> diff --git a/src/features/badge/components/ConditionModal.vue b/src/features/badge/components/ConditionModal.vue index 93842c20a54fa8107deef71f7b72edb6ace53dca..2b0ddfeff8b7a088e533dd68da897d5ad61c60a5 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 b9f8b79020e2af431e9d2ed1cdb7123902fea470..26bf3c7b1813f7528434bf3cb4f9504180ea7739 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 adfa2fa4eb2d4ed5b67d60ec8d54612fcf5f0771..0097030955efa1141e7961afbea0b29e8e1e9eca 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/features/ePocFlow/ePocFlow.vue b/src/features/ePocFlow/ePocFlow.vue index 81eb6a6ece7ecee61908feb3dfbc4f13e76d1c53..5d3a268cce1d96b8a8c5a87d0e36ba3c0cb85958 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/ePocFlow/nodes/ActivityNode.vue b/src/features/ePocFlow/nodes/ActivityNode.vue index 8e41271b7e382014d532ae9876f2b916cf9eeffe..eb29a66bc4854dd01385ce23b9b14998b8d9de8f 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 e3746d85e894c026e6cdf62e8838ec8e42d8caec..40db90e73904a1a0599bd5e89b235987ed4725d2 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/features/forms/FormPanel.vue b/src/features/forms/FormPanel.vue index 84b2a99b752df763fca662bc2b7a5b92ac1aa474..f962328fbb61d5a9e629267f393a59453fdeace4 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/forms/components/inputs/HtmlInput.vue b/src/features/forms/components/inputs/HtmlInput.vue index b999d8be701f33e78a595ace8a6f339c2d9670d0..d3491cf51fdb4896a8e039342d24bcdcc0100c7a 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 7d118557e65749cbe8e15b4259f6dc45293c007c..0d9b2d10ff54574b3582cade55403735bc2d64bb 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 89dca04c78fb032c72381e1344a2e2440acb926d..30962353c9707eb8825af7e2c582a64bae7a3d73 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 135fe49d8dc57ad9bb7dfb61f38076eadaf9a097..007fee3ba982f6ca2cdc4e466407bd6a42b3b1f1 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 626ca022c0018011dc31ad5f372a57e103c968cf..6b4ce097c3cb93739401676ed00dcb08fb9e2458 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 f7ae8a63e1808e7d2cbb586461d44da2dac55d65..14cef722abc91723cd41d9e673d8c90ee5f659e2 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(); @@ -22,29 +24,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 { @@ -52,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="">Sélectionnez</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 d391601dd993ef9f776afbc30f4b354123a8f453..259fd89d2157d1df1a83b65aff6ab2a46374cf67 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 528a33a805847a7d3315fc782c890394359b08ef..05f29b4b4e567fa50c94dcdd84826e6049a44c6e 100644 --- a/src/features/settings/SettingsModal.vue +++ b/src/features/settings/SettingsModal.vue @@ -1,20 +1,19 @@ <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 { ref, watch } from 'vue'; import { useSettingsStore } from '@/src/shared/stores'; +import { useI18n } from 'vue-i18n'; + +const { locale } = useI18n(); const modal = ref(null); const settingsStore = useSettingsStore(); const spellcheck = ref(false); - -onMounted(() => { - settingsStore.init(); -}) +const localeTmp = ref(); function save() { + locale.value = localeTmp.value; settingsStore.setSettings(spellcheck.value); modal.value.close(); } @@ -23,29 +22,44 @@ function open() { modal.value.open(); } -watch(() => modal.value?.isOpen, (isOpen) => { - if (isOpen) spellcheck.value = settingsStore?.settings?.spellcheck; -}) +watch( + () => modal.value?.isOpen, + (isOpen) => { + // set values here to make sure the settings were fetched from the store + if (isOpen) { + spellcheck.value = settingsStore?.settings?.spellcheck; + localeTmp.value = locale.value; + } + }, +); 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')" /> + <SettingsInput + v-model="localeTmp" + type="select" + :label="$t('settings.lang')" + :options="[ + { label: 'English', value: 'en' }, + { label: 'Français', value: 'fr' }, + ]" + /> </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.save') }}</button> </template> </Modal> </template> @@ -54,7 +68,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 eccdcaf9a02497b97564a52e090c7bf70a4c2a02..77113d9b2adca0b30fc3c06bb0ffcaac17104820 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 ff7b968be486494bf4c47fc9f2187197b899f468..4216d9b99722af5ae3af2ff6e7d8d9b2b7230835 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/sideBar/components/SideActions.vue b/src/features/sideBar/components/SideActions.vue index bb26f0a5e4e0a3269ced31535f92220839f5ddc5..ce3bcdb29262e7f507975a0e3454d81039f12ce9 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/features/topBar/HamburgerMenu.vue b/src/features/topBar/HamburgerMenu.vue index d4701bc920162bba9efda1cca535485d4fc26650..0a2f0e1aef53a07616b9836f8d00cf4c75278024 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 061b9e2a1027407bcc71df38f507554ba877c6df..075e6c279310cbc0e5db6b0ab2712688a46b99b5 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 0b699578c9d121d64ab0112da3e4ffe3f5cd9344..c9a69f7956f64d0edc29239781517289d1555810 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 0e45eb53167987e45aabd3073ab2761ae9b6a54c..a76829f41ee45c937787e06e51569a5cde09376b 100644 --- a/src/features/topBar/TopBar.vue +++ b/src/features/topBar/TopBar.vue @@ -4,20 +4,17 @@ 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(); 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 ? 'Ajuster' : `${Math.round(zoom.value * 100)}%`)); +const zoomString = computed(() => (zoom.value === 0 ? t('header.adjust') : `${Math.round(zoom.value * 100)}%`)); zoomTo(zoom.value); @@ -34,38 +31,49 @@ function updateZoom(val: number) { } } -function since(date: string) { - if (!date) return 'jamais'; +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); 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") - ); -} - -setInterval(() => { - savedSince.value = since(editorStore.currentProject.modified); -}, 60000); + 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' + ); + } +}); 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/main.ts b/src/main.ts index 99d548e14f881d03cf3b7f4005daa78e238d0216..d98ecb5b22a93d32f8dc457cb255103f9e6f74c0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -6,10 +6,13 @@ import { createPinia } from 'pinia'; import VueTippy from 'vue-tippy'; import 'tippy.js/dist/tippy.css'; 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); app.mount('#app'); diff --git a/src/router.ts b/src/router.ts index a06482a067c92d8c2df4242e95e220ab4050040a..c263ba9da2cca26f07cbfca80650ba23e2b30357 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/data/badge.data.ts b/src/shared/data/badge.data.ts index 678d8aa1e8caedffd1b1e97acf1c5228f90d2cf5..80c2640b398910579165e1d702703155b7689e48 100644 --- a/src/shared/data/badge.data.ts +++ b/src/shared/data/badge.data.ts @@ -1,21 +1,24 @@ import env from '@/src/shared/utils/env'; import { ElementType, VerbKey, Verbs } from '@/src/shared/interfaces'; +import { i18n } from '@/i18n/config'; +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: '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' }, -}; +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 ddc656765f5bc30b7bcbc000e4a89fa75167ddc4..334839878a0930199d2db698159f414007eb8ab3 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 cd2ff1590b6f376897ebdcba51a125d0ef06f592..0c3e55a9e53408f8ab8aea3a36ecd43e502715af 100644 --- a/src/shared/data/forms/badgeForm.data.ts +++ b/src/shared/data/forms/badgeForm.data.ts @@ -1,54 +1,58 @@ import { Form } from '@/src/shared/interfaces'; import { badgeButtons } from './formButtons.data'; +import { computed, ComputedRef } from 'vue'; +import { i18n } from '@/i18n/config'; -export const customBadgeForm: Form = { - type: 'badge', - name: 'Paramètres du badge', - icon: 'icon-badge', - buttons: badgeButtons, - fields: [ - { - inputs: [ - { - id: 'title', - type: 'text', - label: 'Titre', - value: '', - placeholder: 'Saisissez...', - }, - { - id: 'icon', - type: 'icon-picker', - label: 'Icône du badge', - value: '', - placeholder: "Modifier l'icône", - }, - ], - }, - { - name: "Conditions d'obtention du badge", - inputs: [ - { - id: 'conditions', - type: 'badge-conditions', - label: '', - value: [], - }, - ], - }, - { - name: 'Présentation du badge', - inputs: [ - { - id: 'description', - type: 'textarea', - label: '', - value: '', - placeholder: 'Saisissez une présentation du badge', - }, - ], - }, - ], -}; +export const customBadgeForm: ComputedRef<Form> = computed(() => { + return { + type: 'badge', + name: i18n.global.t('forms.badge.text'), + 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 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 20a0f2cdb2cbdf4a1ceee9a3a1a443aecc2fe6db..0dd25100acc53e33de0e16aa389bd20ad0556e02 100644 --- a/src/shared/data/forms/contentForm.data.ts +++ b/src/shared/data/forms/contentForm.data.ts @@ -1,153 +1,161 @@ import { Form } from '@/src/shared/interfaces'; import { contentButtons } from './formButtons.data'; +import { computed, ComputedRef } from 'vue'; +import { i18n } from '@/i18n/config'; -export const textForm: Form = { - type: 'text', - name: 'Contenu', - icon: 'icon-texte', - buttons: contentButtons, - fields: [ - { - inputs: [ - { - id: 'html', - type: 'html', - label: '', - value: '', - placeholder: 'Saisissez un résumé...', - }, - ], - }, - ], -}; +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 videoForm: Form = { - type: 'video', - name: 'Vidéo', - icon: 'icon-video', - buttons: contentButtons, - fields: [ - { - inputs: [ - { - id: 'source', - type: 'file', - label: 'Vidéo', - placeholder: 'Ajouter une vidéo', - value: '', - accept: '.mp4', - hint: 'Format recommandé: 16:9 (720x480)' - }, - { - id: 'summary', - type: 'html', - label: 'Résumé', - value: '', - placeholder: 'Saisissez...', - }, - { - id: 'transcript', - type: 'file', - label: 'Transcription', - 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 vidéo' - }, - { - id: 'poster', - type: 'file', - label: 'Vignette', - value: '', - placeholder: 'Ajouter une vignette', - accept: '.png,.jpg,.jpeg,.gif,.bmp,.svg,.webp', - hint: 'Format recommandé: idem à la vidéo' - }, - ], - }, - { - name: 'Sous-titres', - inputs: [ - { - id: 'subtitles', - label: 'Sous-titres', - type: 'repeat', - value: [], - inputs: [ - { - id: 'label', - type: 'text', - label: 'Nom de la langue', - value: '', - placeholder: 'English', - }, - { - id: 'lang', - type: 'text', - label: 'Code de langue', - value: '', - placeholder: 'en', - }, - { - id: 'src', - type: 'file', - label: 'Fichier', - value: '', - placeholder: 'Ajouter des sous-titres', - accept: '.vtt', - hint: 'Extensions acceptées : .vtt' - }, - ], - }, - ], - }, - ], -}; +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', { format: '16:9 (720x480)' }), + }, + { + 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 audioForm: Form = { - type: 'audio', - name: 'Audio', - icon: 'icon-audio', - buttons: contentButtons, - fields: [ - { - inputs: [ - { - id: 'source', - type: 'file', - label: 'Piste audio', - placeholder: 'Ajouter une piste audio', - value: '', - accept: '.mp3', - }, - { - id: 'summary', - type: 'html', - label: 'Résumé', - value: '', - placeholder: 'Saisissez...', - }, - { - id: 'transcript', - type: 'file', - label: 'Transcription', - 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' - }, - { - id: 'subtitles', - type: 'file', - label: 'Sous-titres', - value: '', - placeholder: 'Ajouter des sous-titres', - accept: '.vtt', - hint: 'Extensions acceptées: .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 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 2787e704fd4dbf5a61c918fee4a9348dedb7bfcd..86b4819aedbeec26f1cae1b9f96b653dc0c93a51 100644 --- a/src/shared/data/forms/formButtons.data.ts +++ b/src/shared/data/forms/formButtons.data.ts @@ -1,39 +1,68 @@ import { FormButton } from '@/src/shared/interfaces'; import env from '@/src/shared/utils/env'; +import { computed, ComputedRef } from 'vue'; +import { i18n } from '@/i18n/config'; -export const baseButtons = [ - { label: 'Supprimer', icon: 'icon-supprimer', action: 'delete' }, - { label: 'Ajouter un badge', icon: 'icon-plus', action: 'add-badge' }, -]; +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 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' }, - ] - : [...baseButtons, { label: 'Dupliquer la page', icon: 'icon-plus', action: 'duplicate-page' }]; +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 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' }, - ] - : [...baseButtons, { label: "Dupliquer l'évaluation", icon: 'icon-plus', action: 'duplicate-page' }]; + return [ + ...baseButtons.value, + { label: i18n.global.t('forms.buttons.duplicatePage'), 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' }, - ] - : [...baseButtons]; +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 badgeButtons: FormButton[] = [ - { label: 'Supprimer', icon: 'icon-supprimer', action: 'delete-badge' }, - { label: "Revenir à l'ePoc", icon: 'icon-epoc', action: 'back-to-epoc' }, -]; +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: 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 b4e4d4bad8f78a9a5e1d4052ddfbc7ddcf70247b..66c8d0a9985b4c57c20b682481a50d8ce78817fa 100644 --- a/src/shared/data/forms/nodeForm.data.ts +++ b/src/shared/data/forms/nodeForm.data.ts @@ -1,11 +1,13 @@ import { Form } from '@/src/shared/interfaces'; import { activityButtons, baseButtons, pageButtons } from './formButtons.data'; +import { computed, ComputedRef } from 'vue'; +import { i18n } from '@/i18n/config'; -export const conditionForm: Form = { +export const conditionForm: ComputedRef<Form> = computed(() => ({ type: 'condition', - name: 'Conditions', + name: i18n.global.t('global.conditions'), icon: 'icon-condition', - buttons: baseButtons, + buttons: baseButtons.value, fields: [ { inputs: [ @@ -14,34 +16,34 @@ export const conditionForm: Form = { type: 'text', label: '', value: '', - placeholder: 'Saisissez la condition 1...', + placeholder: i18n.global.t('forms.node.conditionPlaceholder', { condition: '1' }), }, { id: 'condition2', type: 'text', label: '', value: '', - placeholder: 'Saisissez la 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: 'Label', + label: i18n.global.t('global.label'), value: '', - placeholder: 'Saisissez...', + placeholder: i18n.global.t('forms.type'), }, ], }, @@ -50,15 +52,18 @@ export const legacyConditionForm: Form = { inputs: [ { id: 'choices', - label: 'Choix', + label: i18n.global.t('forms.node.choice'), type: 'repeat', - value: ['Parcours A', 'Parcours B'], + value: [ + i18n.global.t('forms.node.course', { course: 'A' }), + i18n.global.t('forms.node.course', { course: 'B' }), + ], inputs: [ { id: '', type: 'text', label: '', - placeholder: 'Parcours X', + placeholder: i18n.global.t('forms.node.course', { course: 'X' }), value: '', }, ], @@ -66,11 +71,11 @@ export const legacyConditionForm: Form = { ], }, { - name: 'Contenus conditionnels', + name: i18n.global.t('forms.node.conditional'), inputs: [ { id: 'conditionalFlag', - label: 'Contenu', + label: i18n.global.t('forms.content.text'), type: 'repeat', value: [], inputs: [ @@ -78,7 +83,7 @@ export const legacyConditionForm: Form = { id: 'id', type: 'text', label: '', - placeholder: 'Contenu', + placeholder: i18n.global.t('forms.type'), value: '', }, { @@ -95,37 +100,37 @@ export const legacyConditionForm: Form = { ], }, ], -}; +})); -export const chapterForm: Form = { +export const chapterForm: ComputedRef<Form> = computed(() => ({ type: 'chapter', - name: 'Chapitre', + name: i18n.global.t('global.chapter'), icon: 'icon-chapitre', - buttons: baseButtons, + buttons: baseButtons.value, fields: [ { inputs: [ { id: 'title', type: 'text', - label: 'Titre', + label: i18n.global.t('forms.node.title'), value: '', - placeholder: 'Saisissez...', + placeholder: i18n.global.t('forms.type'), }, { id: 'duration', type: 'score', - label: 'Durée (en minutes)', + label: i18n.global.t('forms.node.duration'), value: 0, }, ], }, { - name: 'Objectifs pédagogiques', + name: i18n.global.t('forms.node.objectives'), inputs: [ { id: 'objectives', - label: 'Objectif', + label: i18n.global.t('global.objective'), type: 'repeat', value: [], inputs: [ @@ -133,7 +138,7 @@ export const chapterForm: Form = { id: '', type: 'textarea', label: '', - placeholder: 'Saisissez un objectif ...', + placeholder: i18n.global.t('forms.type'), value: '', }, ], @@ -141,11 +146,11 @@ export const chapterForm: Form = { ], }, ], -}; +})); -export const epocForm: Form = { +export const epocForm: ComputedRef<Form> = computed(() => ({ type: 'epoc', - name: "A propos de l'ePoc", + name: i18n.global.t('forms.node.about'), icon: 'icon-epoc', buttons: [], fields: [ @@ -154,90 +159,90 @@ export const epocForm: Form = { { id: 'title', type: 'text', - label: 'Titre', + label: i18n.global.t('forms.node.title'), value: '', - placeholder: 'Saisissez...', + placeholder: i18n.global.t('forms.type'), }, { id: 'image', type: 'file', - label: 'Image de couverture', - placeholder: 'Ajouter une image de couverture', + 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: 'Format recommandé : carré (180x180)<br> Image visible dans la liste des ePocs', + hint: i18n.global.t('forms.node.cover.hint'), }, { id: 'teaser', type: 'file', - label: 'Teaser vidéo', + label: i18n.global.t('forms.node.teaser.title'), value: '', - placeholder: 'Ajouter un teaser', + placeholder: i18n.global.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: i18n.global.t('forms.node.teaser.hint'), }, { id: 'thumbnail', type: 'file', - label: 'Vignette de la vidéo', + label: i18n.global.t('forms.node.thumbnail.title'), value: '', - placeholder: 'Ajouter une vignette', + placeholder: i18n.global.t('forms.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: i18n.global.t('forms.node.thumbnail.hint'), }, { id: 'summary', type: 'html-text', - label: 'Présentation', + label: i18n.global.t('forms.node.presentation'), value: '', - placeholder: "Saisissez une présentation de l'ePoc...", + placeholder: i18n.global.t('forms.type'), }, { id: 'edition', type: 'text', - label: 'Edition', + label: i18n.global.t('forms.node.edition'), value: String(new Date().getFullYear()), }, ], }, { - name: 'Auteurs', + name: i18n.global.t('forms.node.author.title', 2), inputs: [ { id: 'authors', - label: 'Auteur', + label: i18n.global.t('forms.node.author.title', 1), type: 'repeat', value: [], inputs: [ { id: 'name', type: 'text', - label: 'Nom', - placeholder: 'Jeanne Dupont', + label: i18n.global.t('global.name'), + placeholder: i18n.global.t('forms.node.author.placeholder'), value: '', }, { id: 'image', type: 'file', - label: 'Image', - placeholder: 'Ajouter une image', + 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: "Format recommandé : carré (100x100)<br> Image visible dans la page de présentation de l'ePoc", + hint: i18n.global.t('forms.node.author.image.hint'), }, { id: 'title', type: 'text', - label: 'Fonction', - placeholder: "Chercheuse à l'Inria", + label: i18n.global.t('forms.node.author.position.title'), + placeholder: i18n.global.t('forms.node.author.position.placeholder'), value: '', - hint: 'Profession, fonction, affiliation…', + hint: i18n.global.t('forms.node.author.position.hint'), }, { id: 'description', type: 'html-text', - label: 'Courte biographie', - placeholder: 'Saisissez une courte biographie...', + label: i18n.global.t('forms.node.author.biography'), + placeholder: i18n.global.t('forms.type'), value: '', }, ], @@ -245,11 +250,11 @@ export const epocForm: Form = { ], }, { - name: 'Objectifs pédagogiques', + name: i18n.global.t('forms.node.objectives'), inputs: [ { id: 'objectives', - label: 'Objectif', + label: i18n.global.t('global.objective'), type: 'repeat', value: [], inputs: [ @@ -257,7 +262,7 @@ export const epocForm: Form = { id: '', type: 'textarea', label: '', - placeholder: 'Saisissez un objectif ...', + placeholder: i18n.global.t('forms.type'), value: '', }, ], @@ -265,50 +270,50 @@ export const epocForm: Form = { ], }, { - name: 'Paramètres :', + name: i18n.global.t('settings.title'), inputs: [ { id: 'certificateBadgeCount', type: 'score', - label: "Nombre de badge pour obtenir l'attestation", + label: i18n.global.t('forms.node.certificateBadge'), value: 1, }, { id: 'certificateScore', type: 'score', - label: "Score pour obtenir l'attestation", + label: i18n.global.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: i18n.global.t('forms.node.certificateScoreHint'), }, { id: 'chapterParameter', type: 'text', - label: 'Label des chapitres', + label: i18n.global.t('forms.node.chapterLabel'), value: '', - placeholder: 'Saisissez...', + placeholder: i18n.global.t('forms.type'), }, { id: 'chapterDuration', type: 'score', - label: 'Durée des chapitres (en minutes)', + label: i18n.global.t('forms.node.chapterDuration'), value: 0, }, ], }, { - name: 'Plugins', + name: i18n.global.t('forms.node.plugin.title', 2), inputs: [ { id: 'plugins', - label: 'Plugin', + label: i18n.global.t('forms.node.plugin.title', 1), type: 'repeat', value: [], inputs: [ { id: 'script', type: 'file', - label: 'Fichier de script', - placeholder: 'Ajouter un script', + label: i18n.global.t('forms.node.plugin.script'), + placeholder: i18n.global.t('forms.node.plugin.scriptPlaceholder'), targetDirectory: 'plugins', value: '', accept: '.js', @@ -316,8 +321,8 @@ export const epocForm: Form = { { id: 'template', type: 'file', - label: 'Template html du plugin', - placeholder: 'Ajouter un template', + label: i18n.global.t('forms.node.plugin.template'), + placeholder: i18n.global.t('forms.node.plugin.templatePlaceholder'), targetDirectory: 'plugins', value: '', accept: 'html', @@ -327,72 +332,72 @@ export const epocForm: Form = { ], }, { - name: 'Licence', + name: i18n.global.t('forms.node.licence.title'), inputs: [ { id: 'licenceName', type: 'text', - label: 'Nom', + label: i18n.global.t('global.name'), placeholder: 'CC-BY 4.0', value: '', - hint: 'Nom de la licence de votre contenu ePoc', + hint: i18n.global.t('forms.node.licence.hint'), }, { id: 'licenceUrl', type: 'text', - label: 'URL', - placeholder: 'https://creativecommons.org/licenses/by/4.0/deed', + label: i18n.global.t('forms.node.licence.url'), + placeholder: i18n.global.t('forms.node.licence.urlPlaceholder'), value: '', - hint: 'Texte complet de la licence choisie', + hint: i18n.global.t('forms.node.licence.urlHint'), }, ], }, ], -}; +})); -export const pageForm: Form = { +export const pageForm: ComputedRef<Form> = computed(() => ({ type: 'page', - name: 'Page', + name: i18n.global.t('forms.node.page.title'), icon: 'icon-ecran', - buttons: pageButtons, + buttons: pageButtons.value, fields: [ { inputs: [ { id: 'title', type: 'text', - label: 'Titre', + label: i18n.global.t('forms.node.title'), value: '', - placeholder: 'Saisissez...', + placeholder: i18n.global.t('forms.type'), }, { id: 'subtitle', type: 'text', - label: 'Sous-titre', + label: i18n.global.t('forms.node.subtitle'), value: '', - placeholder: 'Saisissez...', + placeholder: i18n.global.t('forms.type'), }, { id: 'hidden', type: 'checkbox', - label: 'Caché dans la table des matières', + label: i18n.global.t('forms.node.page.hidden'), value: false, }, { id: 'conditional', type: 'checkbox', - label: "Ne s'affiche qu'a certaines conditions", + label: i18n.global.t('forms.node.page.conditional'), value: false, - hint: "Option utilisé pour l'affichage conditionnel", + hint: i18n.global.t('forms.node.page.conditionalHint'), }, ], }, { - name: 'Composants', + name: i18n.global.t('forms.node.page.components'), inputs: [ { id: 'components', - label: 'Composants', + label: i18n.global.t('forms.node.page.components'), type: 'repeat', value: [], addButton: false, @@ -401,58 +406,58 @@ export const pageForm: Form = { ], }, ], -}; +})); -export const activityForm: Form = { +export const activityForm: ComputedRef<Form> = computed(() => ({ type: 'activity', - name: 'Évaluation', + name: i18n.global.t('forms.node.activity'), icon: 'icon-ecran', - buttons: activityButtons, + buttons: activityButtons.value, fields: [ { inputs: [ { id: 'title', type: 'text', - label: 'Titre', + label: i18n.global.t('forms.node.title'), value: '', - placeholder: 'Saisissez...', + placeholder: i18n.global.t('forms.type'), }, { id: 'subtitle', type: 'text', - label: 'Sous-titre', + label: i18n.global.t('forms.node.subtitle'), value: '', - placeholder: 'Saisissez...', + placeholder: i18n.global.t('forms.type'), }, { id: 'summary', type: 'textarea', - label: 'Résumé', + label: i18n.global.t('forms.content.summary'), value: '', - placeholder: 'Saisissez...', + placeholder: i18n.global.t('forms.type'), }, { id: 'hidden', type: 'checkbox', - label: 'Caché dans la table des matières', + label: i18n.global.t('forms.node.page.hidden'), value: false, }, { id: 'conditional', type: 'checkbox', - label: "Ne s'affiche qu'a certaines conditions", + label: i18n.global.t('forms.node.page.conditional'), value: false, - hint: "Option utilisé pour l'affichage conditionnel", + hint: i18n.global.t('forms.node.page.conditionalHint'), }, ], }, { - name: 'Composants', + name: i18n.global.t('forms.node.page.components'), inputs: [ { id: 'components', - label: 'Composants', + label: i18n.global.t('forms.node.page.components'), type: 'repeat', value: [], addButton: false, @@ -461,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 dbf9d3bce036cd912fb27b55407a593b3effdb5b..88741e58aba03d6bf07cad4b9905493d3ec31dc7 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 '@/i18n/config'; +import { capitalizeFirstLetter } from '../../utils/string'; +import { ComputedRef, computed } from 'vue'; -export const qcmForm: Form = { +export const qcmForm: ComputedRef<Form> = computed(() => ({ type: 'choice', - name: 'QCM', + name: i18n.global.t('questions.types.qcm'), icon: 'icon-qcm', displayFieldIndex: true, - buttons: contentButtons, + buttons: contentButtons.value, fields: [ { - name: "Configuration de l'évaluation", + name: i18n.global.t('questions.configuration'), inputs: [ { id: 'score', type: 'score', - label: 'Score', + label: i18n.global.t('questions.score'), value: 0, }, ], }, { - name: 'Question', + name: i18n.global.t('questions.question'), inputs: [ { id: 'label', type: 'textarea', - label: 'Question', + label: i18n.global.t('questions.question'), value: '', - placeholder: 'Posez la question', + placeholder: i18n.global.t('questions.askQuestion'), }, { id: 'statement', type: 'html-inline', - label: 'Consigne', + label: i18n.global.t('questions.instruction'), value: '', - placeholder: 'Instruction pour répondre à la question', + placeholder: i18n.global.t('questions.instructionPlaceholder'), }, ], }, { - name: 'Réponses', + name: i18n.global.t('questions.responses'), inputs: [ { id: 'responses', - label: 'Réponse', + label: i18n.global.t('questions.response'), type: 'repeat', value: [], inputs: [ { id: 'label', type: 'text', - label: 'Réponse', - placeholder: 'Saisissez une réponse...', + label: i18n.global.t('questions.response'), + placeholder: i18n.global.t('questions.typeResponse'), value: '', }, { id: 'value', type: 'hidden', label: '', - placeholder: 'Valeur cachée', + placeholder: i18n.global.t('forms.type'), value: '', }, { id: 'feedback', type: 'textarea', - label: 'Explication', - placeholder: 'Saisissez une explication...', + label: i18n.global.t('questions.explanation'), + placeholder: i18n.global.t('questions.typeExplanation'), value: '', collapsible: true, - collapsibleLabel: 'Ajouter une explication', + collapsibleLabel: i18n.global.t('questions.addExplanation'), }, { id: 'isCorrect', type: 'checkbox', - label: 'Bonne réponse', + label: i18n.global.t('questions.correctResponse'), value: false, }, ], @@ -81,63 +84,63 @@ export const qcmForm: Form = { ], }, { - name: 'Explication', + name: i18n.global.t('questions.explanation'), inputs: [ { id: 'explanation', type: 'html', label: '', value: '', - placeholder: 'Saisissez une explication', + placeholder: i18n.global.t('questions.typeExplanation'), }, ], }, ], -}; +})); -export const dragDropForm: Form = { +export const dragDropForm: ComputedRef<Form> = computed(() => ({ type: 'drag-and-drop', - name: 'Drag & Drop', + name: i18n.global.t('questions.types.dragDrop'), icon: 'icon-dragdrop', displayFieldIndex: true, - buttons: contentButtons, + buttons: contentButtons.value, fields: [ { - name: "Configuration de l'évaluation", + name: i18n.global.t('questions.configuration'), inputs: [ { id: 'score', type: 'score', - label: 'Score', + label: i18n.global.t('questions.score'), value: 0, }, ], }, { - name: 'Question', + name: i18n.global.t('questions.question'), inputs: [ { id: 'label', type: 'textarea', - label: 'Question', + label: i18n.global.t('questions.question'), value: '', - placeholder: 'Posez la question', + placeholder: i18n.global.t('questions.askQuestion'), }, { id: 'statement', type: 'html-inline', - label: 'Consigne', + label: i18n.global.t('questions.instruction'), value: '', - placeholder: 'Instruction pour répondre à la question', + placeholder: i18n.global.t('questions.instructionPlaceholder'), }, ], }, { - name: 'Catégories de réponses proposées', + name: i18n.global.t('questions.categories'), inputs: [ { id: 'categories', - label: 'Catégorie', + label: i18n.global.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: i18n.global.t('questions.typeCategory'), value: '', }, ], @@ -153,36 +156,36 @@ export const dragDropForm: Form = { ], }, { - name: 'Réponses proposées', + name: i18n.global.t('questions.proposedResponses'), inputs: [ { id: 'responses', - label: 'Réponse', + label: i18n.global.t('questions.response'), type: 'repeat', value: [], inputs: [ { id: 'label', type: 'text', - label: 'Réponse', - placeholder: 'Saisissez une réponse...', + label: i18n.global.t('questions.response'), + placeholder: i18n.global.t('questions.typeResponse'), value: '', }, { id: 'value', type: 'hidden', label: '', - placeholder: 'Valeur cachée', + placeholder: i18n.global.t('forms.type'), value: '', }, { id: 'feedback', type: 'textarea', - label: 'Explication', - placeholder: 'Saisissez une explication...', + label: i18n.global.t('questions.explanation'), + placeholder: i18n.global.t('questions.typeExplanation'), value: '', collapsible: true, - collapsibleLabel: 'Ajouter une explication', + collapsibleLabel: i18n.global.t('questions.addExplanation'), }, { id: 'category', @@ -198,87 +201,87 @@ export const dragDropForm: Form = { ], }, { - name: 'Explication', + name: i18n.global.t('questions.explanation'), inputs: [ { id: 'explanation', type: 'html', label: '', value: '', - placeholder: 'Saisissez une explication', + placeholder: i18n.global.t('questions.typeExplanation'), }, ], }, ], -}; +})); -export const reorderForm: Form = { +export const reorderForm: ComputedRef<Form> = computed(() => ({ type: 'reorder', - name: 'Reorder', + name: i18n.global.t('questions.types.reorder'), icon: 'icon-reorder', displayFieldIndex: true, - buttons: contentButtons, + buttons: contentButtons.value, fields: [ { - name: "Configuration de l'évaluation", + name: i18n.global.t('questions.configuration'), inputs: [ { id: 'score', type: 'score', - label: 'Score', + label: i18n.global.t('questions.score'), value: 0, }, ], }, { - name: 'Question', + name: i18n.global.t('questions.question'), inputs: [ { id: 'label', type: 'textarea', - label: 'Question', + label: i18n.global.t('questions.question'), value: '', - placeholder: 'Posez la question', + placeholder: i18n.global.t('questions.askQuestion'), }, { id: 'statement', type: 'html-inline', - label: 'Consigne', + label: i18n.global.t('questions.instruction'), value: '', - placeholder: 'Instruction pour répondre à la question', + placeholder: i18n.global.t('questions.instructionPlaceholder'), }, ], }, { - name: 'Réponses', + name: i18n.global.t('questions.responses'), inputs: [ { id: 'responses', - label: 'Réponse', + label: i18n.global.t('questions.response'), type: 'repeat', value: [], inputs: [ { id: 'label', type: 'text', - label: 'Réponse', - placeholder: 'Saisissez une réponse...', + label: i18n.global.t('questions.response'), + placeholder: i18n.global.t('questions.typeResponse'), value: '', }, { id: 'feedback', type: 'textarea', - label: 'Explication', - placeholder: 'Saisissez une explication...', + label: i18n.global.t('questions.explanation'), + placeholder: i18n.global.t('questions.typeExplanation'), value: '', collapsible: true, - collapsibleLabel: 'Ajouter une explication', + collapsibleLabel: i18n.global.t('questions.addExplanation'), }, { id: 'value', type: 'hidden', label: '', - placeholder: 'Valeur cachée', + placeholder: i18n.global.t('forms.type'), value: '', }, ], @@ -286,72 +289,75 @@ export const reorderForm: Form = { ], }, { - name: 'Explication', + name: i18n.global.t('questions.explanation'), inputs: [ { id: 'explanation', type: 'html', label: '', value: '', - placeholder: 'Saisissez une explication...', + placeholder: i18n.global.t('questions.typeExplanation'), }, ], }, ], -}; +})); -export const swipeForm: Form = { +export const swipeForm: ComputedRef<Form> = computed(() => ({ type: 'swipe', - name: 'Swipe', + name: i18n.global.t('questions.types.swipe'), icon: 'icon-swipe', displayFieldIndex: true, - buttons: contentButtons, + buttons: contentButtons.value, fields: [ { - name: "Configuration de l'évaluation", + name: i18n.global.t('questions.configuration'), inputs: [ { id: 'score', type: 'score', - label: 'Score', + label: i18n.global.t('questions.score'), value: 0, }, ], }, { - name: 'Question', + name: i18n.global.t('questions.question'), inputs: [ { id: 'label', type: 'textarea', - label: 'Question', + label: i18n.global.t('questions.question'), value: '', - placeholder: 'Posez la question', + placeholder: i18n.global.t('questions.askQuestion'), }, { id: 'statement', type: 'html-inline', - label: 'Consigne', + label: i18n.global.t('questions.instruction'), value: '', - placeholder: 'Instruction pour répondre à la question', + placeholder: i18n.global.t('questions.instructionPlaceholder'), }, ], }, { - name: 'Catégories de choix proposées', + name: i18n.global.t('questions.proposedChoices'), inputs: [ { id: 'categories', - label: 'Choix', + label: i18n.global.t('forms.node.choice'), type: 'repeat', - value: ['Droite', 'Gauche'], + value: [ + capitalizeFirstLetter(i18n.global.t('global.right')), + capitalizeFirstLetter(i18n.global.t('global.left')), + ], addButton: false, inputs: [ { id: '', type: 'text', label: '', - placeholder: 'Saisissez une réponse...', + placeholder: i18n.global.t('questions.typeResponse'), value: '', }, ], @@ -359,35 +365,35 @@ export const swipeForm: Form = { ], }, { - name: 'Réponse proposée', + name: i18n.global.t('questions.proposedResponses'), inputs: [ { id: 'responses', - label: 'Carte', + label: i18n.global.t('questions.card'), type: 'repeat', value: [], inputs: [ { id: 'label', type: 'text', - label: 'Réponse', - placeholder: 'Saisissez une proposition', + label: i18n.global.t('questions.response'), + placeholder: i18n.global.t('questions.typeProposition'), value: '', }, { id: 'feedback', type: 'textarea', - label: 'Explication', - placeholder: 'Saisissez une explication...', + label: i18n.global.t('questions.explanation'), + placeholder: i18n.global.t('questions.typeExplanation'), value: '', collapsible: true, - collapsibleLabel: 'Ajouter une explication', + collapsibleLabel: i18n.global.t('questions.addExplanation'), }, { id: 'value', type: 'hidden', label: '', - placeholder: 'Valeur cachée', + placeholder: i18n.global.t('forms.type'), value: '', }, { @@ -404,63 +410,63 @@ export const swipeForm: Form = { ], }, { - name: 'Explication', + name: i18n.global.t('questions.explanation'), inputs: [ { id: 'explanation', type: 'html', label: '', value: '', - placeholder: 'Saisissez une explication...', + placeholder: i18n.global.t('questions.typeExplanation'), }, ], }, ], -}; +})); -export const listForm: Form = { +export const listForm: ComputedRef<Form> = computed(() => ({ type: 'dropdown-list', - name: 'Liste déroulante', + name: i18n.global.t('questions.types.dropdownList'), icon: 'icon-liste', displayFieldIndex: true, - buttons: contentButtons, + buttons: contentButtons.value, fields: [ { - name: "Configuration de l'évaluation", + name: i18n.global.t('questions.configuration'), inputs: [ { id: 'score', type: 'score', - label: 'Score', + label: i18n.global.t('questions.score'), value: 0, }, ], }, { - name: 'Question', + name: i18n.global.t('questions.question'), inputs: [ { id: 'label', type: 'textarea', - label: 'Question', + label: i18n.global.t('questions.question'), value: '', - placeholder: 'Posez la question', + placeholder: i18n.global.t('questions.askQuestion'), }, { id: 'statement', type: 'html-inline', - label: 'Consigne', + label: i18n.global.t('questions.instruction'), value: '', - placeholder: 'Instruction pour répondre à la question', + placeholder: i18n.global.t('questions.instructionPlaceholder'), }, ], }, { - name: 'Catégories de choix proposées', + name: i18n.global.t('questions.proposedChoices'), inputs: [ { id: 'categories', - label: 'Choix', + label: i18n.global.t('forms.node.choice'), type: 'repeat', value: [], inputs: [ @@ -468,7 +474,7 @@ export const listForm: Form = { id: '', type: 'text', label: '', - placeholder: 'Saisissez une réponse...', + placeholder: i18n.global.t('questions.typeResponse'), value: '', }, ], @@ -476,35 +482,35 @@ export const listForm: Form = { ], }, { - name: 'Cartes', + name: i18n.global.t('questions.cards'), inputs: [ { id: 'responses', - label: 'Carte', + label: i18n.global.t('questions.card'), type: 'repeat', value: [], inputs: [ { id: 'label', type: 'text', - label: 'Réponse', - placeholder: 'Saisissez une question...', + label: i18n.global.t('questions.response'), + placeholder: i18n.global.t('questions.typeProposition'), value: '', }, { id: 'feedback', type: 'textarea', - label: 'Explication', - placeholder: 'Saisissez une explication...', + label: i18n.global.t('questions.explanation'), + placeholder: i18n.global.t('questions.typeExplanation'), value: '', collapsible: true, - collapsibleLabel: 'Ajouter une explication', + collapsibleLabel: i18n.global.t('questions.addExplanation'), }, { id: 'value', type: 'hidden', label: '', - placeholder: 'Valeur cachée', + placeholder: i18n.global.t('forms.type'), value: '', }, { @@ -521,64 +527,64 @@ export const listForm: Form = { ], }, { - name: 'Explication', + name: i18n.global.t('questions.explanation'), inputs: [ { id: 'explanation', type: 'html', label: '', value: '', - placeholder: 'Saisissez une explication...', + placeholder: i18n.global.t('questions.typeExplanation'), }, ], }, ], -}; +})); -export const customQuestionForm: Form = { +export const customQuestionForm: ComputedRef<Form> = computed(() => ({ type: 'custom', - name: 'Question personnalisée', + name: i18n.global.t('questions.types.custom'), icon: 'icon-terminal', displayFieldIndex: true, - buttons: contentButtons, + buttons: contentButtons.value, fields: [ { - name: "Configuration de l'évaluation", + name: i18n.global.t('questions.configuration'), inputs: [ { id: 'score', type: 'score', - label: 'Score', + label: i18n.global.t('questions.score'), value: 0, }, ], }, { - name: 'Question', + name: i18n.global.t('questions.question'), inputs: [ { id: 'label', type: 'textarea', - label: 'Question', + label: i18n.global.t('questions.question'), value: '', - placeholder: 'Posez la question', + placeholder: i18n.global.t('questions.askQuestion'), }, { id: 'statement', type: 'html-inline', - label: 'Consigne', + label: i18n.global.t('questions.instruction'), value: '', - placeholder: 'Instruction pour répondre à la question', + placeholder: i18n.global.t('questions.instructionPlaceholder'), }, ], }, { - name: 'Template', + name: i18n.global.t('questions.template.title'), inputs: [ { id: 'template', type: 'select', - label: 'Selectionnez un template', + label: i18n.global.t('questions.template.select'), value: '', options: [], linkedOptions: 'plugins.*.template', @@ -586,26 +592,26 @@ export const customQuestionForm: Form = { ], }, { - name: 'Données', + name: i18n.global.t('questions.template.data'), inputs: [ { type: 'repeat', id: 'data', - label: 'Données', + label: i18n.global.t('questions.template.data'), value: [], inputs: [ { id: 'key', type: 'text', - label: 'Clé', - placeholder: 'Clé', + label: i18n.global.t('questions.template.key'), + placeholder: i18n.global.t('questions.template.key'), value: '', }, { id: 'value', type: 'textarea', - label: 'Valeur', - placeholder: 'Valeur', + label: i18n.global.t('questions.template.value'), + placeholder: i18n.global.t('questions.template.value'), value: '', }, ], @@ -613,29 +619,36 @@ export const customQuestionForm: Form = { ], }, { - name: 'Réponse', + name: i18n.global.t('questions.response'), inputs: [ { id: 'correctResponse', - label: 'Réponse', + label: i18n.global.t('questions.response'), type: 'text', value: '', }, ], }, { - name: 'Explication', + name: i18n.global.t('questions.explanation'), inputs: [ { id: 'explanation', type: 'html', label: '', value: '', - placeholder: 'Saisissez une explication', + 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 8ec7d3120c3b56923c323cee6b839cba21648c8a..e55c54579e7f67b1ed4fb9dc88f86624f199cabb 100644 --- a/src/shared/data/sideBar.data.ts +++ b/src/shared/data/sideBar.data.ts @@ -1,108 +1,110 @@ import { SideAction } from '@/src/shared/interfaces'; +import { i18n } from '@/i18n/config'; +import { computed, ComputedRef } from 'vue'; -export const questions: SideAction[] = [ +export const questions: ComputedRef<SideAction[]> = computed(() => [ { icon: 'icon-qcm', type: 'choice', - label: 'QCM', + label: i18n.global.t('questions.types.qcm'), }, { icon: 'icon-dragdrop', type: 'drag-and-drop', - label: 'Drag & Drop', + label: i18n.global.t('questions.types.dragDrop'), }, { icon: 'icon-reorder', type: 'reorder', - label: 'Reorder', + label: i18n.global.t('questions.types.reorder'), }, { icon: 'icon-swipe', type: 'swipe', - label: 'Swipe', + label: i18n.global.t('questions.types.swipe'), }, { icon: 'icon-liste', type: 'dropdown-list', - label: 'Liste déroulante', + label: i18n.global.t('questions.types.dropdownList'), }, { icon: 'icon-terminal', type: 'custom', - label: 'Question personnalisée', - } -]; + label: i18n.global.t('questions.types.custom'), + }, +]); -const contents: SideAction[] = [ +const contents: ComputedRef<SideAction[]> = computed(() => [ { icon: 'icon-texte', type: 'text', - label: 'Texte', - tooltip: 'Glisser/déposer pour ajouter un texte', + label: i18n.global.t('sidebar.content.text'), + tooltip: i18n.global.t('sidebar.content.textTooltip'), }, { icon: 'icon-video', type: 'video', - label: 'Vidéo', - tooltip: 'Glisser/déposer pour ajouter une vidéo', + label: i18n.global.t('sidebar.content.video'), + tooltip: i18n.global.t('sidebar.content.videoTooltip'), }, { icon: 'icon-audio', type: 'audio', - label: 'Audio', - tooltip: 'Glisser/déposer pour ajouter un audio', + 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: 'Texte', - tooltip: 'Glisser/déposer pour ajouter un texte', + label: i18n.global.t('sidebar.content.text'), + tooltip: i18n.global.t('sidebar.content.textTooltip'), }, { icon: 'icon-video', type: 'video', - label: 'Vidéo', - tooltip: 'Glisser/déposer pour ajouter une vidéo', + label: i18n.global.t('sidebar.content.video'), + tooltip: i18n.global.t('sidebar.content.videoTooltip'), }, { icon: 'icon-audio', type: 'audio', - label: 'Audio', - tooltip: 'Glisser/déposer pour ajouter un audio', + label: i18n.global.t('sidebar.content.audio'), + tooltip: i18n.global.t('sidebar.content.audioTooltip'), }, { icon: 'icon-question', type: 'question', - label: 'Question', - tooltip: 'Cliquer pour ajouter une question', + label: i18n.global.t('sidebar.pages.question'), + tooltip: i18n.global.t('sidebar.pages.questionTooltip'), }, { icon: 'icon-condition', type: 'condition', - label: 'Conditions', - tooltip: 'Glisser/déposer pour ajouter une condition', + label: i18n.global.t('sidebar.pages.conditions'), + tooltip: i18n.global.t('sidebar.pages.conditionsTooltip'), }, { icon: 'icon-condition-legacy', type: 'legacy-condition', - label: 'Conditions (legacy)', - tooltip: 'Glisser/déposer pour ajouter une condition', + label: i18n.global.t('sidebar.pages.conditionsLegacy'), + tooltip: i18n.global.t('sidebar.pages.conditionsTooltip'), }, { icon: 'icon-modele', type: 'model', - label: 'Modèle', - tooltip: 'Cliquer pour ouvrir le menu modèle', + label: i18n.global.t('sidebar.pages.model'), + tooltip: i18n.global.t('sidebar.pages.modelTooltip'), }, { icon: 'icon-badge', type: 'badge', - label: 'Badge', - tooltip: 'Cliquer pour ouvrir le menu badge', + label: i18n.global.t('sidebar.pages.badge'), + tooltip: i18n.global.t('sidebar.pages.badgeTooltip'), }, -]; +]); diff --git a/src/shared/interfaces/settings.interface.ts b/src/shared/interfaces/settings.interface.ts index c7b59d43c32c1ad339b0d9f779e43c2689ab01f2..7b4a70bfc447e7f67caf012ea62f244dcc9045b6 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 7d3bf840175565ce5c0e1ee73a011346b27122c1..b551e1fc9aaaffe1bb1ebb1e01e3d867ada6ac18 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/services/graph/badge.service.ts b/src/shared/services/graph/badge.service.ts index c9c0272ce19341c75a45a59af2d2f4fc99fe0ee7..0a37bfb0ad2e2617f18af9bdc6b26774fdc15394 100644 --- a/src/shared/services/graph/badge.service.ts +++ b/src/shared/services/graph/badge.service.ts @@ -6,25 +6,27 @@ 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 '@/i18n/config'; +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 @@ -64,66 +66,66 @@ export function createRule(entry: Condition[]): Rule { return { and: rules }; } -const phraseType = { - video: 'la vidéo', - chapter: 'le chapitre', - page: 'la page', - html: 'le texte', - audio: "l'audio", - activity: "l'évaluation", - question: 'la 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: 'Avoir commencé', - false: 'Ne pas avoir pas commencé', + true: i18n.global.t('badge.phrase.verb.started.true'), + false: i18n.global.t('badge.phrase.verb.started.false'), }, completed: { - true: 'Avoir terminé', - false: 'Ne pas avoir terminé', + true: i18n.global.t('badge.phrase.verb.completed.true'), + false: i18n.global.t('badge.phrase.verb.completed.false'), }, viewed: { - true: 'Avoir vu', - false: 'Ne pas avoir vu', + true: i18n.global.t('badge.phrase.verb.viewed.true'), + false: i18n.global.t('badge.phrase.verb.viewed.false'), }, read: { - true: 'Avoir lu', - false: 'Ne pas avoir lu', + true: i18n.global.t('badge.phrase.verb.read.true'), + false: i18n.global.t('badge.phrase.verb.read.false'), }, played: { - true: 'Avoir lancé', - false: 'Ne pas avoir lancé', + true: i18n.global.t('badge.phrase.verb.played.true'), + false: i18n.global.t('badge.phrase.verb.played.false'), }, watched: { - true: 'Avoir regardé', - false: 'Ne pas avoir regardé', + true: i18n.global.t('badge.phrase.verb.watched.true'), + false: i18n.global.t('badge.phrase.verb.watched.false'), }, listened: { - true: 'Avoir écouté', - false: 'Ne pas avoir écouté', + true: i18n.global.t('badge.phrase.verb.listened.true'), + false: i18n.global.t('badge.phrase.verb.listened.false'), }, attempted: { - true: 'Avoir tenté', - false: 'Ne pas avoir tenté', + true: i18n.global.t('badge.phrase.verb.attempted.true'), + false: i18n.global.t('badge.phrase.verb.attempted.false'), }, passed: { - true: 'Avoir réussi', - false: 'Avoir échoué', + true: i18n.global.t('badge.phrase.verb.passed.true'), + false: i18n.global.t('badge.phrase.verb.passed.false'), }, - scored: "Avoir obtenu un score d'au moins", -}; + 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 = `${phraseVerb[verb]} ${value} à`; + 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/chapter.service.ts b/src/shared/services/graph/chapter.service.ts index ec165cde575f6ea44023c7733f9584cb2a9fd048..79bb2dc3dd4b42b6885b3b6bb91e41e8a20ffa95 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 '@/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/services/graph/content.service.ts b/src/shared/services/graph/content.service.ts index 288cb409e2a131d30873ef74a5cc4eb762712e23..ac562fd17dd747fd77f10e49256342e3dbe4df27 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 58ef472d363d1c3dbbf4d75b113a1fa48a765705..426be115e39763b46a8652760d668476f5f29393 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 46fe0e727ef9e472d76b8849ea72c34d498fbef2..5e8e279d30576877f4cd581491fda8e85a483de7 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 e5f69b2db69c3dc1ead701894b8ca1e852257ca5..2d91bc251e1cb487df4f105baef3dba4e1097525 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; } - } + }, }, }); diff --git a/src/shared/stores/settingsStore.ts b/src/shared/stores/settingsStore.ts index 43a720fcfaa8b13c26be1bf2682b84d50180d2a9..ef51377e51c274a0a13c1be41a8bf4247c140cb3 100644 --- a/src/shared/stores/settingsStore.ts +++ b/src/shared/stores/settingsStore.ts @@ -1,19 +1,30 @@ import { defineStore } from 'pinia'; import { editorService } from '@/src/shared/services'; import { Settings } from '@/src/shared/interfaces'; +import { i18n } from '@/i18n/config'; interface SettingsState { - settings?: Settings + settings?: Settings; + initialized: boolean; } export const useSettingsStore = defineStore('settings', { state: (): SettingsState => ({ - settings: undefined + settings: undefined, + initialized: false, }), actions: { init() { editorService.fetchSettings(); + this.initialized = true; + }, + + initSettings(settings: Settings) { + this.settings = settings; + if (this.settings.locale && this.settings.locale !== i18n.global.locale) { + i18n.global.locale = this.settings.locale; + } }, sendSettings() { @@ -22,8 +33,9 @@ export const useSettingsStore = defineStore('settings', { setSettings(spellcheck: boolean) { this.settings.spellcheck = spellcheck; + this.settings.locale = i18n.global.locale; this.sendSettings(); - } - } -}) + }, + }, +}); diff --git a/src/shared/utils/string.ts b/src/shared/utils/string.ts new file mode 100644 index 0000000000000000000000000000000000000000..4a515878b5e0760b3b5998ee9c31075bac9726e9 --- /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); +} diff --git a/src/views/EditorPage.vue b/src/views/EditorPage.vue index 534e3cde6500ffccbdc788a78621738950b49b11..42563c8601712e88a31db2e0188f7fa8f085b84a 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(); @@ -32,8 +34,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 +87,13 @@ 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' : '')); + +//? For some reason, this code doesn't work in App.vue +const settingsStore = useSettingsStore(); +if (!settingsStore.initialized) { + settingsStore.init(); +} </script> <template> @@ -95,8 +106,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 3d9b0ef31d1743b50fec8325d5ced13b7e87f114..cd34a15f0b8f33f7453237872a5141d05ce96187 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(); } @@ -44,30 +49,30 @@ 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> + <span v-else>{{ $t('landing.loadingEpoc') }}</span> </div> <div v-else> <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 +83,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>