Implémentation complète du multilingue FR/EN

- Installation vue-i18n v11 et création des fichiers de traduction (fr.json, en.json)
- Création store locale avec détection hiérarchique (URL > localStorage > navigator)
- Modification des routes avec préfixe /:locale? optionnel
- Toggle FR/EN dans Menu.vue avec synchronisation immédiate
- Traduction de ~200 textes dans 27 composants Vue
- Suppression des labels hardcodés en français côté backend
- Ajout route Kirby catch-all en/(:all?) pour /en/ URLs
- Helper addLocalePrefix() pour préserver locale dans liens dialogs
- Traduction pseudo-élément CSS via data attribute

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
isUnknown 2026-02-02 18:21:11 +01:00
parent 3af95b1d20
commit 82eb8d88cc
49 changed files with 1079 additions and 295 deletions

65
package-lock.json generated
View file

@ -19,6 +19,7 @@
"three": "^0.168.0", "three": "^0.168.0",
"uniqid": "^5.4.0", "uniqid": "^5.4.0",
"vue": "^3.5.6", "vue": "^3.5.6",
"vue-i18n": "^11.2.8",
"vue-router": "^4.4.5" "vue-router": "^4.4.5"
}, },
"devDependencies": { "devDependencies": {
@ -542,6 +543,50 @@
"@swc/helpers": "^0.5.0" "@swc/helpers": "^0.5.0"
} }
}, },
"node_modules/@intlify/core-base": {
"version": "11.2.8",
"resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-11.2.8.tgz",
"integrity": "sha512-nBq6Y1tVkjIUsLsdOjDSJj4AsjvD0UG3zsg9Fyc+OivwlA/oMHSKooUy9tpKj0HqZ+NWFifweHavdljlBLTwdA==",
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "11.2.8",
"@intlify/shared": "11.2.8"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/message-compiler": {
"version": "11.2.8",
"resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-11.2.8.tgz",
"integrity": "sha512-A5n33doOjmHsBtCN421386cG1tWp5rpOjOYPNsnpjIJbQ4POF0QY2ezhZR9kr0boKwaHjbOifvyQvHj2UTrDFQ==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "11.2.8",
"source-map-js": "^1.0.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/shared": {
"version": "11.2.8",
"resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-11.2.8.tgz",
"integrity": "sha512-l6e4NZyUgv8VyXXH4DbuucFOBmxLF56C/mqh2tvApbzl2Hrhi1aTDcuv5TKdxzfHYmpO3UB0Cz04fgDT9vszfw==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@jridgewell/sourcemap-codec": { "node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5", "version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
@ -1857,6 +1902,26 @@
} }
} }
}, },
"node_modules/vue-i18n": {
"version": "11.2.8",
"resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-11.2.8.tgz",
"integrity": "sha512-vJ123v/PXCZntd6Qj5Jumy7UBmIuE92VrtdX+AXr+1WzdBHojiBxnAxdfctUFL+/JIN+VQH4BhsfTtiGsvVObg==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "11.2.8",
"@intlify/shared": "11.2.8",
"@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": { "node_modules/vue-router": {
"version": "4.5.1", "version": "4.5.1",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.1.tgz",

View file

@ -21,6 +21,7 @@
"three": "^0.168.0", "three": "^0.168.0",
"uniqid": "^5.4.0", "uniqid": "^5.4.0",
"vue": "^3.5.6", "vue": "^3.5.6",
"vue-i18n": "^11.2.8",
"vue-router": "^4.4.5" "vue-router": "^4.4.5"
}, },
"devDependencies": { "devDependencies": {

View file

@ -28,6 +28,7 @@ return [
'install' => 'true' 'install' => 'true'
], ],
'routes' => [ 'routes' => [
require(__DIR__ . '/routes/en-locale.php'),
require(__DIR__ . '/routes/logout.php'), require(__DIR__ . '/routes/logout.php'),
require(__DIR__ . '/routes/login.php'), require(__DIR__ . '/routes/login.php'),
require(__DIR__ . '/routes/toggle-favorite.php'), require(__DIR__ . '/routes/toggle-favorite.php'),

View file

@ -0,0 +1,44 @@
<?php
/**
* Route catch-all pour le préfixe /en/
* Permet d'accéder aux pages avec /en/... pour la version anglaise
*/
return [
'pattern' => 'en/(:all?)',
'action' => function ($uri = '') {
// Si l'URI se termine par .json, chercher la page et retourner le JSON
if (str_ends_with($uri, '.json')) {
$uri = str_replace('.json', '', $uri);
$page = page($uri);
if (!$page) {
// Si pas de page trouvée, essayer avec 'home'
if ($uri === '' || $uri === '/') {
$page = page('home');
} else {
return false; // 404
}
}
// Retourner la page pour que Kirby serve le template .json.php
return $page;
}
// Pour les URLs normales (sans .json), chercher la page
$page = page($uri);
if (!$page) {
// Si pas de page trouvée, essayer avec 'home'
if ($uri === '' || $uri === '/') {
$page = page('home');
} else {
return false; // 404
}
}
// Retourner la page pour que Kirby serve le template .php
return $page;
}
];

View file

@ -32,7 +32,7 @@ return function ($page, $kirby, $site) {
return [ return [
"title" => (string) $project->title(), "title" => (string) $project->title(),
"uri" => (string) $project->uri(), "uri" => (string) $project->uri(),
"step" => (string) $project->getStepLabel(), "step" => (string) $project->currentStep(),
"uuid" => (string) $project->uuid(), "uuid" => (string) $project->uuid(),
]; ];
})->data; })->data;

View file

@ -99,7 +99,6 @@ class ProjectPage extends NotificationsPage {
} }
return [ return [
'label' => $child->title()->value(),
'id' => $child->stepName()->value(), 'id' => $child->stepName()->value(),
'slug' => $child->slug(), 'slug' => $child->slug(),
'index' => intval($child->stepIndex()->value()), 'index' => intval($child->stepIndex()->value()),
@ -217,18 +216,6 @@ class ProjectPage extends NotificationsPage {
} }
} }
public function getStepLabel() {
$stepsLabel = [
"clientBrief" => "brief",
"proposal" => "offre commerciale",
"extendedBrief" => "brief enrichi",
"industrialIdeation" => "idéation industrielle",
"virtualSample" => "échantillon virtuel",
"physicalSample" => "échantillon physique",
];
return $stepsLabel[$this->currentStep()->value()];
}
// public function printManagers() { // public function printManagers() {
// return A::implode($this->managers()->toUsers()->pluck('name'), ', '); // return A::implode($this->managers()->toUsers()->pluck('name'), ', ');

View file

@ -94,7 +94,6 @@ function processDTLProposals($page) {
], ],
"path" => "/projects/" . $page->slug() . "?dialog=proposal&fileIndex=" . $index, "path" => "/projects/" . $page->slug() . "?dialog=proposal&fileIndex=" . $index,
"date" => $proposalFile->modified("d/MM/Y"), "date" => $proposalFile->modified("d/MM/Y"),
"stepLabel" => "Proposition commerciale",
]; ];
} }
break; break;
@ -112,7 +111,6 @@ function processDTLProposals($page) {
], ],
"path" => "/projects/" . $page->slug() . "?dialog=industrial-ideation", "path" => "/projects/" . $page->slug() . "?dialog=industrial-ideation",
"date" => $proposalFile->modified("d/MM/Y"), "date" => $proposalFile->modified("d/MM/Y"),
"stepLabel" => "Idéation industrielle",
]; ];
} }
break; break;
@ -129,7 +127,6 @@ function processDTLProposals($page) {
], ],
"path" => "/projects/" . $page->slug() . "?dialog=virtual-sample", "path" => "/projects/" . $page->slug() . "?dialog=virtual-sample",
"date" => $proposalPage->modified("d/MM/Y"), "date" => $proposalPage->modified("d/MM/Y"),
"stepLabel" => "Échantillon virtuel - piste dynamique",
]; ];
} }
break; break;
@ -147,7 +144,6 @@ function processDTLProposals($page) {
], ],
"path" => "/projects/" . $page->slug() . "?dialog=virtual-sample", "path" => "/projects/" . $page->slug() . "?dialog=virtual-sample",
"date" => $proposalFile->modified("d/MM/Y"), "date" => $proposalFile->modified("d/MM/Y"),
"stepLabel" => "Échantillon virtuel - piste statique",
]; ];
} }
break; break;

View file

@ -20,7 +20,7 @@
} }
.comments.empty::after { .comments.empty::after {
content: "Partagez vos idées en ajoutant des commentaires"; content: attr(data-empty-message);
height: 100%; height: 100%;
display: grid; display: grid;
place-items: center; place-items: center;

View file

@ -29,7 +29,22 @@
<div v-if="isExpanded" id="menu" class="flex | rounded-xl"> <div v-if="isExpanded" id="menu" class="flex | rounded-xl">
<header class="w-full | flex"> <header class="w-full | flex">
<!-- TODO: à dynamiser en récupérant le $site->title() --> <!-- TODO: à dynamiser en récupérant le $site->title() -->
<p lang="en">Design to Pack</p> <p :lang="currentLocale">Design to Pack</p>
<div class="lang-toggle">
<button
@click="switchLocale('fr')"
:class="{ active: currentLocale === 'fr' }"
>
FR
</button>
<span class="slash">/</span>
<button
@click="switchLocale('en')"
:class="{ active: currentLocale === 'en' }"
>
EN
</button>
</div>
</header> </header>
<nav class="w-full | flow"> <nav class="w-full | flow">
<ul class="flex"> <ul class="flex">
@ -51,25 +66,23 @@
>{{ mainItem.title }}</router-link >{{ mainItem.title }}</router-link
> >
<span <span
v-if="mainItem.title === 'Inspirations' && page?.newInspirations" v-if="
mainItem.title === t('menu.inspirations') && page?.newInspirations
"
class="pill pill--secondary" class="pill pill--secondary"
>{{ 'Nouveautés' }}</span >{{ t('menu.news') }}</span
> >
</li> </li>
</ul> </ul>
<details :class="{ skeleton: !currentProjects }" open> <details :class="{ skeleton: !currentProjects }" open>
<summary>Projets en cours</summary> <summary>{{ t('menu.currentProjects') }}</summary>
<ul v-if="currentProjects.length > 0"> <ul v-if="currentProjects.length > 0">
<li <li
v-for="project in currentProjects" v-for="project in currentProjects"
:class="{ active: isCurrent(project) }" :class="{ active: isCurrent(project) }"
> >
<router-link <router-link
:to=" :to="getProjectPath(project)"
isEmptyBrief(project)
? project.uri + '/client-brief'
: project.uri
"
:class="hasUnreadNotification(project) ? 'new' : undefined" :class="hasUnreadNotification(project) ? 'new' : undefined"
:data-dtl="project.isDTLEnabled ? 'true' : undefined" :data-dtl="project.isDTLEnabled ? 'true' : undefined"
@click="collapse()" @click="collapse()"
@ -79,13 +92,13 @@
</ul> </ul>
</details> </details>
<details v-if="archivedProjects.length"> <details v-if="archivedProjects.length">
<summary>Projets archivés</summary> <summary>{{ t('menu.archivedProjects') }}</summary>
<ul> <ul>
<li <li
v-for="project in archivedProjects" v-for="project in archivedProjects"
:class="{ active: isCurrent(project) }" :class="{ active: isCurrent(project) }"
> >
<router-link :to="project.uri" @click="collapse()">{{ <router-link :to="getProjectPath(project)" @click="collapse()">{{
project.title project.title
}}</router-link> }}</router-link>
</li> </li>
@ -96,13 +109,19 @@
<ul class="flex"> <ul class="flex">
<li data-icon="user"> <li data-icon="user">
<a <a
:href="user.role === 'admin' ? '/panel/account' : '/account'" :href="
user.role === 'admin'
? '/panel/account'
: currentLocale === 'en'
? '/en/account'
: '/account'
"
@click="collapse()" @click="collapse()"
>Profil</a >{{ t('menu.profile') }}</a
> >
</li> </li>
<li data-icon="logout"> <li data-icon="logout">
<a href="/logout" @click="collapse()">Déconnexion</a> <a href="/logout" @click="collapse()">{{ t('menu.logout') }}</a>
</li> </li>
</ul> </ul>
</footer> </footer>
@ -113,17 +132,23 @@
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useProjectsStore } from '../stores/projects'; import { useProjectsStore } from '../stores/projects';
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useUserStore } from '../stores/user'; import { useUserStore } from '../stores/user';
import { usePageStore } from '../stores/page'; import { usePageStore } from '../stores/page';
import { useProjectStore } from '../stores/project'; import { useProjectStore } from '../stores/project';
import { useLocaleStore } from '../stores/locale';
import { useI18n } from 'vue-i18n';
const route = useRoute(); const route = useRoute();
const router = useRouter();
const isExpanded = ref(true); const isExpanded = ref(true);
const { user, notifications } = storeToRefs(useUserStore()); const { user, notifications } = storeToRefs(useUserStore());
const { currentProjects, archivedProjects } = storeToRefs(useProjectsStore()); const { currentProjects, archivedProjects } = storeToRefs(useProjectsStore());
const { isEmptyBrief } = useProjectStore(); const { isEmptyBrief } = useProjectStore();
const { page } = storeToRefs(usePageStore()); const { page } = storeToRefs(usePageStore());
const localeStore = useLocaleStore();
const { currentLocale } = storeToRefs(localeStore);
const { t } = useI18n();
const unreadNotificationsCount = computed(() => { const unreadNotificationsCount = computed(() => {
if (!user.value) return 0; if (!user.value) return 0;
@ -135,34 +160,37 @@ const unreadNotificationsCount = computed(() => {
return count === 0 ? 0 : count; return count === 0 ? 0 : count;
}); });
const mainItems = [ const mainItems = computed(() => {
const prefix = currentLocale.value === 'en' ? '/en' : '';
return [
{ {
title: 'Home', title: t('menu.home'),
path: '/', path: prefix + '/',
icon: 'home', icon: 'home',
}, },
{ {
title: 'Notifications', title: t('menu.notifications'),
path: '/notifications', path: prefix + '/notifications',
icon: 'megaphone', icon: 'megaphone',
}, },
{ {
title: 'Réunions', title: t('menu.meetings'),
path: '/reunions', path: prefix + '/reunions',
icon: 'calendar', icon: 'calendar',
disabled: true, disabled: true,
}, },
{ {
title: 'Design to Light', title: t('menu.designToLight'),
path: '/design-to-light', path: prefix + '/design-to-light',
icon: 'leaf', icon: 'leaf',
}, },
{ {
title: 'Inspirations', title: t('menu.inspirations'),
path: '/inspirations', path: prefix + '/inspirations',
icon: 'inspiration', icon: 'inspiration',
}, },
]; ];
});
function toggleExpand() { function toggleExpand() {
isExpanded.value = !isExpanded.value; isExpanded.value = !isExpanded.value;
@ -192,6 +220,33 @@ function collapse() {
isExpanded.value = false; isExpanded.value = false;
} }
} }
function switchLocale(newLocale) {
if (newLocale === currentLocale.value) return;
let newPath = route.path;
if (newLocale === 'en') {
if (!newPath.startsWith('/en')) {
newPath = '/en' + newPath;
}
} else {
if (newPath.startsWith('/en')) {
newPath = newPath.replace(/^\/en/, '') || '/';
}
}
localeStore.setLocale(newLocale);
router.push(newPath);
}
function getProjectPath(project) {
const prefix = currentLocale.value === 'en' ? '/en' : '';
const path = isEmptyBrief(project)
? project.uri + '/client-brief'
: project.uri;
return prefix + path;
}
</script> </script>
<style> <style>
@ -243,6 +298,24 @@ button[aria-controls='menu'][aria-expanded='false']
overflow-y: auto; overflow-y: auto;
} }
/* Lang toggle */
.lang-toggle {
font-family: var(--font-sans);
}
.lang-toggle button {
padding: 0;
}
.lang-toggle button.active {
background-color: transparent !important;
color: var(--color-primary);
}
.lang-toggle span.slash {
padding: 0 0.2rem;
}
@media (max-width: 1023px) { @media (max-width: 1023px) {
button[aria-controls='menu'][aria-expanded='true'] { button[aria-controls='menu'][aria-expanded='true'] {
left: 0; left: 0;

View file

@ -5,12 +5,12 @@
modal modal
:draggable="false" :draggable="false"
:dismissableMask="true" :dismissableMask="true"
header="Demander la création dun projet" :header="t('dialogs.requestProject')"
class="dialog" class="dialog"
:closeOnEscape="true" :closeOnEscape="true"
> >
<template #header> <template #header>
<h2 class="font-serif text-lg">Demander la création dun projet</h2> <h2 class="font-serif text-lg">{{ t('dialogs.requestProject') }}</h2>
</template> </template>
<form <form
@ -18,24 +18,28 @@
class="w-full h-full p-16 flex flex-col" class="w-full h-full p-16 flex flex-col"
style="--row-gap: 1rem" style="--row-gap: 1rem"
> >
<label for="project-title" class="sr-only">Nom du projet</label> <label for="project-title" class="sr-only">{{
t('forms.projectName')
}}</label>
<input <input
type="text" type="text"
v-model="title" v-model="title"
id="project-title" id="project-title"
placeholder="Nom du projet" :placeholder="t('forms.projectName')"
class="w-full rounded-md border border-grey-200 px-16 py-12" class="w-full rounded-md border border-grey-200 px-16 py-12"
required required
/> />
<label for="project-details" class="sr-only">Détails du projet</label> <label for="project-details" class="sr-only">{{
t('forms.projectDetails')
}}</label>
<textarea <textarea
id="project-details" id="project-details"
name="details" name="details"
v-model="details" v-model="details"
cols="30" cols="30"
rows="10" rows="10"
placeholder="Détails du projet…" :placeholder="t('forms.projectDetailsPlaceholder')"
class="w-full flex-1 rounded-md border border-grey-200 px-16 py-12" class="w-full flex-1 rounded-md border border-grey-200 px-16 py-12"
required required
></textarea> ></textarea>
@ -52,26 +56,24 @@
class="flex font-medium mt-4" class="flex font-medium mt-4"
style="--column-gap: var(--space-4)" style="--column-gap: var(--space-4)"
> >
Créer avec {{ t('dialogs.createWithDTL') }}
<span class="flex justify-center text-sm" data-icon="leaf" <span class="flex justify-center text-sm" data-icon="leaf">{{
>Design to Light</span t('dtl.title')
> }}</span>
</label> </label>
<p class="text-sm mt-8 mb-4"> <p class="text-sm mt-8 mb-4">
Découvrez la note environnementale de votre projet et allégez limpact {{ t('dialogs.dtlDescription') }}
de votre projet grâce à nos expertises doptimisation du poids de
flacon.
</p> </p>
<router-link to="/design-to-light" class="text-sm font-medium" <router-link to="/design-to-light" class="text-sm font-medium">{{
>En savoir plus</router-link t('dialogs.learnMore')
> }}</router-link>
</div> </div>
<footer class="flex-columns w-full mt-16" style="column-gap: 0.5rem"> <footer class="flex-columns w-full mt-16" style="column-gap: 0.5rem">
<button class="btn btn--black-10" @click="emits('close')"> <button class="btn btn--black-10" @click="emits('close')">
Annuler {{ t('buttons.cancel') }}
</button> </button>
<button class="btn" type="submit">Soumettre</button> <button class="btn" type="submit">{{ t('buttons.submit') }}</button>
</footer> </footer>
</form> </form>
</Dialog> </Dialog>
@ -81,6 +83,9 @@
import Dialog from 'primevue/dialog'; import Dialog from 'primevue/dialog';
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { useApiStore } from '../stores/api'; import { useApiStore } from '../stores/api';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const title = ref(''); const title = ref('');
const details = ref(''); const details = ref('');

View file

@ -1,6 +1,6 @@
<template> <template>
<header class="flex"> <header class="flex">
<h2 id="tabslist" class="sr-only">Projets</h2> <h2 id="tabslist" class="sr-only">{{ t('brief.projects') }}</h2>
<Tabs :tabs="tabs" @update:currentTab="changeTab" /> <Tabs :tabs="tabs" @update:currentTab="changeTab" />
</header> </header>
<section <section
@ -11,6 +11,7 @@
:aria-label="tabs[0].label" :aria-label="tabs[0].label"
class="flow" class="flow"
:class="{ skeleton: isProjectsLoading }" :class="{ skeleton: isProjectsLoading }"
:data-empty-text="t('projects.none')"
> >
<Project <Project
v-for="project in currentProjects" v-for="project in currentProjects"
@ -25,6 +26,7 @@
tabindex="0" tabindex="0"
:aria-label="tabs[1].label" :aria-label="tabs[1].label"
class="flow" class="flow"
:data-empty-text="t('projects.none')"
> >
<Project <Project
v-for="project in archivedProjects" v-for="project in archivedProjects"
@ -39,7 +41,9 @@ import Project from './project/Project.vue';
import { useProjectsStore } from '../stores/projects'; import { useProjectsStore } from '../stores/projects';
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { projects, currentProjects, archivedProjects, isProjectsLoading } = const { projects, currentProjects, archivedProjects, isProjectsLoading } =
storeToRefs(useProjectsStore()); storeToRefs(useProjectsStore());
@ -47,13 +51,13 @@ const currentTab = ref('currentProjects');
const tabs = computed(() => { const tabs = computed(() => {
return [ return [
{ {
label: 'Projets en cours', label: t('projects.current'),
id: 'currentProjects', id: 'currentProjects',
count: currentProjects.value.length, count: currentProjects.value.length,
isActive: currentTab.value === 'currentProjects', isActive: currentTab.value === 'currentProjects',
}, },
{ {
label: 'Projets archivés', label: t('projects.archived'),
id: 'archivedProjects', id: 'archivedProjects',
count: archivedProjects.value.length, count: archivedProjects.value.length,
isActive: currentTab.value === 'archivedProjects', isActive: currentTab.value === 'archivedProjects',
@ -72,7 +76,7 @@ section {
min-height: calc(100vh - 8.5rem); min-height: calc(100vh - 8.5rem);
} }
section:not(.skeleton):empty::after { section:not(.skeleton):empty::after {
content: 'Aucun projet pour le moment'; content: attr(data-empty-text);
position: absolute; position: absolute;
inset: 0; inset: 0;
display: grid; display: grid;

View file

@ -12,7 +12,7 @@
v-model="currentValue" v-model="currentValue"
:options="items" :options="items"
optionLabel="title" optionLabel="title"
:placeholder="'Sélectionnez une déclinaison'" :placeholder="t('forms.selectVariation')"
:maxSelectedLabels="3" :maxSelectedLabels="3"
class="font-serif" class="font-serif"
:class="{ active: currentValue }" :class="{ active: currentValue }"
@ -45,7 +45,7 @@
<p v-if="currentValue"> <p v-if="currentValue">
{{ currentValue.title }} {{ currentValue.title }}
</p> </p>
<p v-else>Sélectionnez une déclinaison</p> <p v-else>{{ t('forms.selectVariation') }}</p>
</template> </template>
<template #option="slotProps"> <template #option="slotProps">
@ -65,6 +65,9 @@
import { onBeforeMount, ref, watch, nextTick } from 'vue'; import { onBeforeMount, ref, watch, nextTick } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useDialogStore } from '../stores/dialog'; import { useDialogStore } from '../stores/dialog';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
// Props // Props
const { items, label, isCompareModeEnabled, index } = defineProps({ const { items, label, isCompareModeEnabled, index } = defineProps({

View file

@ -39,9 +39,7 @@
<footer v-if="!comment.isEditMode" class="comment__replies"> <footer v-if="!comment.isEditMode" class="comment__replies">
<p v-if="comment.replies?.length > 0"> <p v-if="comment.replies?.length > 0">
{{ comment.replies.length }} réponse{{ {{ comment.replies.length }} {{ comment.replies.length > 1 ? t('comments.replies') : t('comments.reply') }}
comment.replies.length > 1 ? 's' : ''
}}
</p> </p>
<div <div
v-if="userStore.canEditComment(comment)" v-if="userStore.canEditComment(comment)"
@ -52,14 +50,14 @@
data-icon="edit" data-icon="edit"
@click="editComment($event)" @click="editComment($event)"
> >
<span class="sr-only">Éditer</span> <span class="sr-only">{{ t('comments.edit') }}</span>
</button> </button>
<button <button
class="btn btn--transparent btn--icon btn--sm" class="btn btn--transparent btn--icon btn--sm"
data-icon="delete" data-icon="delete"
@click="deleteComment($event)" @click="deleteComment($event)"
> >
<span class="sr-only">Supprimer</span> <span class="sr-only">{{ t('buttons.delete') }}</span>
</button> </button>
</div> </div>
</footer> </footer>
@ -68,11 +66,11 @@
<input <input
type="submit" type="submit"
class="btn btn--tranparent" class="btn btn--tranparent"
value="Sauvegarder" :value="t('buttons.save')"
@click="saveEditedComment($event)" @click="saveEditedComment($event)"
/> />
<button class="btn btn--white-10" @click="cancelEditComment($event)"> <button class="btn btn--white-10" @click="cancelEditComment($event)">
Annuler {{ t('buttons.cancel') }}
</button> </button>
</footer> </footer>
</article> </article>
@ -88,9 +86,11 @@ import { computed, onMounted, ref, useTemplateRef } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { usePageStore } from '../../stores/page'; import { usePageStore } from '../../stores/page';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
dayjs.locale('fr'); dayjs.locale('fr');
const { t } = useI18n();
const { comment, commentIndex } = defineProps({ const { comment, commentIndex } = defineProps({
comment: Object, comment: Object,
commentIndex: Number, commentIndex: Number,
@ -125,11 +125,11 @@ function formatDate() {
const dateNumber = parseInt(dayjs(comment.date).format('YYMMD')); const dateNumber = parseInt(dayjs(comment.date).format('YYMMD'));
if (dateNumber === todayNumber) { if (dateNumber === todayNumber) {
return "Aujourd'hui"; return t('dates.today');
} }
if (dateNumber === todayNumber - 1) { if (dateNumber === todayNumber - 1) {
return 'hier'; return t('dates.yesterday');
} }
return dayjs(comment.date).format('D MMM YY'); return dayjs(comment.date).format('D MMM YY');
@ -153,7 +153,7 @@ async function read() {
page.value.uri page.value.uri
); );
} catch (error) { } catch (error) {
console.log('Erreur lors de la lecture de la notification : ', error); console.log(t('errors.readNotificationFailed'), error);
} }
} }

View file

@ -1,9 +1,10 @@
<template> <template>
<aside id="comments-container" aria-labelledby="comments-label"> <aside id="comments-container" aria-labelledby="comments-label">
<h2 id="comments-label" class="sr-only">Commentaires</h2> <h2 id="comments-label" class="sr-only">{{ t('comments.title') }}</h2>
<div <div
class="comments | flow" class="comments | flow"
:class="{ empty: !comments || comments.length === 0 }" :class="{ empty: !comments || comments.length === 0 }"
:data-empty-message="t('comments.emptyMessage')"
> >
<template v-if="comments"> <template v-if="comments">
<template v-if="!openedComment"> <template v-if="!openedComment">
@ -26,7 +27,7 @@
isAddOpen = false; isAddOpen = false;
" "
> >
<span>Retour à la liste</span> <span>{{ t('buttons.backToList') }}</span>
</button> </button>
<Comment <Comment
:comment="openedComment" :comment="openedComment"
@ -53,7 +54,7 @@
class="btn btn--white-20 | w-full" class="btn btn--white-20 | w-full"
@click="toggleCommentPositionMode(true)" @click="toggleCommentPositionMode(true)"
> >
Ajouter un commentaire {{ t('buttons.addComment') }}
</button> </button>
<button <button
v-else-if="openedComment && !isAddOpen" v-else-if="openedComment && !isAddOpen"
@ -61,7 +62,7 @@
class="btn btn--white-20 | justify-start w-full | text-white-50" class="btn btn--white-20 | justify-start w-full | text-white-50"
@click="isAddOpen = true" @click="isAddOpen = true"
> >
Répondre {{ t('buttons.reply') }}
</button> </button>
<!-- TODO: afficher #new-comment une fois le bouton Ajouter un commentaire cliqué --> <!-- TODO: afficher #new-comment une fois le bouton Ajouter un commentaire cliqué -->
<div <div
@ -70,11 +71,10 @@
class="bg-primary | text-sm text-white | rounded-lg | p-12" class="bg-primary | text-sm text-white | rounded-lg | p-12"
> >
<p class="flex justify-start | mb-12" data-icon="comment"> <p class="flex justify-start | mb-12" data-icon="comment">
<strong>Nouveau commentaire</strong> <strong>{{ t('comments.new') }}</strong>
</p> </p>
<p> <p>
Dans la zone du contenu, cliquez vous souhaitez positionner le {{ t('comments.newInstruction') }}
commentaire
</p> </p>
</div> </div>
<form <form
@ -84,13 +84,13 @@
class="flow | p-12 | rounded-xl" class="flow | p-12 | rounded-xl"
@submit="handleSubmit" @submit="handleSubmit"
> >
<label class="sr-only" for="comment">Votre commentaire</label> <label class="sr-only" for="comment">{{ t('comments.your') }}</label>
<textarea <textarea
v-model="draftComment.text" v-model="draftComment.text"
:disabled="isSubmitting ? true : undefined" :disabled="isSubmitting ? true : undefined"
name="comment" name="comment"
id="comment" id="comment"
placeholder="Ajouter un commentaire…" :placeholder="t('forms.commentPlaceholder')"
rows="5" rows="5"
class="text-sm | rounded-lg bg-black p-12" class="text-sm | rounded-lg bg-black p-12"
></textarea> ></textarea>
@ -99,11 +99,11 @@
type="submit" type="submit"
class="btn" class="btn"
:class="{ submitting: isSubmitting }" :class="{ submitting: isSubmitting }"
:value="isSubmitting ? 'En cours' : undefined" :value="isSubmitting ? t('comments.inProgress') : undefined"
:disabled="isSubmitting ? true : undefined" :disabled="isSubmitting ? true : undefined"
/> />
<button class="btn btn--white-10" @click="isAddOpen = false"> <button class="btn btn--white-10" @click="isAddOpen = false">
Annuler {{ t('buttons.cancel') }}
</button> </button>
</footer> </footer>
</form> </form>
@ -122,9 +122,11 @@ import { useDialogStore } from '../../stores/dialog';
import Comment from './Comment.vue'; import Comment from './Comment.vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
dayjs.locale('fr'); dayjs.locale('fr');
const { t } = useI18n();
const { user } = useUserStore(); const { user } = useUserStore();
const { page } = usePageStore(); const { page } = usePageStore();
const dialog = useDialogStore(); const dialog = useDialogStore();

View file

@ -1,13 +1,16 @@
<template> <template>
<button id="dtl-btn" class="bg-black rounded-md p-4" :title="'Design to Light: ' + (hasAlternatives ? 'New' : grade)" data-icon="leaf" :data-grade="grade" :data-new="hasAlternatives ? true : undefined"> <button id="dtl-btn" class="bg-black rounded-md p-4" :title="t('dtl.grade', { grade: hasAlternatives ? t('menu.news') : grade })" data-icon="leaf" :data-grade="grade" :data-new="hasAlternatives ? true : undefined">
<span lang="en" class="sr-only">Design to Light</span> <span lang="en" class="sr-only">{{ t('dtl.title') }}</span>
<span v-if="hasAlternatives" lang="en" class="new">New</span> <span v-if="hasAlternatives" lang="en" class="new">{{ t('menu.news') }}</span>
</button> </button>
</template> </template>
<script setup> <script setup>
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { usePageStore } from "../../stores/page"; import { usePageStore } from "../../stores/page";
import { computed } from "vue"; import { computed } from "vue";
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { page } = storeToRefs(usePageStore()); const { page } = storeToRefs(usePageStore());

View file

@ -17,8 +17,8 @@
proposals.length === 1 && isDialogOpen proposals.length === 1 && isDialogOpen
? activeProposal.title ? activeProposal.title
? activeProposal.title ? activeProposal.title
: 'Design to light' : t('dtl.title')
: 'Design to light' : t('dtl.title')
}} }}
</h2> </h2>
<button <button
@ -27,7 +27,7 @@
class="btn btn--icon btn--transparent | ml-auto" class="btn btn--icon btn--transparent | ml-auto"
data-icon="close" data-icon="close"
> >
<span class="sr-only">Fermer</span> <span class="sr-only">{{ t('buttons.close') }}</span>
</button> </button>
</header> </header>
<nav v-if="!isDialogOpen" class="tabs" role="tablist" tabindex="-1"> <nav v-if="!isDialogOpen" class="tabs" role="tablist" tabindex="-1">
@ -42,8 +42,8 @@
proposal.title proposal.title
? proposal.title ? proposal.title
: index === 0 : index === 0
? 'Proposition initiale' ? t('dtl.initialProposal')
: 'Alternative ' + index : t('dtl.alternative', { index })
}} }}
</button> </button>
</nav> </nav>
@ -67,12 +67,12 @@
/> />
</router-link> </router-link>
<p> <p>
Données basées sur la proposition <br />du {{ activeProposal.date }} {{ t('dtl.proposalBasedOn') }} <br />du {{ activeProposal.date }}
<br />{{ activeProposal.stepLabel }} <br />{{ stepLabel }}
</p> </p>
</div> </div>
<div id="note-globale" class="px-32 py-16 border-b flow"> <div id="note-globale" class="px-32 py-16 border-b flow">
<h3>Note globale</h3> <h3>{{ t('dtl.globalScore') }}</h3>
<div class="flex" style="--column-gap: 1rem"> <div class="flex" style="--column-gap: 1rem">
<p :data-grade="activeProposal.grades.global.letter"> <p :data-grade="activeProposal.grades.global.letter">
<strong class="sr-only">{{ <strong class="sr-only">{{
@ -100,15 +100,15 @@
</div> </div>
</div> </div>
<div id="positionnement" class="px-32 py-16 border-b flow"> <div id="positionnement" class="px-32 py-16 border-b flow">
<h3>Positionnement</h3> <h3>{{ t('dtl.positioning') }}</h3>
<dl> <dl>
<dt id="design">Design</dt> <dt id="design">{{ t('dtl.design') }}</dt>
<dd> <dd>
<span class="sr-only">{{ <span class="sr-only">{{
activeProposal.grades.position.complexity activeProposal.grades.position.complexity
}}</span> }}</span>
</dd> </dd>
<dt id="poids">Poids</dt> <dt id="poids">{{ t('dtl.weight') }}</dt>
<dd> <dd>
<span class="sr-only">{{ <span class="sr-only">{{
activeProposal.grades.position.weight activeProposal.grades.position.weight
@ -132,7 +132,7 @@
</button> </button>
</div> </div>
<div id="indicateur" class="px-32 py-16 border-b flow"> <div id="indicateur" class="px-32 py-16 border-b flow">
<h3>Indicateur des composants impliqués</h3> <h3>{{ t('dtl.indicators') }}</h3>
<div class="grid"> <div class="grid">
<template <template
v-for="indicator in activeProposal.grades.indicators" v-for="indicator in activeProposal.grades.indicators"
@ -170,8 +170,8 @@
> >
{{ {{
page.hasOptimizationRequest page.hasOptimizationRequest
? "Demande d'expertise en cours de traitement…" ? t('dtl.requestPending')
: 'Demander une expertise doptimisation' : t('dtl.requestOptimization')
}} }}
</button> </button>
</footer> </footer>
@ -190,6 +190,7 @@ import { storeToRefs } from 'pinia';
import { ref, onBeforeUnmount, computed } from 'vue'; import { ref, onBeforeUnmount, computed } from 'vue';
import { useDialogStore } from '../../stores/dialog'; import { useDialogStore } from '../../stores/dialog';
import { usePageStore } from '../../stores/page'; import { usePageStore } from '../../stores/page';
import { useI18n } from 'vue-i18n';
const { proposals } = defineProps({ const { proposals } = defineProps({
proposals: Array, proposals: Array,
@ -197,6 +198,7 @@ const { proposals } = defineProps({
const { page } = storeToRefs(usePageStore()); const { page } = storeToRefs(usePageStore());
const { openedFile, activeTracks } = storeToRefs(useDialogStore()); const { openedFile, activeTracks } = storeToRefs(useDialogStore());
const { t } = useI18n();
const isDialogOpen = computed(() => { const isDialogOpen = computed(() => {
if (openedFile.value) { if (openedFile.value) {
@ -215,6 +217,17 @@ const emits = defineEmits(['close']);
const activeProposal = const activeProposal =
proposals.length === 1 ? computed(() => proposals[0]) : ref(proposals[0]); proposals.length === 1 ? computed(() => proposals[0]) : ref(proposals[0]);
const stepLabel = computed(() => {
const proposal = activeProposal.value || activeProposal;
const location = proposal.location;
if (location.step === 'virtualSample') {
return location.type === 'dynamic' ? t('dtl.dynamicTrack') : t('dtl.staticTrack');
}
return t('steps.' + location.step);
});
window.addEventListener('keyup', closeOnEscape); window.addEventListener('keyup', closeOnEscape);
window.addEventListener('click', close); window.addEventListener('click', close);

View file

@ -5,15 +5,15 @@
modal modal
:draggable="false" :draggable="false"
:dismissableMask="true" :dismissableMask="true"
header="Demander un rendez-vous" :header="t('dialogs.requestMeeting')"
class="dialog" class="dialog"
:closeOnEscape="true" :closeOnEscape="true"
@click="preventClose($event)" @click="preventClose($event)"
> >
<template #header> <template #header>
<h2 class="font-serif text-lg">Demander un rendez-vous</h2> <h2 class="font-serif text-lg">{{ t('dialogs.requestMeeting') }}</h2>
<p class="flex justify-center text-sm" data-icon="leaf"> <p class="flex justify-center text-sm" data-icon="leaf">
Design to Light {{ t('dtl.title') }}
</p> </p>
</template> </template>
@ -22,7 +22,7 @@
class="w-full h-full p-16 flex flex-col" class="w-full h-full p-16 flex flex-col"
style="--row-gap: 1rem" style="--row-gap: 1rem"
> >
<label for="projects" class="sr-only">Projet</label> <label for="projects" class="sr-only">{{ t('brief.projects') }}</label>
<select <select
name="projects" name="projects"
id="projects" id="projects"
@ -30,7 +30,7 @@
class="w-full rounded-md border border-grey-200 px-16 py-12" class="w-full rounded-md border border-grey-200 px-16 py-12"
required required
> >
<option value="">Sélectionnez le projet</option> <option value="">{{ t('forms.selectVariation') }}</option>
<option <option
v-for="project in currentProjects" v-for="project in currentProjects"
:key="project.uri" :key="project.uri"
@ -41,35 +41,37 @@
</option> </option>
</select> </select>
<label for="appointment-subject" class="sr-only" <label for="appointment-subject" class="sr-only">{{
>Objet du rendez-vous</label t('forms.meetingSubject')
> }}</label>
<input <input
type="text" type="text"
v-model="subject" v-model="subject"
id="appointment-subject" id="appointment-subject"
placeholder="Objet du rendez-vous" :placeholder="t('forms.meetingSubject')"
class="w-full rounded-md border border-grey-200 px-16 py-12" class="w-full rounded-md border border-grey-200 px-16 py-12"
required required
/> />
<label for="appointment-details" class="sr-only">Détails du projet</label> <label for="appointment-details" class="sr-only">{{
t('forms.meetingDetails')
}}</label>
<textarea <textarea
id="appointment-details" id="appointment-details"
name="details" name="details"
v-model="details" v-model="details"
cols="30" cols="30"
rows="10" rows="10"
placeholder="Décrivez votre demande…" :placeholder="t('forms.meetingDetailsPlaceholder')"
class="w-full flex-1 rounded-md border border-grey-200 px-16 py-12" class="w-full flex-1 rounded-md border border-grey-200 px-16 py-12"
required required
></textarea> ></textarea>
<footer class="flex-columns w-full mt-16" style="column-gap: 0.5rem"> <footer class="flex-columns w-full mt-16" style="column-gap: 0.5rem">
<button class="btn btn--black-10" @click="emits('close')"> <button class="btn btn--black-10" @click="emits('close')">
Annuler {{ t('buttons.cancel') }}
</button> </button>
<button class="btn" type="submit">Soumettre</button> <button class="btn" type="submit">{{ t('buttons.submit') }}</button>
</footer> </footer>
</form> </form>
</Dialog> </Dialog>
@ -82,6 +84,9 @@ import { storeToRefs } from 'pinia';
import { useProjectsStore } from '../../stores/projects'; import { useProjectsStore } from '../../stores/projects';
import { usePageStore } from '../../stores/page'; import { usePageStore } from '../../stores/page';
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { page } = storeToRefs(usePageStore()); const { page } = storeToRefs(usePageStore());
const { currentProjects } = storeToRefs(useProjectsStore()); const { currentProjects } = storeToRefs(useProjectsStore());

View file

@ -3,7 +3,7 @@
<!-- Favorite button --> <!-- Favorite button -->
<button <button
class="favorite" class="favorite"
:aria-label="isFavorite ? 'Retirer des favoris' : 'Ajouter aux favoris'" :aria-label="isFavorite ? t('inspirations.removeFromFavorites') : t('inspirations.addToFavorites')"
:aria-pressed="isFavorite" :aria-pressed="isFavorite"
@click="toggleFavorite" @click="toggleFavorite"
> >
@ -34,6 +34,9 @@
import { computed } from "vue"; import { computed } from "vue";
import { useUserStore } from "../../stores/user"; import { useUserStore } from "../../stores/user";
import { useApiStore } from "../../stores/api"; import { useApiStore } from "../../stores/api";
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
// Props // Props
const { item, inspirationUri } = defineProps({ const { item, inspirationUri } = defineProps({
@ -70,7 +73,7 @@ async function toggleFavorite() {
// Update item favorite users list based on API response // Update item favorite users list based on API response
item.favoriteForUsers = newFavoriteUsers; item.favoriteForUsers = newFavoriteUsers;
} catch (error) { } catch (error) {
console.error("Failed to toggle favorite:", error); console.error(t('errors.toggleFavoriteFailed'), error);
isFavorite.value = previousState; // Rollback on failure isFavorite.value = previousState; // Rollback on failure
} }
} }

View file

@ -10,12 +10,12 @@
<strong <strong
class="notification__type | font-medium text-primary" class="notification__type | font-medium text-primary"
data-icon="calendar" data-icon="calendar"
>Demande de rendez-vous</strong >{{ t('notifications.meetingRequest') }}</strong
> >
<span class="notification__client | text-grey-700" <span class="notification__client | text-grey-700"
>{{ notification.project.title }} >{{ notification.project.title }}
{{ {{
notification.project.status === "draft" ? "(brouillon)" : "" notification.project.status === "draft" ? t('notifications.draft') : ""
}}</span }}</span
> >
<time <time
@ -29,7 +29,7 @@
v-if="notification.type" v-if="notification.type"
class="notification__body | text-md font-medium | line-clamp" class="notification__body | text-md font-medium | line-clamp"
v-html=" v-html="
'Auteur : ' + t('notifications.author') + ' ' +
(notification.author.name (notification.author.name
? notification.author.name + ' (' + notification.author.email + ')' ? notification.author.name + ' (' + notification.author.email + ')'
: notification.author.email) + : notification.author.email) +
@ -44,6 +44,9 @@
import { useNotificationsStore } from "../../stores/notifications"; import { useNotificationsStore } from "../../stores/notifications";
import { useUserStore } from "../../stores/user"; import { useUserStore } from "../../stores/user";
import { useApiStore } from "../../stores/api"; import { useApiStore } from "../../stores/api";
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { notification } = defineProps({ notification: Object }); const { notification } = defineProps({ notification: Object });
const { formatDate } = useNotificationsStore(); const { formatDate } = useNotificationsStore();

View file

@ -3,14 +3,14 @@
class="notification | bg-white rounded-lg | p-16 | flow" class="notification | bg-white rounded-lg | p-16 | flow"
data-type="content" data-type="content"
@click="readNotification()" @click="readNotification()"
title="Aller au contenu" :title="t('notifications.goToContent')"
> >
<header> <header>
<p class="flex"> <p class="flex">
<strong <strong
class="notification__type | font-medium text-primary" class="notification__type | font-medium text-primary"
data-icon="content" data-icon="content"
>Contenu</strong >{{ t('notifications.content') }}</strong
> >
<span class="notification__client | text-grey-700">{{ <span class="notification__client | text-grey-700">{{
notification.project.title notification.project.title
@ -36,6 +36,9 @@ import { useRouter } from "vue-router";
import { useNotificationsStore } from "../../stores/notifications"; import { useNotificationsStore } from "../../stores/notifications";
import { useUserStore } from "../../stores/user"; import { useUserStore } from "../../stores/user";
import { useApiStore } from "../../stores/api"; import { useApiStore } from "../../stores/api";
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const { notification } = defineProps({ notification: Object }); const { notification } = defineProps({ notification: Object });

View file

@ -10,12 +10,12 @@
<strong <strong
class="notification__type | font-medium text-primary" class="notification__type | font-medium text-primary"
data-icon="document" data-icon="document"
>Demande de création de projet</strong >{{ t('notifications.projectRequest') }}</strong
> >
<span class="notification__client | text-grey-700" <span class="notification__client | text-grey-700"
>{{ notification.project.title }} >{{ notification.project.title }}
{{ {{
notification.project.status === "draft" ? "(brouillon)" : "" notification.project.status === "draft" ? t('notifications.draft') : ""
}}</span }}</span
> >
<time <time
@ -29,7 +29,7 @@
v-if="notification.type" v-if="notification.type"
class="notification__body | text-md font-medium | line-clamp" class="notification__body | text-md font-medium | line-clamp"
v-html=" v-html="
'De la part de ' + t('notifications.from') + ' ' +
(notification.author.name (notification.author.name
? notification.author.name + ' (' + notification.author.email + ')' ? notification.author.name + ' (' + notification.author.email + ')'
: notification.author.email) + : notification.author.email) +
@ -44,6 +44,9 @@
import { useNotificationsStore } from "../../stores/notifications"; import { useNotificationsStore } from "../../stores/notifications";
import { useUserStore } from "../../stores/user"; import { useUserStore } from "../../stores/user";
import { useApiStore } from "../../stores/api"; import { useApiStore } from "../../stores/api";
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { notification } = defineProps({ notification: Object }); const { notification } = defineProps({ notification: Object });
const { formatDate } = useNotificationsStore(); const { formatDate } = useNotificationsStore();

View file

@ -14,9 +14,9 @@
data-icon="comment" data-icon="comment"
@click="isCommentsOpen = !isCommentsOpen" @click="isCommentsOpen = !isCommentsOpen"
> >
<span class="sr-only" <span class="sr-only">{{
>{{ isCommentsOpen ? 'Masquer' : 'Afficher' }} les commentaires</span isCommentsOpen ? t('buttons.hideComments') : t('buttons.showComments')
> }}</span>
</button> </button>
<a <a
id="download-pdf" id="download-pdf"
@ -25,7 +25,7 @@
:href="openedFile.url" :href="openedFile.url"
download download
> >
<span class="sr-only">Télécharger le fichier PDF</span> <span class="sr-only">{{ t('buttons.downloadPdf') }}</span>
</a> </a>
<Comments v-if="isCommentsOpen" /> <Comments v-if="isCommentsOpen" />
</template> </template>
@ -36,6 +36,9 @@ import { ref, watch, computed, unref } from 'vue';
import { useDialogStore } from '../../stores/dialog'; import { useDialogStore } from '../../stores/dialog';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { VPdfViewer, useLicense } from '@vue-pdf-viewer/viewer'; import { VPdfViewer, useLicense } from '@vue-pdf-viewer/viewer';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const licenseKey = const licenseKey =
import.meta.env.VITE_VPV_LICENSE ?? import.meta.env.VITE_VPV_LICENSE ??

View file

@ -31,7 +31,7 @@
:data-status="setStatus(project.steps, project.currentStep, step)" :data-status="setStatus(project.steps, project.currentStep, step)"
> >
<span class="pill" :data-icon="step.id"> <span class="pill" :data-icon="step.id">
<span>{{ step.label }}</span> <span>{{ t('steps.' + step.id) }}</span>
</span> </span>
</li> </li>
</ol> </ol>
@ -42,6 +42,7 @@
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import 'dayjs/locale/fr'; import 'dayjs/locale/fr';
import { useProjectStore } from '../../stores/project'; import { useProjectStore } from '../../stores/project';
import { useI18n } from 'vue-i18n';
dayjs.locale('fr'); dayjs.locale('fr');
@ -52,4 +53,5 @@ const frenchFormattedModified = dayjs(project.modified).format(
); );
const { stepsLabels, setStatus, isEmptyBrief } = useProjectStore(); const { stepsLabels, setStatus, isEmptyBrief } = useProjectStore();
const { t } = useI18n();
</script> </script>

View file

@ -5,7 +5,7 @@
:data-status="setStatus(page.steps, page.content.currentstep, step)" :data-status="setStatus(page.steps, page.content.currentstep, step)"
> >
<h2 :id="step.id"> <h2 :id="step.id">
<span :data-icon="step.id">{{ step.label }}</span> <span :data-icon="step.id">{{ t('steps.' + step.id) }}</span>
</h2> </h2>
<div <div
ref="cards-node" ref="cards-node"
@ -29,6 +29,7 @@ import SimpleDocument from './cards/SimpleDocument.vue';
import VirtualSample from './cards/VirtualSample.vue'; import VirtualSample from './cards/VirtualSample.vue';
import PhysicalSample from './cards/PhysicalSample.vue'; import PhysicalSample from './cards/PhysicalSample.vue';
import { useUserStore } from '../../stores/user'; import { useUserStore } from '../../stores/user';
import { useI18n } from 'vue-i18n';
const { step } = defineProps({ const { step } = defineProps({
step: Object, step: Object,
@ -49,6 +50,7 @@ const { page } = usePageStore();
const { setStatus } = useProjectStore(); const { setStatus } = useProjectStore();
const cardsNode = useTemplateRef('cards-node'); const cardsNode = useTemplateRef('cards-node');
const { user } = useUserStore(); const { user } = useUserStore();
const { t } = useI18n();
// Hooks // Hooks
onMounted(() => { onMounted(() => {

View file

@ -5,7 +5,7 @@
modal modal
:draggable="false" :draggable="false"
:dismissableMask="true" :dismissableMask="true"
header="Titre du PDF" :header="t('dialogs.pdfTitle')"
class="dialog" class="dialog"
:class="[ :class="[
{ 'with-comments': isCommentsOpen }, { 'with-comments': isCommentsOpen },
@ -22,7 +22,7 @@
class="btn" class="btn"
@click="validate()" @click="validate()"
> >
Valider et envoyer le brief {{ t('buttons.validate') }}
</button> </button>
<h2 <h2
v-if="openedFile" v-if="openedFile"
@ -52,7 +52,9 @@ import { useRoute, useRouter } from 'vue-router';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useApiStore } from '../../stores/api'; import { useApiStore } from '../../stores/api';
import { usePageStore } from '../../stores/page'; import { usePageStore } from '../../stores/page';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { openedFile, isCommentsOpen } = storeToRefs(useDialogStore()); const { openedFile, isCommentsOpen } = storeToRefs(useDialogStore());
const router = useRouter(); const router = useRouter();

View file

@ -6,12 +6,12 @@
<label <label
for="project-description" for="project-description"
class="flex | text-sm text-grey-700 | mb-8" class="flex | text-sm text-grey-700 | mb-8"
>Description du projet</label >{{ t('forms.description') }}</label
> >
<textarea <textarea
name="project-description" name="project-description"
id="project-description" id="project-description"
placeholder="Ajoutez une description générale de votre projet…" :placeholder="t('forms.descriptionPlaceholder')"
rows="2" rows="2"
class="border border-grey-200 | rounded-xl | p-16 | w-full" class="border border-grey-200 | rounded-xl | p-16 | w-full"
v-model="page.content.description" v-model="page.content.description"
@ -19,7 +19,7 @@
></textarea> ></textarea>
</div> </div>
<fieldset class="project-details__filters | flex-1"> <fieldset class="project-details__filters | flex-1">
<legend class="text-sm text-grey-700 | mb-8">Filtrer par tags</legend> <legend class="text-sm text-grey-700 | mb-8">{{ t('forms.filterByTags') }}</legend>
<div class="flex" style="gap: var(--space-8)"> <div class="flex" style="gap: var(--space-8)">
<button <button
class="btn btn--sm btn--grey" class="btn btn--sm btn--grey"
@ -28,7 +28,7 @@
role="switch" role="switch"
@click="clearTags()" @click="clearTags()"
> >
Voir tout {{ t('buttons.seeAll') }}
</button> </button>
<template v-for="tag in page.tags" :key="tag"> <template v-for="tag in page.tags" :key="tag">
<input <input
@ -53,6 +53,9 @@ import { ref, watch } from "vue";
import { usePageStore } from "../../../stores/page"; import { usePageStore } from "../../../stores/page";
import StringUtils from "../../../utils/string"; import StringUtils from "../../../utils/string";
import debounce from "lodash/debounce"; import debounce from "lodash/debounce";
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { selectedTags } = defineProps({ const { selectedTags } = defineProps({
selectedTags: Array, selectedTags: Array,
@ -93,7 +96,7 @@ const saveDescription = debounce(() => {
console.log(json); console.log(json);
}) })
.catch((error) => { .catch((error) => {
console.error("Erreur lors de la sauvegarde :", error); console.error(t('errors.saveFailed'), error);
isWaitingForSave.value = false; isWaitingForSave.value = false;
}); });
} }

View file

@ -3,7 +3,7 @@
v-model:visible="isOpen" v-model:visible="isOpen"
id="image-details" id="image-details"
modal modal
header="Détails de limage" :header="t('dialogs.imageDetails')"
class="bg-white | text-grey-800 | rounded-2xl | overflow-hidden" class="bg-white | text-grey-800 | rounded-2xl | overflow-hidden"
> >
<picture :style="'--image: url('+image.url+')'"> <picture :style="'--image: url('+image.url+')'">
@ -11,7 +11,7 @@
</picture> </picture>
<div class="flex flex-col | p-32" style="--row-gap: var(--space-32)"> <div class="flex flex-col | p-32" style="--row-gap: var(--space-32)">
<fieldset class="image__tags"> <fieldset class="image__tags">
<legend class="text-sm text-grey-700 | mb-8">Tags</legend> <legend class="text-sm text-grey-700 | mb-8">{{ t('forms.tags') }}</legend>
<div class="flex" style="gap: var(--space-8)"> <div class="flex" style="gap: var(--space-8)">
<template v-for="(pageTag, index) in page.tags" :key="index"> <template v-for="(pageTag, index) in page.tags" :key="index">
<input <input
@ -33,12 +33,12 @@
<label <label
for="image-description" for="image-description"
class="flex | text-sm text-grey-700 | mb-8" class="flex | text-sm text-grey-700 | mb-8"
>Description de limage</label >{{ t('forms.imageDescription') }}</label
> >
<textarea <textarea
name="image-description" name="image-description"
id="image-description" id="image-description"
placeholder="Ajoutez une description à limage…" :placeholder="t('forms.imageDescriptionPlaceholder')"
class="border border-grey-200 | rounded-xl | p-16 | w-full" class="border border-grey-200 | rounded-xl | p-16 | w-full"
v-model="image.description" v-model="image.description"
@input="saveDescription()" @input="saveDescription()"
@ -49,7 +49,7 @@
class="btn btn--black-10 | ml-auto mt-auto" class="btn btn--black-10 | ml-auto mt-auto"
@click="remove()" @click="remove()"
> >
Supprimer cette image {{ t('buttons.deleteImage') }}
</button> </button>
</div> </div>
</Dialog> </Dialog>
@ -61,6 +61,9 @@ import { usePageStore } from "../../../stores/page";
import StringUtils from "../../../utils/string"; import StringUtils from "../../../utils/string";
import Dialog from "primevue/dialog"; import Dialog from "primevue/dialog";
import debounce from "lodash/debounce"; import debounce from "lodash/debounce";
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { imageDetails } = defineProps({ const { imageDetails } = defineProps({
imageDetails: Object, imageDetails: Object,
@ -97,7 +100,7 @@ function saveTags() {
console.log(json); console.log(json);
}) })
.catch((error) => { .catch((error) => {
console.error("Erreur lors de la sauvegarde :", error); console.error(t('errors.saveFailed'), error);
}); });
} }
@ -122,7 +125,7 @@ const saveDescription = debounce(() => {
emit(""); emit("");
}) })
.catch((error) => { .catch((error) => {
console.error("Erreur lors de la sauvegarde :", error); console.error(t('errors.saveFailed'), error);
}); });
}, 1000); }, 1000);
@ -142,7 +145,7 @@ function remove() {
isOpen.value = false; isOpen.value = false;
}) })
.catch((error) => { .catch((error) => {
console.error("Erreur lors de la suppression :", error); console.error(t('errors.deleteFailed'), error);
}); });
} }
</script> </script>

View file

@ -5,7 +5,7 @@
:draggable="false" :draggable="false"
:dismissableMask="true" :dismissableMask="true"
modal modal
header="Ajouter des images" :header="t('dialogs.addImages')"
class="bg-white | text-grey-800 | rounded-2xl | overflow-hidden | p-32" class="bg-white | text-grey-800 | rounded-2xl | overflow-hidden | p-32"
> >
<div class="with-sidebar | h-full"> <div class="with-sidebar | h-full">
@ -56,19 +56,18 @@
id="delete-image" id="delete-image"
v-model:visible="deleteIsOpen" v-model:visible="deleteIsOpen"
modal modal
header="Êtes-vous sûr de vouloir supprimer cette image ?" :header="t('dialogs.deleteConfirm')"
class="bg-white | text-grey-800 | rounded-2xl | overflow-hidden | text-center | w-full max-w | p-16 pt-32" class="bg-white | text-grey-800 | rounded-2xl | overflow-hidden | text-center | w-full max-w | p-16 pt-32"
style="--row-gap: var(--space-32); --max-w: 40rem" style="--row-gap: var(--space-32); --max-w: 40rem"
> >
<p class="text-grey-700 | px-16"> <p class="text-grey-700 | px-16">
Si vous supprimez cette image, celle-ci disparaîtra de votre brief ainsi {{ t('dialogs.deleteWarning') }}
que toutes les informations qui lui sont attribuées.
</p> </p>
<template #footer> <template #footer>
<button class="btn btn--secondary | flex-1" @click="deleteIsOpen = false"> <button class="btn btn--secondary | flex-1" @click="deleteIsOpen = false">
Annuler {{ t('buttons.cancel') }}
</button> </button>
<button class="btn | flex-1" @click="">Supprimer</button> <button class="btn | flex-1" @click="">{{ t('buttons.delete') }}</button>
</template> </template>
</Dialog> </Dialog>
</template> </template>
@ -79,6 +78,9 @@ import ImagesEditPanel from './ImagesEditPanel.vue';
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useAddImagesModalStore } from '../../../../stores/addImagesModal'; import { useAddImagesModalStore } from '../../../../stores/addImagesModal';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { isAddImagesModalOpen } = defineProps({ const { isAddImagesModalOpen } = defineProps({
isAddImagesModalOpen: Boolean, isAddImagesModalOpen: Boolean,

View file

@ -8,8 +8,8 @@
:multiple="true" :multiple="true"
accept="image/*" accept="image/*"
:maxFileSize="1000000" :maxFileSize="1000000"
invalidFileSizeMessage="Fichier trop lourd" :invalidFileSizeMessage="t('errors.saveFailed')"
chooseLabel="Ajouter une ou plusieurs images" :chooseLabel="t('forms.addImages')"
class="flex flex-col justify-center | bg-white | border border-grey-200 | text-grey-800 | font-medium | rounded-xl" class="flex flex-col justify-center | bg-white | border border-grey-200 | text-grey-800 | font-medium | rounded-xl"
ref="uploadBtn" ref="uploadBtn"
> >
@ -38,7 +38,7 @@
removeFileCallback, removeFileCallback,
}" }"
> >
<div v-if="files.length > 0">Fichiers importés</div> <div v-if="files.length > 0">{{ t('forms.uploadedFiles') }}</div>
</template> </template>
</FileUpload> </FileUpload>
<figure <figure
@ -71,6 +71,9 @@ import { storeToRefs } from "pinia";
import { computed, ref } from "vue"; import { computed, ref } from "vue";
import { useAddImagesModalStore } from "../../../../stores/addImagesModal"; import { useAddImagesModalStore } from "../../../../stores/addImagesModal";
import ArrayUtils from "../../../../utils/array"; import ArrayUtils from "../../../../utils/array";
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { page } = storeToRefs(usePageStore()); const { page } = storeToRefs(usePageStore());
const toast = useToast(); const toast = useToast();

View file

@ -3,7 +3,7 @@
v-if="images.length > 0" v-if="images.length > 0"
:step="step" :step="step"
:images="images" :images="images"
:uri="'/' + step.uri" :uri="addLocalePrefix(step.uri)"
/> />
<Document v-if="pdf" :step="step" :pdf="pdf" /> <Document v-if="pdf" :step="step" :pdf="pdf" />
@ -12,11 +12,11 @@
class="btn | w-full" class="btn | w-full"
@click="goToImagesBrief()" @click="goToImagesBrief()"
> >
Ajouter un brief via la plateforme {{ t('brief.addPlatform') }}
</button> </button>
<div class="btn | w-full" v-if="!pdf && step.id === 'clientBrief'"> <div class="btn | w-full" v-if="!pdf && step.id === 'clientBrief'">
<label for="upload-pdf"> <label for="upload-pdf">
Ajouter un brief PDF {{ t('brief.addPdf') }}
<input <input
id="upload-pdf" id="upload-pdf"
type="file" type="file"
@ -35,10 +35,13 @@ import Images from "./Images.vue";
import Document from "./Document.vue"; import Document from "./Document.vue";
import { useBriefStore } from "../../../stores/brief"; import { useBriefStore } from "../../../stores/brief";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import { addLocalePrefix } from "../../../utils/router";
const { step } = defineProps({ step: Object }); const { step } = defineProps({ step: Object });
const { addPdf } = useBriefStore(); const { addPdf } = useBriefStore();
const router = useRouter(); const router = useRouter();
const { t } = useI18n();
const images = computed(() => { const images = computed(() => {
return step.files.filter((file) => file.type === "image"); return step.files.filter((file) => file.type === "image");

View file

@ -25,7 +25,7 @@
v-if="pdf.comments?.length > 0" v-if="pdf.comments?.length > 0"
class="order-last | text-sm text-primary font-medium" class="order-last | text-sm text-primary font-medium"
> >
<router-link :to="'/' + step.uri + '&comments=true'"> <router-link :to="addLocalePrefix(step.uri) + '&comments=true'">
{{ pdf.comments.length }} commentaire{{ {{ pdf.comments.length }} commentaire{{
pdf.comments.length > 1 ? "s" : "" pdf.comments.length > 1 ? "s" : ""
}} }}
@ -39,6 +39,7 @@ import { useRoute } from "vue-router";
import DateTime from "./DateTime.vue"; import DateTime from "./DateTime.vue";
import { computed } from "vue"; import { computed } from "vue";
import { useDesignToLightStore } from "../../../stores/designToLight"; import { useDesignToLightStore } from "../../../stores/designToLight";
import { addLocalePrefix } from "../../../utils/router";
const { step, pdf, index } = defineProps({ const { step, pdf, index } = defineProps({
step: Object, step: Object,

View file

@ -2,7 +2,7 @@
<article class="card"> <article class="card">
<hgroup class="order-last"> <hgroup class="order-last">
<h3 class="card__title | font-serif | text-lg"> <h3 class="card__title | font-serif | text-lg">
<router-link :to="uri" class="link-full">{{ step.label }}</router-link> <router-link :to="uri" class="link-full">{{ t('steps.' + step.id) }}</router-link>
</h3> </h3>
</hgroup> </hgroup>
<DateTime :date="step.modified" /> <DateTime :date="step.modified" />
@ -42,6 +42,7 @@ import DateTime from './DateTime.vue';
import { useDesignToLightStore } from '../../../stores/designToLight'; import { useDesignToLightStore } from '../../../stores/designToLight';
import { useVirtualSampleStore } from '../../../stores/virtualSample'; import { useVirtualSampleStore } from '../../../stores/virtualSample';
import { computed } from 'vue'; import { computed } from 'vue';
import { useI18n } from 'vue-i18n';
const { images, step, uri } = defineProps({ const { images, step, uri } = defineProps({
images: Array, images: Array,
@ -51,6 +52,7 @@ const { images, step, uri } = defineProps({
const { isDesignToLightStep } = useDesignToLightStore(); const { isDesignToLightStep } = useDesignToLightStore();
const { allVariations } = useVirtualSampleStore(); const { allVariations } = useVirtualSampleStore();
const { t } = useI18n();
const commentsCount = computed(() => { const commentsCount = computed(() => {
let count = 0; let count = 0;

View file

@ -5,7 +5,7 @@
:style="'--cover: url(' + step.cover + ')'" :style="'--cover: url(' + step.cover + ')'"
> >
<h3 class="text-lg font-serif"> <h3 class="text-lg font-serif">
<router-link :to="'/' + step.uri" class="link-full">{{ <router-link :to="addLocalePrefix(step.uri)" class="link-full">{{
step.title step.title
}}</router-link> }}</router-link>
</h3> </h3>
@ -27,6 +27,7 @@
<script setup> <script setup>
import dayjs from "dayjs"; import dayjs from "dayjs";
import "dayjs/locale/fr"; import "dayjs/locale/fr";
import { addLocalePrefix } from "../../../utils/router";
const { step } = defineProps({ step: Object }); const { step } = defineProps({ step: Object });
</script> </script>

View file

@ -11,6 +11,7 @@ import Images from './Images.vue';
import { computed } from 'vue'; import { computed } from 'vue';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useVirtualSampleStore } from '../../../stores/virtualSample'; import { useVirtualSampleStore } from '../../../stores/virtualSample';
import { addLocalePrefix } from '../../../utils/router';
const { step } = defineProps({ step: Object }); const { step } = defineProps({ step: Object });
@ -27,7 +28,7 @@ const images = computed(() => {
return allVariations.value.map((variation) => getFrontView(variation)) ?? []; return allVariations.value.map((variation) => getFrontView(variation)) ?? [];
}); });
const uri = '/' + step.uri; const uri = addLocalePrefix(step.uri);
function getFrontView(variation) { function getFrontView(variation) {
if (variation.files.length === 1) return variation.files[0]; if (variation.files.length === 1) return variation.files[0];

View file

@ -19,8 +19,8 @@
> >
<span>{{ <span>{{
isCompareModeEnabled isCompareModeEnabled
? 'Quitter le mode comparer' ? t('buttons.exitCompare')
: 'Comparer les pistes' : t('buttons.compareTracks')
}}</span> }}</span>
</button> </button>
</header> </header>
@ -41,7 +41,7 @@
:backgroundColor="activeTrack.backgroundColor" :backgroundColor="activeTrack.backgroundColor"
/> />
<div v-else class="track-empty | bg-white rounded-xl w-full p-32"> <div v-else class="track-empty | bg-white rounded-xl w-full p-32">
<p>Contenu non disponible pour cette piste</p> <p>{{ t('virtualSample.noContent') }}</p>
</div> </div>
</template> </template>
@ -49,7 +49,7 @@
v-if="isCompareModeEnabled && activeTracks.length < 2" v-if="isCompareModeEnabled && activeTracks.length < 2"
class="track-empty | bg-white rounded-xl w-full p-32" class="track-empty | bg-white rounded-xl w-full p-32"
> >
<p>Sélectionnez sur la piste que vous souhaitez comparer</p> <p>{{ t('virtualSample.selectToCompare') }}</p>
</div> </div>
</div> </div>
</div> </div>
@ -62,11 +62,14 @@ import { usePageStore } from '../../../stores/page';
import { useDialogStore } from '../../../stores/dialog'; import { useDialogStore } from '../../../stores/dialog';
import { useVirtualSampleStore } from '../../../stores/virtualSample'; import { useVirtualSampleStore } from '../../../stores/virtualSample';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import Interactive360 from './Interactive360.vue'; import Interactive360 from './Interactive360.vue';
import SingleImage from './SingleImage.vue'; import SingleImage from './SingleImage.vue';
import Selector from '../../Selector.vue'; import Selector from '../../Selector.vue';
import slugify from 'slugify'; import slugify from 'slugify';
const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();

View file

@ -5,7 +5,7 @@
modal modal
:draggable="false" :draggable="false"
:dismissableMask="true" :dismissableMask="true"
header="Titre du rendu" :header="t('dialogs.renderTitle')"
class="dialog" class="dialog"
:class="[ :class="[
{ 'with-comments': isCommentsOpen }, { 'with-comments': isCommentsOpen },
@ -26,7 +26,7 @@
aria-controls="dynamic" aria-controls="dynamic"
@click="activeTab = 'dynamic'" @click="activeTab = 'dynamic'"
> >
<span>Présentation dynamique</span> <span>{{ t('virtualSample.dynamicPresentation') }}</span>
</button> </button>
<button <button
v-if="step.files.static" v-if="step.files.static"
@ -38,10 +38,10 @@
:aria-pressed="activeTab === 'static' ? true : false" :aria-pressed="activeTab === 'static' ? true : false"
aria-controls="static" aria-controls="static"
> >
<span>Vue statique</span> <span>{{ t('virtualSample.staticView') }}</span>
</button> </button>
</div> </div>
<h2 class="font-serif text-lg">Échantillon virtuel</h2> <h2 class="font-serif text-lg">{{ t('virtualSample.title') }}</h2>
</template> </template>
<DynamicView id="dynamic" v-if="activeTab === 'dynamic'" /> <DynamicView id="dynamic" v-if="activeTab === 'dynamic'" />
@ -66,8 +66,8 @@
> >
<span>{{ <span>{{
!isLoopAnimationEnabled !isLoopAnimationEnabled
? 'Animation en boucle' ? t('buttons.loopAnimation')
: 'Arrêter lanimation' : t('buttons.stopAnimation')
}}</span> }}</span>
</button> </button>
<button <button
@ -79,7 +79,7 @@
@click="isCommentsOpen = !isCommentsOpen" @click="isCommentsOpen = !isCommentsOpen"
> >
<span class="sr-only" <span class="sr-only"
>{{ isCommentsOpen ? 'Masquer' : 'Afficher' }} les commentaires</span >{{ isCommentsOpen ? t('buttons.hideComments') : t('buttons.showComments') }}</span
> >
</button> </button>
</template> </template>
@ -103,7 +103,9 @@ import { useVirtualSampleStore } from '../../../stores/virtualSample';
import { useDialogStore } from '../../../stores/dialog'; import { useDialogStore } from '../../../stores/dialog';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { usePageStore } from '../../../stores/page'; import { usePageStore } from '../../../stores/page';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { file } = defineProps({ const { file } = defineProps({
file: Object, file: Object,
}); });
@ -141,12 +143,12 @@ watch(isOpen, (newValue) => {
const downloadText = computed(() => { const downloadText = computed(() => {
if (activeTab.value === 'dynamic') { if (activeTab.value === 'dynamic') {
if (activeTracks.value.length === 1) { if (activeTracks.value.length === 1) {
return "Télécharger l'image"; return t('buttons.downloadImage');
} else { } else {
return 'Télécharger les images'; return t('buttons.downloadImages');
} }
} else { } else {
return 'Télécharger le PDF'; return t('buttons.downloadPdf');
} }
}); });

198
src/locales/en.json Normal file
View file

@ -0,0 +1,198 @@
{
"menu": {
"home": "Home",
"notifications": "Notifications",
"meetings": "Meetings",
"designToLight": "Design to Light",
"inspirations": "Inspirations",
"profile": "Profile",
"logout": "Logout",
"currentProjects": "Current projects",
"archivedProjects": "Archived projects",
"news": "New",
"show": "Show menu",
"hide": "Hide menu",
"newModifications": "New modifications"
},
"steps": {
"clientBrief": "Client brief",
"proposal": "Commercial proposal",
"extendedBrief": "Extended brief",
"industrialIdeation": "Industrial ideation",
"virtualSample": "Virtual sample",
"physicalSample": "Physical sample"
},
"dtl": {
"dynamicTrack": "Virtual sample - dynamic track",
"staticTrack": "Virtual sample - static track",
"title": "Design to Light",
"grade": "Design to Light: {grade}",
"globalScore": "Global score",
"positioning": "Positioning",
"design": "Design",
"weight": "Weight",
"indicators": "Component indicators",
"requestOptimization": "Request optimization expertise",
"requestPending": "Expertise request being processed…",
"initialProposal": "Initial proposal",
"alternative": "Alternative {index}",
"proposalBasedOn": "Data based on the proposal"
},
"buttons": {
"edit": "Edit",
"cancel": "Cancel",
"save": "Save",
"delete": "Delete",
"submit": "Submit",
"retry": "Retry",
"add": "Add",
"close": "Close",
"validate": "Validate and send the brief",
"validateMinimum": "Add at least one image",
"showPassword": "Show password",
"hidePassword": "Hide password",
"showProject": "Show project",
"hideProject": "Hide project",
"showComments": "Show comments",
"hideComments": "Hide comments",
"backToProject": "Back to project",
"backToList": "Back to list",
"addComment": "Add a comment",
"reply": "Reply…",
"seeAll": "See all",
"deleteImage": "Delete this image",
"downloadImage": "Download image",
"downloadImages": "Download images",
"downloadPdf": "Download PDF",
"update": "Update",
"requestMeeting": "Request a meeting",
"markAllAsRead": "Mark all as read",
"loopAnimation": "Loop animation",
"stopAnimation": "Stop animation",
"compareTracks": "Compare tracks",
"exitCompare": "Exit compare mode"
},
"forms": {
"email": "Email",
"emailPlaceholder": "email@example.com",
"password": "Password",
"passwordPlaceholder": "Minimum 8 characters",
"newPassword": "New password",
"confirmPassword": "Confirm new password…",
"projectName": "Project name",
"projectDetails": "Project details",
"projectDetailsPlaceholder": "Project details…",
"meetingSubject": "Meeting subject",
"meetingDetails": "Project details",
"meetingDetailsPlaceholder": "Describe your request…",
"description": "Project description",
"descriptionPlaceholder": "Add a general description of your project…",
"imageDescription": "Image description",
"imageDescriptionPlaceholder": "Add a description to the image…",
"commentPlaceholder": "Add a comment…",
"filterByTags": "Filter by tags",
"tags": "Tags",
"selectVariation": "Select a variation",
"uploadedFiles": "Uploaded files",
"addImages": "Add one or more images"
},
"auth": {
"login": "Login",
"passwordShown": "Password shown",
"passwordHidden": "Password hidden",
"fillFields": "Please fill in the fields.",
"inProgress": "In progress…",
"updateSuccess": "Update successful",
"updateInProgress": "in progress…"
},
"account": {
"comments": "Comments",
"noClient": "No associated client",
"managedProjects": "Managed projects",
"myProjects": "My projects",
"currentStep": "Current step:",
"projectCount": "Number of projects",
"clientLogo": "{clientName} logo"
},
"brief": {
"addPlatform": "Add a brief via the platform",
"addPdf": "Add a PDF brief",
"projects": "Projects"
},
"comments": {
"title": "Comments",
"your": "Your comment",
"new": "New comment",
"newInstruction": "In the content area, click where you want to position the comment",
"reply": "reply",
"replies": "replies",
"edit": "Edit",
"inProgress": "In progress",
"emptyMessage": "Share your ideas by adding comments"
},
"dates": {
"today": "Today",
"yesterday": "yesterday",
"updatedOn": "Last updated on"
},
"projects": {
"none": "No projects at the moment",
"current": "Current projects",
"archived": "Archived projects"
},
"notifications": {
"title": "Notifications",
"none": "You have no new notifications",
"noneUnread": "You have no unread notifications",
"all": "All",
"unread": "Unread",
"projectRequest": "Project creation request",
"meetingRequest": "Meeting request",
"content": "Content",
"draft": "(draft)",
"from": "From",
"author": "Author:",
"goToContent": "Go to content"
},
"meetings": {
"none": "You have no scheduled meetings",
"upcoming": "Upcoming",
"past": "Past"
},
"inspirations": {
"title": "Inspirations",
"favorites": "My Favorites",
"addToFavorites": "Add to favorites",
"removeFromFavorites": "Remove from favorites",
"new": "New"
},
"dialogs": {
"requestProject": "Request project creation",
"requestMeeting": "Request a meeting",
"imageDetails": "Image details",
"addImages": "Add images",
"deleteConfirm": "Are you sure you want to delete this image?",
"deleteWarning": "If you delete this image, it will disappear from your brief along with all the information attributed to it.",
"pdfTitle": "PDF title",
"renderTitle": "Render title",
"createWithDTL": "Create with Design to Light",
"learnMore": "Learn more",
"dtlDescription": "Discover the environmental score of your project..."
},
"virtualSample": {
"title": "Virtual sample",
"dynamicPresentation": "Dynamic presentation",
"staticView": "Static view",
"noContent": "Content not available for this track",
"selectToCompare": "Select the track you want to compare"
},
"errors": {
"saveFailed": "Save failed",
"deleteFailed": "Delete failed",
"toggleProjectFailed": "Failed to show/hide project",
"toggleFavoriteFailed": "Failed to toggle favorite",
"readNotificationFailed": "Failed to read notification:",
"readAllNotificationsFailed": "Could not read all notifications",
"markNotificationFailed": "Could not mark notification as read"
}
}

198
src/locales/fr.json Normal file
View file

@ -0,0 +1,198 @@
{
"menu": {
"home": "Home",
"notifications": "Notifications",
"meetings": "Réunions",
"designToLight": "Design to Light",
"inspirations": "Inspirations",
"profile": "Profil",
"logout": "Déconnexion",
"currentProjects": "Projets en cours",
"archivedProjects": "Projets archivés",
"news": "Nouveautés",
"show": "Afficher le menu",
"hide": "Masquer le menu",
"newModifications": "Nouvelles modifications"
},
"steps": {
"clientBrief": "Brief client",
"proposal": "Proposition commerciale",
"extendedBrief": "Brief enrichi",
"industrialIdeation": "Idéation industrielle",
"virtualSample": "Échantillon virtuel",
"physicalSample": "Échantillon physique"
},
"dtl": {
"dynamicTrack": "Échantillon virtuel - piste dynamique",
"staticTrack": "Échantillon virtuel - piste statique",
"title": "Design to Light",
"grade": "Design to Light: {grade}",
"globalScore": "Note globale",
"positioning": "Positionnement",
"design": "Design",
"weight": "Poids",
"indicators": "Indicateur des composants impliqués",
"requestOptimization": "Demander une expertise d'optimisation",
"requestPending": "Demande d'expertise en cours de traitement…",
"initialProposal": "Proposition initiale",
"alternative": "Alternative {index}",
"proposalBasedOn": "Données basées sur la proposition"
},
"buttons": {
"edit": "Modifier",
"cancel": "Annuler",
"save": "Sauvegarder",
"delete": "Supprimer",
"submit": "Soumettre",
"retry": "Réessayer",
"add": "Ajouter",
"close": "Fermer",
"validate": "Valider et envoyer le brief",
"validateMinimum": "Ajoutez au moins une image",
"showPassword": "Afficher le mot de passe",
"hidePassword": "Masquer le mot de passe",
"showProject": "Afficher le projet",
"hideProject": "Masquer le projet",
"showComments": "Afficher les commentaires",
"hideComments": "Masquer les commentaires",
"backToProject": "Retour au projet",
"backToList": "Retour à la liste",
"addComment": "Ajouter un commentaire",
"reply": "Répondre…",
"seeAll": "Voir tout",
"deleteImage": "Supprimer cette image",
"downloadImage": "Télécharger l'image",
"downloadImages": "Télécharger les images",
"downloadPdf": "Télécharger le PDF",
"update": "Mettre à jour",
"requestMeeting": "Demander un RDV",
"markAllAsRead": "Marquer tout comme lu",
"loopAnimation": "Animation en boucle",
"stopAnimation": "Arrêter l'animation",
"compareTracks": "Comparer les pistes",
"exitCompare": "Quitter le mode comparer"
},
"forms": {
"email": "Email",
"emailPlaceholder": "mail@exemple.com",
"password": "Mot de passe",
"passwordPlaceholder": "Minimum 8 caractères",
"newPassword": "Nouveau mot de passe",
"confirmPassword": "Confirmez le nouveau mot de passe…",
"projectName": "Nom du projet",
"projectDetails": "Détails du projet",
"projectDetailsPlaceholder": "Détails du projet…",
"meetingSubject": "Objet du rendez-vous",
"meetingDetails": "Détails du projet",
"meetingDetailsPlaceholder": "Décrivez votre demande…",
"description": "Description du projet",
"descriptionPlaceholder": "Ajoutez une description générale de votre projet…",
"imageDescription": "Description de l'image",
"imageDescriptionPlaceholder": "Ajoutez une description à l'image…",
"commentPlaceholder": "Ajouter un commentaire…",
"filterByTags": "Filtrer par tags",
"tags": "Tags",
"selectVariation": "Sélectionnez une déclinaison",
"uploadedFiles": "Fichiers importés",
"addImages": "Ajouter une ou plusieurs images"
},
"auth": {
"login": "Connexion",
"passwordShown": "Mot de passe affiché",
"passwordHidden": "Mot de passe masqué",
"fillFields": "Veuillez remplir les champs.",
"inProgress": "En cours…",
"updateSuccess": "Mise à jour réussie",
"updateInProgress": "en cours…"
},
"account": {
"comments": "Commentaires",
"noClient": "Pas de client associé",
"managedProjects": "Projets managés",
"myProjects": "Mes projets",
"currentStep": "Étape en cours :",
"projectCount": "Nombre de projets",
"clientLogo": "logo {clientName}"
},
"brief": {
"addPlatform": "Ajouter un brief via la plateforme",
"addPdf": "Ajouter un brief PDF",
"projects": "Projets"
},
"comments": {
"title": "Commentaires",
"your": "Votre commentaire",
"new": "Nouveau commentaire",
"newInstruction": "Dans la zone du contenu, cliquez où vous souhaitez positionner le commentaire",
"reply": "réponse",
"replies": "réponses",
"edit": "Éditer",
"inProgress": "En cours",
"emptyMessage": "Partagez vos idées en ajoutant des commentaires"
},
"dates": {
"today": "Aujourd'hui",
"yesterday": "hier",
"updatedOn": "Dernière mise à jour le"
},
"projects": {
"none": "Aucun projet pour le moment",
"current": "Projets en cours",
"archived": "Projets archivés"
},
"notifications": {
"title": "Notifications",
"none": "Vous n'avez pas de nouvelles notifications",
"noneUnread": "Vous n'avez pas de notifications non lues",
"all": "Tout",
"unread": "Non lu",
"projectRequest": "Demande de création de projet",
"meetingRequest": "Demande de rendez-vous",
"content": "Contenu",
"draft": "(brouillon)",
"from": "De la part de",
"author": "Auteur :",
"goToContent": "Aller au contenu"
},
"meetings": {
"none": "Vous n'avez aucune réunion programmée",
"upcoming": "À venir",
"past": "Passées"
},
"inspirations": {
"title": "Les Inspirations",
"favorites": "Mes Favoris",
"addToFavorites": "Ajouter aux favoris",
"removeFromFavorites": "Retirer des favoris",
"new": "Nouveauté"
},
"dialogs": {
"requestProject": "Demander la création d'un projet",
"requestMeeting": "Demander un rendez-vous",
"imageDetails": "Détails de l'image",
"addImages": "Ajouter des images",
"deleteConfirm": "Êtes-vous sûr de vouloir supprimer cette image ?",
"deleteWarning": "Si vous supprimez cette image, celle-ci disparaîtra de votre brief ainsi que toutes les informations qui lui sont attribuées.",
"pdfTitle": "Titre du PDF",
"renderTitle": "Titre du rendu",
"createWithDTL": "Créer avec Design to Light",
"learnMore": "En savoir plus",
"dtlDescription": "Découvrez la note environnementale de votre projet..."
},
"virtualSample": {
"title": "Échantillon virtuel",
"dynamicPresentation": "Présentation dynamique",
"staticView": "Vue statique",
"noContent": "Contenu non disponible pour cette piste",
"selectToCompare": "Sélectionnez sur la piste que vous souhaitez comparer"
},
"errors": {
"saveFailed": "Erreur lors de la sauvegarde",
"deleteFailed": "Erreur lors de la suppression",
"toggleProjectFailed": "Erreur lors du masquage/affichage du projet",
"toggleFavoriteFailed": "Failed to toggle favorite",
"readNotificationFailed": "Erreur lors de la lecture de la notification :",
"readAllNotificationsFailed": "Could not read all notifications",
"markNotificationFailed": "Could not mark notification as read"
}
}

View file

@ -6,12 +6,30 @@ import PrimeVue from 'primevue/config';
import ToastService from 'primevue/toastservice'; import ToastService from 'primevue/toastservice';
import Select from 'primevue/select'; import Select from 'primevue/select';
import MultiSelect from 'primevue/multiselect'; import MultiSelect from 'primevue/multiselect';
import { router } from './router/router.js'; import { router, setI18nLocale } from './router/router.js';
import { createI18n } from 'vue-i18n';
import fr from './locales/fr.json';
import en from './locales/en.json';
import { setI18nInstance } from './stores/locale';
const i18n = createI18n({
legacy: false,
locale: 'fr',
fallbackLocale: 'fr',
messages: {
fr,
en,
},
});
// Donner l'instance i18n au store locale pour synchronisation immédiate
setI18nInstance(i18n);
const app = createApp(App); const app = createApp(App);
const pinia = createPinia(); const pinia = createPinia();
app.use(pinia); app.use(pinia);
app.use(i18n);
app.use(PrimeVue, { app.use(PrimeVue, {
unstyled: true, unstyled: true,
}); });
@ -19,4 +37,7 @@ app.use(ToastService);
app.use(router); app.use(router);
app.component('Select', Select); app.component('Select', Select);
app.component('MultiSelect', MultiSelect); app.component('MultiSelect', MultiSelect);
setI18nLocale(i18n);
app.mount('#app'); app.mount('#app');

View file

@ -3,6 +3,7 @@ import routes from './routes';
import { useApiStore } from '../stores/api'; import { useApiStore } from '../stores/api';
import { usePageStore } from '../stores/page'; import { usePageStore } from '../stores/page';
import { useUserStore } from '../stores/user'; import { useUserStore } from '../stores/user';
import { useLocaleStore } from '../stores/locale';
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),
@ -12,13 +13,23 @@ const router = createRouter({
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
const pageStore = usePageStore(); const pageStore = usePageStore();
const userStore = useUserStore(); const userStore = useUserStore();
const localeStore = useLocaleStore();
const urlLocale = to.params.locale === 'en' ? 'en' : 'fr';
localeStore.initLocale(urlLocale);
let apiPath = to.path;
if (urlLocale === 'en' && apiPath.startsWith('/en')) {
apiPath = apiPath.replace(/^\/en/, '') || '/';
}
const api = useApiStore(); const api = useApiStore();
try { try {
const res = await api.fetchData(to.path); const res = await api.fetchData(apiPath);
if (to.path === '/login' && res.user) { const loginPath = urlLocale === 'en' ? '/en/login' : '/login';
location.href = '/'; if (to.path === loginPath && res.user) {
location.href = urlLocale === 'en' ? '/en' : '/';
} }
pageStore.page = res.page; pageStore.page = res.page;
@ -30,4 +41,13 @@ router.beforeEach(async (to, from, next) => {
} }
}); });
export function setI18nLocale(i18n) {
router.afterEach(() => {
const localeStore = useLocaleStore();
if (i18n.global.locale.value !== localeStore.currentLocale) {
i18n.global.locale.value = localeStore.currentLocale;
}
});
}
export { router }; export { router };

View file

@ -10,53 +10,55 @@ import Account from '../views/Account.vue';
const routes = [ const routes = [
{ {
path: '/', path: '/:locale?',
component: Home, component: Home,
}, },
{ {
name: 'Login', name: 'Login',
path: '/login', path: '/:locale?/login',
component: Login, component: Login,
}, },
{ {
name: 'Account', name: 'Account',
path: '/account', path: '/:locale?/account',
component: Account, component: Account,
}, },
{ {
path: '/notifications', path: '/:locale?/notifications',
component: Notifications, component: Notifications,
}, },
{ {
path: '/reunions', path: '/:locale?/reunions',
component: Reunions, component: Reunions,
}, },
{ {
path: '/inspirations', path: '/:locale?/inspirations',
component: Inspirations, component: Inspirations,
}, },
{ {
path: '/design-to-light', path: '/:locale?/design-to-light',
component: DesignToLight, component: DesignToLight,
}, },
{ {
path: '/projects/:id', path: '/:locale?/projects/:id',
component: Kanban, component: Kanban,
}, },
{ {
path: '/projects/:id/client-brief', path: '/:locale?/projects/:id/client-brief',
component: Brief, component: Brief,
}, },
{ {
path: '/projects/:id/extended-brief', path: '/:locale?/projects/:id/extended-brief',
component: Brief, component: Brief,
}, },
// Redirections // Redirections
{ {
path: '/projects/:id/industrial-ideation', path: '/:locale?/projects/:id/industrial-ideation',
redirect: (to) => { redirect: (to) => {
const prefix = to.params.locale === 'en' ? '/en' : '';
return ( return (
prefix +
'/projects/' + '/projects/' +
to.params.id + to.params.id +
'?dialog=industrial-ideation&comments=true' '?dialog=industrial-ideation&comments=true'
@ -64,9 +66,10 @@ const routes = [
}, },
}, },
{ {
path: '/projects/:id/proposal', path: '/:locale?/projects/:id/proposal',
redirect: (to) => { redirect: (to) => {
return '/projects/' + to.params.id + '?dialog=proposal&comments=true'; const prefix = to.params.locale === 'en' ? '/en' : '';
return prefix + '/projects/' + to.params.id + '?dialog=proposal&comments=true';
}, },
}, },
]; ];

61
src/stores/locale.js Normal file
View file

@ -0,0 +1,61 @@
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
let i18nInstance = null;
export function setI18nInstance(i18n) {
i18nInstance = i18n;
}
export const useLocaleStore = defineStore('locale', () => {
const currentLocale = ref('fr');
const isEnglish = computed(() => currentLocale.value === 'en');
const isFrench = computed(() => currentLocale.value === 'fr');
function setLocale(locale) {
if (locale !== 'fr' && locale !== 'en') {
console.warn(`Invalid locale: ${locale}, defaulting to 'fr'`);
locale = 'fr';
}
currentLocale.value = locale;
localStorage.setItem('locale', locale);
document.documentElement.lang = locale;
// Synchroniser i18n immédiatement
if (i18nInstance) {
i18nInstance.global.locale.value = locale;
}
}
function detectUserLocale() {
const browserLang = navigator.language.toLowerCase();
return browserLang.startsWith('en') ? 'en' : 'fr';
}
function initLocale(urlLocale = null) {
let locale = 'fr';
if (urlLocale && (urlLocale === 'en' || urlLocale === 'fr')) {
locale = urlLocale;
} else {
const storedLocale = localStorage.getItem('locale');
if (storedLocale === 'en' || storedLocale === 'fr') {
locale = storedLocale;
} else {
locale = detectUserLocale();
}
}
setLocale(locale);
}
return {
currentLocale,
isEnglish,
isFrench,
setLocale,
detectUserLocale,
initLocale,
};
});

43
src/utils/router.js Normal file
View file

@ -0,0 +1,43 @@
import { useLocaleStore } from '../stores/locale';
export function localeRoute(path) {
const localeStore = useLocaleStore();
const locale = localeStore.currentLocale;
if (locale === 'en') {
return path.startsWith('/en') ? path : `/en${path}`;
}
return path.startsWith('/en') ? path.replace(/^\/en/, '') || '/' : path;
}
export function addLocalePrefix(path) {
const localeStore = useLocaleStore();
const locale = localeStore.currentLocale;
// S'assurer que le path commence par /
if (!path.startsWith('/')) {
path = '/' + path;
}
// Ajouter /en si locale anglaise et pas déjà présent
if (locale === 'en' && !path.startsWith('/en')) {
return '/en' + path;
}
// Enlever /en si locale française
if (locale === 'fr' && path.startsWith('/en')) {
return path.replace(/^\/en/, '') || '/';
}
return path;
}
export function removeLocalePrefix(path) {
return path.replace(/^\/(en|fr)/, '') || '/';
}
export function getLocaleFromPath(path) {
const match = path.match(/^\/(en|fr)/);
return match ? match[1] : 'fr';
}

View file

@ -20,13 +20,13 @@
role="group" role="group"
aria-labelledby="username" aria-labelledby="username"
> >
<label for="username" class="text-grey-700">Email</label> <label for="username" class="text-grey-700">{{ t('forms.email') }}</label>
<input <input
v-if="isEditingEmail" v-if="isEditingEmail"
type="email" type="email"
v-model="email" v-model="email"
id="username" id="username"
placeholder="mail@exemple.com" :placeholder="t('forms.emailPlaceholder')"
autocomplete="username" autocomplete="username"
class="w-full rounded-md border border-grey-200 px-16 py-12 mt-8" class="w-full rounded-md border border-grey-200 px-16 py-12 mt-8"
:class="{ invalid: !isEmailValid }" :class="{ invalid: !isEmailValid }"
@ -55,14 +55,14 @@
@click="isEditingEmail = true" @click="isEditingEmail = true"
class="btn | w-full text-md" class="btn | w-full text-md"
> >
Modifier {{ t('buttons.edit') }}
</button> </button>
<button <button
v-if="isEditingEmail" v-if="isEditingEmail"
class="btn btn--secondary | w-full text-md" class="btn btn--secondary | w-full text-md"
@click="isEditingEmail = false" @click="isEditingEmail = false"
> >
Annuler {{ t('buttons.cancel') }}
</button> </button>
</div> </div>
</section> </section>
@ -72,17 +72,15 @@
class="bg-white rounded-2xl px-16 py-24" class="bg-white rounded-2xl px-16 py-24"
aria-labelledby="password-label" aria-labelledby="password-label"
> >
<h2 class="sr-only" id="password-label">Mot de passe</h2> <h2 class="sr-only" id="password-label">{{ t('forms.password') }}</h2>
<div class="flow"> <div class="flow">
<div class="field | w-full" role="group" aria-labelledby="password"> <div class="field | w-full" role="group" aria-labelledby="password">
<label for="password" class="text-grey-700" <label for="password" class="text-grey-700">{{ t('forms.newPassword') }}</label>
>Nouveau mot de passe</label
>
<input <input
:type="isPasswordVisible ? 'text' : 'password'" :type="isPasswordVisible ? 'text' : 'password'"
v-model="password" v-model="password"
id="password" id="password"
placeholder="Minimum 8 caractères" :placeholder="t('forms.passwordPlaceholder')"
autocomplete="current-password" autocomplete="current-password"
autocapitalize="none" autocapitalize="none"
autocorrect="off" autocorrect="off"
@ -97,11 +95,7 @@
:aria-pressed="isPasswordVisible ? 'true' : 'false'" :aria-pressed="isPasswordVisible ? 'true' : 'false'"
aria-controls="password" aria-controls="password"
@click="isPasswordVisible = !isPasswordVisible" @click="isPasswordVisible = !isPasswordVisible"
:title=" :title="isPasswordVisible ? t('buttons.hidePassword') : t('buttons.showPassword')"
isPasswordVisible
? 'Masquer le mot de passe'
: 'Afficher le mot de passe'
"
> >
<svg <svg
aria-hidden="true" aria-hidden="true"
@ -121,11 +115,7 @@
d="M17.8827 19.2968C16.1814 20.3755 14.1638 21.0002 12.0003 21.0002C6.60812 21.0002 2.12215 17.1204 1.18164 12.0002C1.61832 9.62282 2.81932 7.5129 4.52047 5.93457L1.39366 2.80777L2.80788 1.39355L22.6069 21.1925L21.1927 22.6068L17.8827 19.2968ZM5.9356 7.3497C4.60673 8.56015 3.6378 10.1672 3.22278 12.0002C4.14022 16.0521 7.7646 19.0002 12.0003 19.0002C13.5997 19.0002 15.112 18.5798 16.4243 17.8384L14.396 15.8101C13.7023 16.2472 12.8808 16.5002 12.0003 16.5002C9.51498 16.5002 7.50026 14.4854 7.50026 12.0002C7.50026 11.1196 7.75317 10.2981 8.19031 9.60442L5.9356 7.3497ZM12.9139 14.328L9.67246 11.0866C9.5613 11.3696 9.50026 11.6777 9.50026 12.0002C9.50026 13.3809 10.6196 14.5002 12.0003 14.5002C12.3227 14.5002 12.6309 14.4391 12.9139 14.328ZM20.8068 16.5925L19.376 15.1617C20.0319 14.2268 20.5154 13.1586 20.7777 12.0002C19.8603 7.94818 16.2359 5.00016 12.0003 5.00016C11.1544 5.00016 10.3329 5.11773 9.55249 5.33818L7.97446 3.76015C9.22127 3.26959 10.5793 3.00016 12.0003 3.00016C17.3924 3.00016 21.8784 6.87992 22.8189 12.0002C22.5067 13.6998 21.8038 15.2628 20.8068 16.5925ZM11.7229 7.50857C11.8146 7.50299 11.9071 7.50016 12.0003 7.50016C14.4855 7.50016 16.5003 9.51488 16.5003 12.0002C16.5003 12.0933 16.4974 12.1858 16.4919 12.2775L11.7229 7.50857Z" d="M17.8827 19.2968C16.1814 20.3755 14.1638 21.0002 12.0003 21.0002C6.60812 21.0002 2.12215 17.1204 1.18164 12.0002C1.61832 9.62282 2.81932 7.5129 4.52047 5.93457L1.39366 2.80777L2.80788 1.39355L22.6069 21.1925L21.1927 22.6068L17.8827 19.2968ZM5.9356 7.3497C4.60673 8.56015 3.6378 10.1672 3.22278 12.0002C4.14022 16.0521 7.7646 19.0002 12.0003 19.0002C13.5997 19.0002 15.112 18.5798 16.4243 17.8384L14.396 15.8101C13.7023 16.2472 12.8808 16.5002 12.0003 16.5002C9.51498 16.5002 7.50026 14.4854 7.50026 12.0002C7.50026 11.1196 7.75317 10.2981 8.19031 9.60442L5.9356 7.3497ZM12.9139 14.328L9.67246 11.0866C9.5613 11.3696 9.50026 11.6777 9.50026 12.0002C9.50026 13.3809 10.6196 14.5002 12.0003 14.5002C12.3227 14.5002 12.6309 14.4391 12.9139 14.328ZM20.8068 16.5925L19.376 15.1617C20.0319 14.2268 20.5154 13.1586 20.7777 12.0002C19.8603 7.94818 16.2359 5.00016 12.0003 5.00016C11.1544 5.00016 10.3329 5.11773 9.55249 5.33818L7.97446 3.76015C9.22127 3.26959 10.5793 3.00016 12.0003 3.00016C17.3924 3.00016 21.8784 6.87992 22.8189 12.0002C22.5067 13.6998 21.8038 15.2628 20.8068 16.5925ZM11.7229 7.50857C11.8146 7.50299 11.9071 7.50016 12.0003 7.50016C14.4855 7.50016 16.5003 9.51488 16.5003 12.0002C16.5003 12.0933 16.4974 12.1858 16.4919 12.2775L11.7229 7.50857Z"
></path> ></path>
</svg> </svg>
<span class="sr-only">{{ <span class="sr-only">{{ isPasswordVisible ? t('buttons.hidePassword') : t('buttons.showPassword') }}</span>
isPasswordVisible
? 'Masquer le mot de passe'
: 'Afficher le mot de passe'
}}</span>
</button> </button>
</div> </div>
<div <div
@ -133,14 +123,12 @@
role="group" role="group"
aria-labelledby="password-confirm" aria-labelledby="password-confirm"
> >
<label for="passwordConfirm" class="text-grey-700" <label for="passwordConfirm" class="text-grey-700">{{ t('forms.confirmPassword') }}</label>
>Confirmez le nouveau mot de passe</label
>
<input <input
:type="isPasswordVisible ? 'text' : 'password'" :type="isPasswordVisible ? 'text' : 'password'"
v-model="passwordConfirm" v-model="passwordConfirm"
id="password-confirm" id="password-confirm"
placeholder="Minimum 8 caractères" :placeholder="t('forms.passwordPlaceholder')"
autocomplete="current-password" autocomplete="current-password"
autocapitalize="none" autocapitalize="none"
autocorrect="off" autocorrect="off"
@ -155,11 +143,7 @@
:aria-pressed="isPasswordVisible ? 'true' : 'false'" :aria-pressed="isPasswordVisible ? 'true' : 'false'"
aria-controls="password-confirm" aria-controls="password-confirm"
@click="isPasswordVisible = !isPasswordVisible" @click="isPasswordVisible = !isPasswordVisible"
:title=" :title="isPasswordVisible ? t('buttons.hidePassword') : t('buttons.showPassword')"
isPasswordVisible
? 'Masquer le mot de passe'
: 'Afficher le mot de passe'
"
> >
<svg <svg
aria-hidden="true" aria-hidden="true"
@ -179,11 +163,7 @@
d="M17.8827 19.2968C16.1814 20.3755 14.1638 21.0002 12.0003 21.0002C6.60812 21.0002 2.12215 17.1204 1.18164 12.0002C1.61832 9.62282 2.81932 7.5129 4.52047 5.93457L1.39366 2.80777L2.80788 1.39355L22.6069 21.1925L21.1927 22.6068L17.8827 19.2968ZM5.9356 7.3497C4.60673 8.56015 3.6378 10.1672 3.22278 12.0002C4.14022 16.0521 7.7646 19.0002 12.0003 19.0002C13.5997 19.0002 15.112 18.5798 16.4243 17.8384L14.396 15.8101C13.7023 16.2472 12.8808 16.5002 12.0003 16.5002C9.51498 16.5002 7.50026 14.4854 7.50026 12.0002C7.50026 11.1196 7.75317 10.2981 8.19031 9.60442L5.9356 7.3497ZM12.9139 14.328L9.67246 11.0866C9.5613 11.3696 9.50026 11.6777 9.50026 12.0002C9.50026 13.3809 10.6196 14.5002 12.0003 14.5002C12.3227 14.5002 12.6309 14.4391 12.9139 14.328ZM20.8068 16.5925L19.376 15.1617C20.0319 14.2268 20.5154 13.1586 20.7777 12.0002C19.8603 7.94818 16.2359 5.00016 12.0003 5.00016C11.1544 5.00016 10.3329 5.11773 9.55249 5.33818L7.97446 3.76015C9.22127 3.26959 10.5793 3.00016 12.0003 3.00016C17.3924 3.00016 21.8784 6.87992 22.8189 12.0002C22.5067 13.6998 21.8038 15.2628 20.8068 16.5925ZM11.7229 7.50857C11.8146 7.50299 11.9071 7.50016 12.0003 7.50016C14.4855 7.50016 16.5003 9.51488 16.5003 12.0002C16.5003 12.0933 16.4974 12.1858 16.4919 12.2775L11.7229 7.50857Z" d="M17.8827 19.2968C16.1814 20.3755 14.1638 21.0002 12.0003 21.0002C6.60812 21.0002 2.12215 17.1204 1.18164 12.0002C1.61832 9.62282 2.81932 7.5129 4.52047 5.93457L1.39366 2.80777L2.80788 1.39355L22.6069 21.1925L21.1927 22.6068L17.8827 19.2968ZM5.9356 7.3497C4.60673 8.56015 3.6378 10.1672 3.22278 12.0002C4.14022 16.0521 7.7646 19.0002 12.0003 19.0002C13.5997 19.0002 15.112 18.5798 16.4243 17.8384L14.396 15.8101C13.7023 16.2472 12.8808 16.5002 12.0003 16.5002C9.51498 16.5002 7.50026 14.4854 7.50026 12.0002C7.50026 11.1196 7.75317 10.2981 8.19031 9.60442L5.9356 7.3497ZM12.9139 14.328L9.67246 11.0866C9.5613 11.3696 9.50026 11.6777 9.50026 12.0002C9.50026 13.3809 10.6196 14.5002 12.0003 14.5002C12.3227 14.5002 12.6309 14.4391 12.9139 14.328ZM20.8068 16.5925L19.376 15.1617C20.0319 14.2268 20.5154 13.1586 20.7777 12.0002C19.8603 7.94818 16.2359 5.00016 12.0003 5.00016C11.1544 5.00016 10.3329 5.11773 9.55249 5.33818L7.97446 3.76015C9.22127 3.26959 10.5793 3.00016 12.0003 3.00016C17.3924 3.00016 21.8784 6.87992 22.8189 12.0002C22.5067 13.6998 21.8038 15.2628 20.8068 16.5925ZM11.7229 7.50857C11.8146 7.50299 11.9071 7.50016 12.0003 7.50016C14.4855 7.50016 16.5003 9.51488 16.5003 12.0002C16.5003 12.0933 16.4974 12.1858 16.4919 12.2775L11.7229 7.50857Z"
></path> ></path>
</svg> </svg>
<span class="sr-only">{{ <span class="sr-only">{{ isPasswordVisible ? t('buttons.hidePassword') : t('buttons.showPassword') }}</span>
isPasswordVisible
? 'Masquer le mot de passe'
: 'Afficher le mot de passe'
}}</span>
</button> </button>
</div> </div>
<button <button
@ -203,13 +183,13 @@
aria-labelledby="client-label" aria-labelledby="client-label"
> >
<h2 id="client-label" class="text-grey-700 mb-16"> <h2 id="client-label" class="text-grey-700 mb-16">
{{ user.client ? 'Client' : 'Pas de client associé' }} {{ user.client ? 'Client' : t('account.noClient') }}
</h2> </h2>
<div class="flex" style="--column-gap: 2rem"> <div class="flex" style="--column-gap: 2rem">
<template v-if="user.client"> <template v-if="user.client">
<img <img
:src="user.client.logo" :src="user.client.logo"
:alt="'logo' + user.client.name" :alt="t('account.clientLogo', { clientName: user.client.name })"
class="rounded-md" class="rounded-md"
width="72" width="72"
height="72" height="72"
@ -228,7 +208,7 @@
aria-labelledby="projects-label" aria-labelledby="projects-label"
> >
<h2 id="projects-label" class="text-grey-700 mb-24"> <h2 id="projects-label" class="text-grey-700 mb-24">
{{ user.role === 'pochet' ? 'Projets managés' : 'Mes projets' }} {{ user.role === 'pochet' ? t('account.managedProjects') : t('account.myProjects') }}
</h2> </h2>
<div class="projects-list flow" style="--flow-space: 1rem"> <div class="projects-list flow" style="--flow-space: 1rem">
@ -245,8 +225,8 @@
@click="toggleProject(project.uuid)" @click="toggleProject(project.uuid)"
class="btn btn--sm btn--primary" class="btn btn--sm btn--primary"
:aria-pressed="true" :aria-pressed="true"
:title="'Masquer le projet'" :title="t('buttons.hideProject')"
aria-label="Masquer le projet" :aria-label="t('buttons.hideProject')"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -263,7 +243,7 @@
<div> <div>
<p class="font-medium">{{ project.title }}</p> <p class="font-medium">{{ project.title }}</p>
<p class="text-sm text-grey-600"> <p class="text-sm text-grey-600">
Étape en cours : {{ project.step }} {{ t('account.currentStep') }} {{ t('steps.' + project.step) }}
</p> </p>
</div> </div>
</div> </div>
@ -280,8 +260,8 @@
<button <button
@click="toggleProject(project.uuid)" @click="toggleProject(project.uuid)"
class="btn btn--sm btn--primary" class="btn btn--sm btn--primary"
:title="'Afficher le projet'" :title="t('buttons.showProject')"
aria-label="Afficher le projet" :aria-label="t('buttons.showProject')"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -298,7 +278,7 @@
<div> <div>
<p class="font-medium">{{ project.title }}</p> <p class="font-medium">{{ project.title }}</p>
<p class="text-sm text-grey-600"> <p class="text-sm text-grey-600">
Étape en cours : {{ project.step }} {{ t('account.currentStep') }} {{ t('steps.' + project.step) }}
</p> </p>
</div> </div>
</div> </div>
@ -311,8 +291,8 @@
class="bg-white rounded-2xl px-16 py-24" class="bg-white rounded-2xl px-16 py-24"
aria-labelledby="projects-label" aria-labelledby="projects-label"
> >
<h2 class="sr-only" id="projects-label">Projets</h2> <h2 class="sr-only" id="projects-label">{{ t('brief.projects') }}</h2>
<p class="text-grey-700 mb-16">Nombre de projets</p> <p class="text-grey-700 mb-16">{{ t('account.projectCount') }}</p>
<p class="text-xl">{{ Object.values(user.projects).length }}</p> <p class="text-xl">{{ Object.values(user.projects).length }}</p>
</section> </section>
</div> </div>
@ -323,10 +303,12 @@ import { storeToRefs } from 'pinia';
import { useUserStore } from '../stores/user'; import { useUserStore } from '../stores/user';
import { useApiStore } from '../stores/api'; import { useApiStore } from '../stores/api';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
const userStore = useUserStore(); const userStore = useUserStore();
const { user, visibleProjects } = storeToRefs(userStore); const { user, visibleProjects } = storeToRefs(userStore);
const api = useApiStore(); const api = useApiStore();
const { t } = useI18n();
const hiddenProjectsList = computed(() => { const hiddenProjectsList = computed(() => {
if (!user.value?.projects || !user.value?.hiddenProjects) return []; if (!user.value?.projects || !user.value?.hiddenProjects) return [];
@ -349,14 +331,14 @@ async function toggleProject(projectUuid) {
try { try {
await api.toggleHiddenProject(projectUuid); await api.toggleHiddenProject(projectUuid);
} catch (error) { } catch (error) {
console.error('Erreur lors du masquage/affichage du projet:', error); console.error(t('errors.toggleProjectFailed'), error);
} }
} }
// Email // Email
const email = ref(''); const email = ref('');
const emailBtn = ref({ const emailBtn = ref({
text: 'Mettre à jour', text: t('buttons.update'),
status: 'ready', status: 'ready',
}); });
const isEditingEmail = ref(false); const isEditingEmail = ref(false);
@ -365,7 +347,7 @@ const isEmailValid = computed(() => {
return emailRegex.test(email.value); return emailRegex.test(email.value);
}); });
async function updateEmail() { async function updateEmail() {
emailBtn.value.text = 'En cours…'; emailBtn.value.text = t('auth.inProgress');
emailBtn.value.status = 'pending'; emailBtn.value.status = 'pending';
const headers = { const headers = {
@ -378,11 +360,11 @@ async function updateEmail() {
const json = await response.json(); const json = await response.json();
if (json.status === 'success') { if (json.status === 'success') {
emailBtn.value.text = 'Mise à jour réussie'; emailBtn.value.text = t('auth.updateSuccess');
emailBtn.value.status = 'succeed'; emailBtn.value.status = 'succeed';
setTimeout(() => { setTimeout(() => {
emailBtn.value.text = 'Mettre à jour'; emailBtn.value.text = t('buttons.update');
emailBtn.value.status = 'ready'; emailBtn.value.status = 'ready';
isEditingEmail.value = false; isEditingEmail.value = false;
}, 1500); }, 1500);
@ -394,7 +376,7 @@ async function updateEmail() {
// Password // Password
const password = ref(''); const password = ref('');
const passwordBtn = ref({ const passwordBtn = ref({
text: 'Mettre à jour', text: t('buttons.update'),
status: 'ready', status: 'ready',
}); });
const passwordConfirm = ref(''); const passwordConfirm = ref('');
@ -405,7 +387,7 @@ const isPasswordConfirmed = computed(() => {
); );
}); });
async function updatePassword() { async function updatePassword() {
passwordBtn.value.text = 'en cours…'; passwordBtn.value.text = t('auth.updateInProgress');
passwordBtn.value.status = 'pending'; passwordBtn.value.status = 'pending';
const headers = { const headers = {
@ -420,11 +402,11 @@ async function updatePassword() {
if (json.status === 'success') { if (json.status === 'success') {
password.value = ''; password.value = '';
passwordConfirm.value = ''; passwordConfirm.value = '';
passwordBtn.value.text = 'mise à jour réussie'; passwordBtn.value.text = t('auth.updateSuccess');
passwordBtn.value.status = 'succeed'; passwordBtn.value.status = 'succeed';
setTimeout(() => { setTimeout(() => {
passwordBtn.value.text = 'Mettre à jour'; passwordBtn.value.text = t('buttons.update');
passwordBtn.value.status = 'ready'; passwordBtn.value.status = 'ready';
}, 1500); }, 1500);
} else { } else {

View file

@ -7,7 +7,7 @@
data-icon="arrow-left" data-icon="arrow-left"
aria-labelledby="back-to-project" aria-labelledby="back-to-project"
> >
<span id="back-to-project">Retour au projet</span> <span id="back-to-project">{{ t('buttons.backToProject') }}</span>
</router-link> </router-link>
<button <button
class="btn | ml-auto" class="btn | ml-auto"
@ -18,11 +18,11 @@
" "
:title=" :title="
!page.hasOwnProperty('moodboard') || page.moodboard.length === 0 !page.hasOwnProperty('moodboard') || page.moodboard.length === 0
? 'Ajoutez au moins une image' ? t('buttons.validateMinimum')
: undefined : undefined
" "
> >
Valider et envoyer le brief {{ t('buttons.validate') }}
</button> </button>
</header> </header>
<Images /> <Images />
@ -33,7 +33,9 @@ import Images from '../components/project/brief/Images.vue';
import { usePageStore } from '../stores/page'; import { usePageStore } from '../stores/page';
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useApiStore } from '../stores/api'; import { useApiStore } from '../stores/api';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { page } = storeToRefs(usePageStore()); const { page } = storeToRefs(usePageStore());
const api = useApiStore(); const api = useApiStore();

View file

@ -1,7 +1,7 @@
<template> <template>
<main v-if="page.inspirations" class="flex flex-col"> <main v-if="page.inspirations" class="flex flex-col">
<header class="flex"> <header class="flex">
<h2 id="tabslist" class="sr-only">Inspirations</h2> <h2 id="tabslist" class="sr-only">{{ t('inspirations.title') }}</h2>
<Tabs :tabs="tabs" @update:currentTab="changeTab" /> <Tabs :tabs="tabs" @update:currentTab="changeTab" />
<Selector <Selector
v-if="page.inspirations.length > 1" v-if="page.inspirations.length > 1"
@ -36,6 +36,9 @@ import { useUserStore } from "../stores/user";
import { ref, computed } from "vue"; import { ref, computed } from "vue";
import { usePageStore } from "../stores/page"; import { usePageStore } from "../stores/page";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
// Stores // Stores
const { page } = storeToRefs(usePageStore()); const { page } = storeToRefs(usePageStore());
@ -60,14 +63,14 @@ const favoriteImages = computed(() =>
const tabs = computed(() => [ const tabs = computed(() => [
{ {
label: "Les Inspirations", label: t('inspirations.title'),
id: "all", id: "all",
icon: null, icon: null,
count: currentInspiration.value.media.length, count: currentInspiration.value.media.length,
isActive: currentTab.value === "all", isActive: currentTab.value === "all",
}, },
{ {
label: "Mes Favoris", label: t('inspirations.favorites'),
id: "favorites", id: "favorites",
icon: "favorite", icon: "favorite",
count: favoriteImages.value.length, count: favoriteImages.value.length,

View file

@ -6,7 +6,7 @@
style="--row-gap: 1rem; max-width: 24em" style="--row-gap: 1rem; max-width: 24em"
> >
<div class="field | w-full" role="group" aria-labelledby="username"> <div class="field | w-full" role="group" aria-labelledby="username">
<label for="username">Email</label> <label for="username">{{ t('forms.email') }}</label>
<input <input
@input="updateEmail" @input="updateEmail"
type="email" type="email"
@ -17,13 +17,13 @@
autocorrect="off" autocorrect="off"
spellcheck="false" spellcheck="false"
class="w-full rounded-md border border-grey-200 px-16 py-12 mt-8" class="w-full rounded-md border border-grey-200 px-16 py-12 mt-8"
placeholder="mail@exemple.com" :placeholder="t('forms.emailPlaceholder')"
:aria-invalid="{ true: !isValidEmail }" :aria-invalid="{ true: !isValidEmail }"
required required
/> />
</div> </div>
<div class="field | w-full" role="group" aria-labelledby="password"> <div class="field | w-full" role="group" aria-labelledby="password">
<label for="password">Mot de passe</label> <label for="password">{{ t('forms.password') }}</label>
<input <input
:type="isPasswordVisible ? 'text' : 'password'" :type="isPasswordVisible ? 'text' : 'password'"
v-model="password" v-model="password"
@ -41,18 +41,18 @@
:aria-pressed="isPasswordVisible ? 'true' : 'false'" :aria-pressed="isPasswordVisible ? 'true' : 'false'"
aria-controls="password" aria-controls="password"
@click="isPasswordVisible = !isPasswordVisible" @click="isPasswordVisible = !isPasswordVisible"
:title="isPasswordVisible ? 'Masquer le mot de passe' : 'Afficher le mot de passe'" :title="isPasswordVisible ? t('buttons.hidePassword') : t('buttons.showPassword')"
> >
<svg aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor"> <svg aria-hidden="true" focusable="false" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="16" height="16" fill="currentColor">
<path v-if="isPasswordVisible" d="M12.0003 3C17.3924 3 21.8784 6.87976 22.8189 12C21.8784 17.1202 17.3924 21 12.0003 21C6.60812 21 2.12215 17.1202 1.18164 12C2.12215 6.87976 6.60812 3 12.0003 3ZM12.0003 19C16.2359 19 19.8603 16.052 20.7777 12C19.8603 7.94803 16.2359 5 12.0003 5C7.7646 5 4.14022 7.94803 3.22278 12C4.14022 16.052 7.7646 19 12.0003 19ZM12.0003 16.5C9.51498 16.5 7.50026 14.4853 7.50026 12C7.50026 9.51472 9.51498 7.5 12.0003 7.5C14.4855 7.5 16.5003 9.51472 16.5003 12C16.5003 14.4853 14.4855 16.5 12.0003 16.5ZM12.0003 14.5C13.381 14.5 14.5003 13.3807 14.5003 12C14.5003 10.6193 13.381 9.5 12.0003 9.5C10.6196 9.5 9.50026 10.6193 9.50026 12C9.50026 13.3807 10.6196 14.5 12.0003 14.5Z"></path> <path v-if="isPasswordVisible" d="M12.0003 3C17.3924 3 21.8784 6.87976 22.8189 12C21.8784 17.1202 17.3924 21 12.0003 21C6.60812 21 2.12215 17.1202 1.18164 12C2.12215 6.87976 6.60812 3 12.0003 3ZM12.0003 19C16.2359 19 19.8603 16.052 20.7777 12C19.8603 7.94803 16.2359 5 12.0003 5C7.7646 5 4.14022 7.94803 3.22278 12C4.14022 16.052 7.7646 19 12.0003 19ZM12.0003 16.5C9.51498 16.5 7.50026 14.4853 7.50026 12C7.50026 9.51472 9.51498 7.5 12.0003 7.5C14.4855 7.5 16.5003 9.51472 16.5003 12C16.5003 14.4853 14.4855 16.5 12.0003 16.5ZM12.0003 14.5C13.381 14.5 14.5003 13.3807 14.5003 12C14.5003 10.6193 13.381 9.5 12.0003 9.5C10.6196 9.5 9.50026 10.6193 9.50026 12C9.50026 13.3807 10.6196 14.5 12.0003 14.5Z"></path>
<path v-else d="M17.8827 19.2968C16.1814 20.3755 14.1638 21.0002 12.0003 21.0002C6.60812 21.0002 2.12215 17.1204 1.18164 12.0002C1.61832 9.62282 2.81932 7.5129 4.52047 5.93457L1.39366 2.80777L2.80788 1.39355L22.6069 21.1925L21.1927 22.6068L17.8827 19.2968ZM5.9356 7.3497C4.60673 8.56015 3.6378 10.1672 3.22278 12.0002C4.14022 16.0521 7.7646 19.0002 12.0003 19.0002C13.5997 19.0002 15.112 18.5798 16.4243 17.8384L14.396 15.8101C13.7023 16.2472 12.8808 16.5002 12.0003 16.5002C9.51498 16.5002 7.50026 14.4854 7.50026 12.0002C7.50026 11.1196 7.75317 10.2981 8.19031 9.60442L5.9356 7.3497ZM12.9139 14.328L9.67246 11.0866C9.5613 11.3696 9.50026 11.6777 9.50026 12.0002C9.50026 13.3809 10.6196 14.5002 12.0003 14.5002C12.3227 14.5002 12.6309 14.4391 12.9139 14.328ZM20.8068 16.5925L19.376 15.1617C20.0319 14.2268 20.5154 13.1586 20.7777 12.0002C19.8603 7.94818 16.2359 5.00016 12.0003 5.00016C11.1544 5.00016 10.3329 5.11773 9.55249 5.33818L7.97446 3.76015C9.22127 3.26959 10.5793 3.00016 12.0003 3.00016C17.3924 3.00016 21.8784 6.87992 22.8189 12.0002C22.5067 13.6998 21.8038 15.2628 20.8068 16.5925ZM11.7229 7.50857C11.8146 7.50299 11.9071 7.50016 12.0003 7.50016C14.4855 7.50016 16.5003 9.51488 16.5003 12.0002C16.5003 12.0933 16.4974 12.1858 16.4919 12.2775L11.7229 7.50857Z"></path> <path v-else d="M17.8827 19.2968C16.1814 20.3755 14.1638 21.0002 12.0003 21.0002C6.60812 21.0002 2.12215 17.1204 1.18164 12.0002C1.61832 9.62282 2.81932 7.5129 4.52047 5.93457L1.39366 2.80777L2.80788 1.39355L22.6069 21.1925L21.1927 22.6068L17.8827 19.2968ZM5.9356 7.3497C4.60673 8.56015 3.6378 10.1672 3.22278 12.0002C4.14022 16.0521 7.7646 19.0002 12.0003 19.0002C13.5997 19.0002 15.112 18.5798 16.4243 17.8384L14.396 15.8101C13.7023 16.2472 12.8808 16.5002 12.0003 16.5002C9.51498 16.5002 7.50026 14.4854 7.50026 12.0002C7.50026 11.1196 7.75317 10.2981 8.19031 9.60442L5.9356 7.3497ZM12.9139 14.328L9.67246 11.0866C9.5613 11.3696 9.50026 11.6777 9.50026 12.0002C9.50026 13.3809 10.6196 14.5002 12.0003 14.5002C12.3227 14.5002 12.6309 14.4391 12.9139 14.328ZM20.8068 16.5925L19.376 15.1617C20.0319 14.2268 20.5154 13.1586 20.7777 12.0002C19.8603 7.94818 16.2359 5.00016 12.0003 5.00016C11.1544 5.00016 10.3329 5.11773 9.55249 5.33818L7.97446 3.76015C9.22127 3.26959 10.5793 3.00016 12.0003 3.00016C17.3924 3.00016 21.8784 6.87992 22.8189 12.0002C22.5067 13.6998 21.8038 15.2628 20.8068 16.5925ZM11.7229 7.50857C11.8146 7.50299 11.9071 7.50016 12.0003 7.50016C14.4855 7.50016 16.5003 9.51488 16.5003 12.0002C16.5003 12.0933 16.4974 12.1858 16.4919 12.2775L11.7229 7.50857Z"></path>
</svg> </svg>
<span class="sr-only">{{ isPasswordVisible ? 'Masquer le mot de passe' : 'Afficher le mot de passe' }}</span> <span class="sr-only">{{ isPasswordVisible ? t('buttons.hidePassword') : t('buttons.showPassword') }}</span>
</button> </button>
</div> </div>
<p class="error" v-if="errorMessage" v-html="errorMessage"></p> <p class="error" v-if="errorMessage" v-html="errorMessage"></p>
<div class="sr-only" id="announce" aria-live="assertive"> <div class="sr-only" id="announce" aria-live="assertive">
{{ isPasswordVisible ? 'Mot de passe affiché' : 'Mot de passe masqué' }} {{ isPasswordVisible ? t('auth.passwordShown') : t('auth.passwordHidden') }}
</div> </div>
<button <button
@click="login" @click="login"
@ -68,7 +68,9 @@
</template> </template>
<script setup> <script setup>
import { ref, watch } from 'vue'; import { ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const email = ref(''); const email = ref('');
const isValidEmail = ref(false); const isValidEmail = ref(false);
const password = ref(''); const password = ref('');
@ -76,7 +78,7 @@ const isPasswordVisible = ref(false);
const errorMessage = ref(null); const errorMessage = ref(null);
const submitBtn = ref({ const submitBtn = ref({
message: 'Connexion', message: t('auth.login'),
state: 'ready', state: 'ready',
}); });
@ -90,10 +92,10 @@ function updateEmail(event) {
async function login() { async function login() {
if (email.value.length === 0 || password.value.length === 0) { if (email.value.length === 0 || password.value.length === 0) {
errorMessage.value = 'Veuillez remplir les champs.'; errorMessage.value = t('auth.fillFields');
} }
submitBtn.value.message = 'En cours…'; submitBtn.value.message = t('auth.inProgress');
submitBtn.value.state = 'pending'; submitBtn.value.state = 'pending';
const headers = { const headers = {
@ -114,7 +116,7 @@ async function login() {
} }
} else { } else {
errorMessage.value = json.message; errorMessage.value = json.message;
submitBtn.value.message = 'Réessayer'; submitBtn.value.message = t('buttons.retry');
submitBtn.value.state = 'ready'; submitBtn.value.state = 'ready';
} }
} }

View file

@ -8,7 +8,7 @@
:disabled="!sortedNotifications.length" :disabled="!sortedNotifications.length"
@click="readAll()" @click="readAll()"
> >
Marquer tout come lu {{ t('buttons.markAllAsRead') }}
</button> </button>
</header> </header>
<div <div
@ -32,9 +32,9 @@
/> />
</svg> </svg>
<p v-if="currentTab === 'all'"> <p v-if="currentTab === 'all'">
Vous navez pas de nouvelles notifications {{ t('notifications.none') }}
</p> </p>
<p v-else>Vous navez pas de notifications non lues</p> <p v-else>{{ t('notifications.noneUnread') }}</p>
</div> </div>
<section v-else class="notifications | flow"> <section v-else class="notifications | flow">
<template <template
@ -67,9 +67,11 @@ import Content from '../components/notifications/Content.vue';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import ProjectRequest from '../components/notifications/ProjectRequest.vue'; import ProjectRequest from '../components/notifications/ProjectRequest.vue';
import AppointmentRequest from '../components/notifications/AppointmentRequest.vue'; import AppointmentRequest from '../components/notifications/AppointmentRequest.vue';
import { useI18n } from 'vue-i18n';
dayjs.locale('fr'); dayjs.locale('fr');
const { t } = useI18n();
const userStore = useUserStore(); const userStore = useUserStore();
const router = useRouter(); const router = useRouter();
const { notifications } = storeToRefs(useUserStore()); const { notifications } = storeToRefs(useUserStore());
@ -78,14 +80,14 @@ const currentTab = ref('all');
const tabs = computed(() => { const tabs = computed(() => {
return [ return [
{ {
label: 'Tout', label: t('notifications.all'),
id: 'all', id: 'all',
icon: null, icon: null,
count: null, count: null,
isActive: currentTab.value === 'all', isActive: currentTab.value === 'all',
}, },
{ {
label: 'Non lu', label: t('notifications.unread'),
id: 'unread', id: 'unread',
icon: null, icon: null,
count: null, count: null,
@ -121,7 +123,7 @@ function readAll() {
try { try {
api.markAllNotificationsRead(); api.markAllNotificationsRead();
} catch (error) { } catch (error) {
console.log('Could not read all notifications : ', error); console.log(t('errors.readAllNotificationsFailed'), error);
} }
} }
@ -132,7 +134,7 @@ async function handleNotificationClick(notification) {
try { try {
await api.markNotificationRead(notification); await api.markNotificationRead(notification);
} catch (error) { } catch (error) {
console.log('Could not mark notification as read:', error); console.log(t('errors.markNotificationFailed'), error);
} }
} }

View file

@ -8,8 +8,8 @@
<svg aria-hidden="true" width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg aria-hidden="true" width="40" height="40" viewBox="0 0 40 40" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.25 8.75C5.58696 8.75 4.95107 9.01339 4.48223 9.48223C4.01339 9.95107 3.75 10.587 3.75 11.25V33.75C3.75 34.413 4.01339 35.0489 4.48223 35.5178C4.95107 35.9866 5.58696 36.25 6.25 36.25H33.75C34.413 36.25 35.0489 35.9866 35.5178 35.5178C35.9866 35.0489 36.25 34.413 36.25 33.75V11.25C36.25 10.587 35.9866 9.95107 35.5178 9.48223C35.0489 9.01339 34.413 8.75 33.75 8.75H28.75M3.75 18.75H36.25M11.25 3.75V13.75M28.75 3.75V13.75M11.25 8.75H23.75" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/> <path d="M6.25 8.75C5.58696 8.75 4.95107 9.01339 4.48223 9.48223C4.01339 9.95107 3.75 10.587 3.75 11.25V33.75C3.75 34.413 4.01339 35.0489 4.48223 35.5178C4.95107 35.9866 5.58696 36.25 6.25 36.25H33.75C34.413 36.25 35.0489 35.9866 35.5178 35.5178C35.9866 35.0489 36.25 34.413 36.25 33.75V11.25C36.25 10.587 35.9866 9.95107 35.5178 9.48223C35.0489 9.01339 34.413 8.75 33.75 8.75H28.75M3.75 18.75H36.25M11.25 3.75V13.75M28.75 3.75V13.75M11.25 8.75H23.75" stroke="currentColor" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
</svg> </svg>
<p class="mb-32">Vous navez aucune réunion programmée</p> <p class="mb-32">{{ t('meetings.none') }}</p>
<button class="btn">Demander un RDV</button> <button class="btn">{{ t('buttons.requestMeeting') }}</button>
</div> </div>
</main> </main>
</template> </template>
@ -21,21 +21,23 @@ import { usePageStore } from "../stores/page"
import { useUserStore } from "../stores/user"; import { useUserStore } from "../stores/user";
import { ref, computed } from "vue"; import { ref, computed } from "vue";
import { storeToRefs } from "pinia"; import { storeToRefs } from "pinia";
import { useI18n } from 'vue-i18n';
const { t } = useI18n();
const { page } = storeToRefs(usePageStore()); const { page } = storeToRefs(usePageStore());
const user = useUserStore().user; const user = useUserStore().user;
const currentTab = ref("future"); const currentTab = ref("future");
const tabs = computed(() => { const tabs = computed(() => {
return [ return [
{ {
label: "À venir", label: t('meetings.upcoming'),
id: "future", id: "future",
icon: null, icon: null,
count: null, count: null,
isActive: currentTab.value === "future", isActive: currentTab.value === "future",
}, },
{ {
label: "Passées", label: t('meetings.past'),
id: "past", id: "past",
icon: null, icon: null,
count: null, count: null,