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

View file

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

View file

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

View file

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

View file

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