refactor(i18n) : replace hardcoded French strings with i18n keys

Replace 30+ hardcoded strings across 15 components with $t() calls.
Added keys for common actions, drawers titles, empty states, and modals.

Ticket: T-020

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Matthieu
2026-03-17 15:25:09 +01:00
parent 455121132d
commit 8544babf8c
16 changed files with 80 additions and 44 deletions

View File

@@ -1,5 +1,5 @@
<template> <template>
<AppDrawer v-model="isOpen" :title="isEditing ? 'Modifier un client' : 'Ajouter un client'"> <AppDrawer v-model="isOpen" :title="isEditing ? $t('clients.editClient') : $t('clients.addClient')">
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2"> <form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
<MalioInputText <MalioInputText
v-model="form.name" v-model="form.name"

View File

@@ -1,5 +1,5 @@
<template> <template>
<AppDrawer v-model="isOpen" :title="isEditing ? 'Modifier un projet' : 'Ajouter un projet'"> <AppDrawer v-model="isOpen" :title="isEditing ? $t('projects.editProject') : $t('projects.addProject')">
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2"> <form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
<MalioInputText <MalioInputText
v-model="form.code" v-model="form.code"

View File

@@ -1,5 +1,5 @@
<template> <template>
<AppDrawer v-model="isOpen" :title="isEditing ? 'Modifier un ticket' : 'Ajouter un ticket'"> <AppDrawer v-model="isOpen" :title="isEditing ? $t('tasks.editTask') : $t('tasks.addTask')">
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2"> <form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
<MalioInputText <MalioInputText
v-model="form.title" v-model="form.title"
@@ -267,7 +267,7 @@ async function handleArchive() {
if (timerStore.activeEntry?.task) { if (timerStore.activeEntry?.task) {
const taskIri = typeof timerStore.activeEntry.task === 'string' const taskIri = typeof timerStore.activeEntry.task === 'string'
? timerStore.activeEntry.task ? timerStore.activeEntry.task
: (timerStore.activeEntry.task as any)?.['@id'] ?? `/api/tasks/${(timerStore.activeEntry.task as any)?.id}` : (timerStore.activeEntry.task as Task)?.['@id'] ?? `/api/tasks/${(timerStore.activeEntry.task as Task)?.id}`
if (taskIri === `/api/tasks/${props.task.id}`) { if (taskIri === `/api/tasks/${props.task.id}`) {
await timerStore.stop() await timerStore.stop()
} }

View File

@@ -1,5 +1,5 @@
<template> <template>
<AppDrawer v-model="isOpen" :title="isEditing ? 'Modifier un effort' : 'Ajouter un effort'"> <AppDrawer v-model="isOpen" :title="isEditing ? $t('taskEfforts.editEffort') : $t('taskEfforts.addEffort')">
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2"> <form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
<MalioInputText <MalioInputText
v-model="form.label" v-model="form.label"

View File

@@ -1,5 +1,5 @@
<template> <template>
<AppDrawer v-model="isOpen" :title="isEditing ? 'Modifier un groupe' : 'Ajouter un groupe'"> <AppDrawer v-model="isOpen" :title="isEditing ? $t('taskGroups.editGroup') : $t('taskGroups.addGroup')">
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2"> <form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
<MalioInputText <MalioInputText
v-model="form.title" v-model="form.title"

View File

@@ -24,7 +24,7 @@
{{ task.project.code }}-{{ task.number }} {{ task.project.code }}-{{ task.number }}
</span> </span>
<h2 class="text-lg font-bold tracking-tight text-neutral-900"> <h2 class="text-lg font-bold tracking-tight text-neutral-900">
{{ isEditing ? 'Modifier un ticket' : 'Ajouter un ticket' }} {{ isEditing ? $t('tasks.editTask') : $t('tasks.addTask') }}
</h2> </h2>
</div> </div>
<button <button
@@ -568,7 +568,7 @@ async function handleArchive() {
if (timerStore.activeEntry?.task) { if (timerStore.activeEntry?.task) {
const taskIri = typeof timerStore.activeEntry.task === 'string' const taskIri = typeof timerStore.activeEntry.task === 'string'
? timerStore.activeEntry.task ? timerStore.activeEntry.task
: (timerStore.activeEntry.task as any)?.['@id'] ?? `/api/tasks/${(timerStore.activeEntry.task as any)?.id}` : (timerStore.activeEntry.task as Task)?.['@id'] ?? `/api/tasks/${(timerStore.activeEntry.task as Task)?.id}`
if (taskIri === `/api/tasks/${props.task.id}`) { if (taskIri === `/api/tasks/${props.task.id}`) {
await timerStore.stop() await timerStore.stop()
} }

View File

@@ -1,5 +1,5 @@
<template> <template>
<AppDrawer v-model="isOpen" :title="isEditing ? 'Modifier une priorité' : 'Ajouter une priorité'"> <AppDrawer v-model="isOpen" :title="isEditing ? $t('taskPriorities.editPriority') : $t('taskPriorities.addPriority')">
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2"> <form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
<MalioInputText <MalioInputText
v-model="form.label" v-model="form.label"

View File

@@ -1,5 +1,5 @@
<template> <template>
<AppDrawer v-model="isOpen" :title="isEditing ? 'Modifier un statut' : 'Ajouter un statut'"> <AppDrawer v-model="isOpen" :title="isEditing ? $t('taskStatuses.editStatus') : $t('taskStatuses.addStatus')">
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2"> <form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
<MalioInputText <MalioInputText
v-model="form.label" v-model="form.label"

View File

@@ -1,5 +1,5 @@
<template> <template>
<AppDrawer v-model="isOpen" :title="isEditing ? 'Modifier un tag' : 'Ajouter un tag'"> <AppDrawer v-model="isOpen" :title="isEditing ? $t('taskTags.editTag') : $t('taskTags.addTag')">
<form @submit.prevent="handleSubmit" class="flex flex-col gap-2"> <form @submit.prevent="handleSubmit" class="flex flex-col gap-2">
<MalioInputText <MalioInputText
v-model="form.label" v-model="form.label"

View File

@@ -21,7 +21,7 @@
<!-- Full display: title + project + type dot + duration --> <!-- Full display: title + project + type dot + duration -->
<template v-if="sizeLevel >= 3"> <template v-if="sizeLevel >= 3">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<div class="font-semibold truncate">{{ entry.title || 'Sans titre' }}</div> <div class="font-semibold truncate">{{ entry.title || $t('common.untitled') }}</div>
<span class="ml-auto shrink-0 text-[10px] tabular-nums opacity-80">{{ duration }}</span> <span class="ml-auto shrink-0 text-[10px] tabular-nums opacity-80">{{ duration }}</span>
</div> </div>
<div v-if="entry.project" class="truncate text-[10px] opacity-80">{{ entry.project.name }}</div> <div v-if="entry.project" class="truncate text-[10px] opacity-80">{{ entry.project.name }}</div>
@@ -39,13 +39,13 @@
<!-- Medium: title + duration --> <!-- Medium: title + duration -->
<template v-else-if="sizeLevel === 2"> <template v-else-if="sizeLevel === 2">
<div class="font-semibold truncate">{{ entry.title || 'Sans titre' }}</div> <div class="font-semibold truncate">{{ entry.title || $t('common.untitled') }}</div>
<div class="text-[10px] tabular-nums opacity-80">{{ duration }}</div> <div class="text-[10px] tabular-nums opacity-80">{{ duration }}</div>
</template> </template>
<!-- Small: title only --> <!-- Small: title only -->
<template v-else-if="sizeLevel === 1"> <template v-else-if="sizeLevel === 1">
<div class="font-semibold truncate text-[10px] leading-tight">{{ entry.title || 'Sans titre' }}</div> <div class="font-semibold truncate text-[10px] leading-tight">{{ entry.title || $t('common.untitled') }}</div>
</template> </template>
<!-- Tiny: just a colored bar, no text --> <!-- Tiny: just a colored bar, no text -->

View File

@@ -1,5 +1,5 @@
<template> <template>
<AppDrawer v-model="isOpen" :title="isEditing ? 'Modifier un temps' : 'Ajouter une Activité'"> <AppDrawer v-model="isOpen" :title="isEditing ? $t('timeEntries.editEntry') : $t('timeEntries.addEntry')">
<form class="space-y-4" @submit.prevent="onSubmit"> <form class="space-y-4" @submit.prevent="onSubmit">
<div> <div>
<label class="mb-1 block text-sm font-semibold text-neutral-700">Titre</label> <label class="mb-1 block text-sm font-semibold text-neutral-700">Titre</label>
@@ -117,7 +117,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { TimeEntry } from '~/services/dto/time-entry' import type { TimeEntry, TimeEntryWrite } from '~/services/dto/time-entry'
import type { UserData } from '~/services/dto/user-data' import type { UserData } from '~/services/dto/user-data'
import type { Project } from '~/services/dto/project' import type { Project } from '~/services/dto/project'
import type { TaskTag } from '~/services/dto/task-tag' import type { TaskTag } from '~/services/dto/task-tag'
@@ -257,7 +257,7 @@ async function onSubmit() {
if (isEditing.value && props.entry) { if (isEditing.value && props.entry) {
await update(props.entry.id, payload) await update(props.entry.id, payload)
} else { } else {
await create(payload as any) await create(payload as TimeEntryWrite)
} }
emit('saved') emit('saved')

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="space-y-2"> <div class="space-y-2">
<div v-if="entries.length === 0" class="rounded-lg border border-neutral-200 bg-neutral-50 py-12 text-center text-sm text-neutral-400"> <div v-if="entries.length === 0" class="rounded-lg border border-neutral-200 bg-neutral-50 py-12 text-center text-sm text-neutral-400">
Aucune activité pour cette période {{ $t('timeEntries.noEntries') }}
</div> </div>
<div <div
@@ -20,7 +20,7 @@
<div class="min-w-0 flex-1"> <div class="min-w-0 flex-1">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
<span class="truncate text-sm font-semibold text-neutral-900"> <span class="truncate text-sm font-semibold text-neutral-900">
{{ entry.title || 'Sans titre' }} {{ entry.title || $t('common.untitled') }}
</span> </span>
<span <span
v-for="tag in entry.tags" v-for="tag in entry.tags"
@@ -56,7 +56,7 @@
<!-- Delete action --> <!-- Delete action -->
<button <button
class="shrink-0 rounded-md p-1.5 text-neutral-300 opacity-0 transition hover:bg-red-50 hover:text-red-500 group-hover:opacity-100" class="shrink-0 rounded-md p-1.5 text-neutral-300 opacity-0 transition hover:bg-red-50 hover:text-red-500 group-hover:opacity-100"
title="Supprimer" :title="$t('common.delete')"
@click.stop="emit('deleteEntry', entry)" @click.stop="emit('deleteEntry', entry)"
> >
<Icon name="mdi:delete-outline" size="18" /> <Icon name="mdi:delete-outline" size="18" />

View File

@@ -99,7 +99,7 @@
:style="{ backgroundColor: entry.project?.color ?? '#94a3b8' }" :style="{ backgroundColor: entry.project?.color ?? '#94a3b8' }"
/> />
<div class="min-w-0"> <div class="min-w-0">
<div class="truncate text-xs font-medium text-neutral-800">{{ entry.title || 'Sans titre' }}</div> <div class="truncate text-xs font-medium text-neutral-800">{{ entry.title || $t('common.untitled') }}</div>
<div class="text-[10px] text-neutral-500"> <div class="text-[10px] text-neutral-500">
{{ formatTime(entry.startedAt) }} {{ entry.stoppedAt ? formatTime(entry.stoppedAt) : '...' }} {{ formatTime(entry.startedAt) }} {{ entry.stoppedAt ? formatTime(entry.stoppedAt) : '...' }}
</div> </div>
@@ -141,6 +141,8 @@
<script setup lang="ts"> <script setup lang="ts">
import type { TimeEntry } from '~/services/dto/time-entry' import type { TimeEntry } from '~/services/dto/time-entry'
const { t } = useI18n()
const props = defineProps<{ const props = defineProps<{
entries: TimeEntry[] entries: TimeEntry[]
startDate: Date startDate: Date
@@ -459,7 +461,7 @@ function onMoveStart(payload: { entry: TimeEntry; offsetY: number }, sourceDayIn
dragState.value = { dragState.value = {
entryId: entry.id, entryId: entry.id,
entry, entry,
title: entry.title || 'Sans titre', title: entry.title || t('common.untitled'),
color: entry.project?.color ?? '#94a3b8', color: entry.project?.color ?? '#94a3b8',
durationMinutes, durationMinutes,
ghostHeightPx: Math.max((durationMinutes / 60) * hourHeight, 20), ghostHeightPx: Math.max((durationMinutes / 60) * hourHeight, 20),

View File

@@ -4,19 +4,18 @@
<div class="fixed inset-0 z-50 flex items-center justify-center"> <div class="fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/30" @click="cancel" /> <div class="absolute inset-0 bg-black/30" @click="cancel" />
<div class="relative z-10 w-full max-w-md rounded-lg bg-white p-6 shadow-xl"> <div class="relative z-10 w-full max-w-md rounded-lg bg-white p-6 shadow-xl">
<h3 class="text-lg font-bold text-neutral-900">Supprimer le statut « {{ statusLabel }} »</h3> <h3 class="text-lg font-bold text-neutral-900">{{ $t('taskStatuses.deleteStatus', { label: statusLabel }) }}</h3>
<p class="mt-3 text-sm text-neutral-600"> <p class="mt-3 text-sm text-neutral-600">
{{ taskCount }} tâche{{ taskCount > 1 ? 's sont liées' : ' est liée' }} à ce statut. {{ taskCount > 1 ? $t('taskStatuses.linkedTasksPlural', { count: taskCount }) : $t('taskStatuses.linkedTasks', { count: taskCount }) }}
Choisissez les déplacer :
</p> </p>
<div class="mt-4"> <div class="mt-4">
<MalioSelect <MalioSelect
v-model="targetStatusId" v-model="targetStatusId"
:options="targetOptions" :options="targetOptions"
label="Déplacer vers" :label="$t('taskStatuses.moveTo')"
empty-option-label="Backlog (sans statut)" :empty-option-label="$t('taskStatuses.backlog')"
min-width="w-full" min-width="w-full"
/> />
</div> </div>
@@ -27,7 +26,7 @@
class="rounded-md border border-neutral-300 px-4 py-2 text-sm font-semibold text-neutral-700 hover:bg-neutral-50" class="rounded-md border border-neutral-300 px-4 py-2 text-sm font-semibold text-neutral-700 hover:bg-neutral-50"
@click="cancel" @click="cancel"
> >
Annuler {{ $t('common.cancel') }}
</button> </button>
<button <button
type="button" type="button"
@@ -35,7 +34,7 @@
:disabled="isProcessing" :disabled="isProcessing"
@click="confirm" @click="confirm"
> >
Supprimer {{ $t('common.delete') }}
</button> </button>
</div> </div>
</div> </div>

View File

@@ -22,43 +22,66 @@
"clients": { "clients": {
"created": "Client créé avec succès.", "created": "Client créé avec succès.",
"updated": "Client mis à jour avec succès.", "updated": "Client mis à jour avec succès.",
"deleted": "Client supprimé avec succès." "deleted": "Client supprimé avec succès.",
"addClient": "Ajouter un client",
"editClient": "Modifier un client"
}, },
"projects": { "projects": {
"title": "Projets",
"created": "Projet créé avec succès.", "created": "Projet créé avec succès.",
"updated": "Projet mis à jour avec succès.", "updated": "Projet mis à jour avec succès.",
"deleted": "Projet supprimé avec succès.", "deleted": "Projet supprimé avec succès.",
"archived": "Projet archivé avec succès.", "archived": "Projet archivé avec succès.",
"unarchived": "Projet désarchivé avec succès.", "unarchived": "Projet désarchivé avec succès.",
"showArchived": "Voir les projets archivés", "showArchived": "Voir les projets archivés",
"hideArchived": "Masquer les projets archivés" "hideArchived": "Masquer les projets archivés",
"noProjects": "Aucun projet trouvé.",
"noArchivedProjects": "Aucun projet archivé.",
"addProject": "Ajouter un projet",
"addProjectShort": "Projet",
"editProject": "Modifier un projet"
}, },
"taskStatuses": { "taskStatuses": {
"created": "Statut créé avec succès.", "created": "Statut créé avec succès.",
"updated": "Statut mis à jour avec succès.", "updated": "Statut mis à jour avec succès.",
"deleted": "Statut supprimé avec succès." "deleted": "Statut supprimé avec succès.",
"addStatus": "Ajouter un statut",
"editStatus": "Modifier un statut",
"deleteStatus": "Supprimer le statut « {label} »",
"linkedTasks": "{count} tâche est liée à ce statut. Choisissez où les déplacer :",
"linkedTasksPlural": "{count} tâches sont liées à ce statut. Choisissez où les déplacer :",
"moveTo": "Déplacer vers",
"backlog": "Backlog (sans statut)"
}, },
"taskEfforts": { "taskEfforts": {
"created": "Effort créé avec succès.", "created": "Effort créé avec succès.",
"updated": "Effort mis à jour avec succès.", "updated": "Effort mis à jour avec succès.",
"deleted": "Effort supprimé avec succès." "deleted": "Effort supprimé avec succès.",
"addEffort": "Ajouter un effort",
"editEffort": "Modifier un effort"
}, },
"taskPriorities": { "taskPriorities": {
"created": "Priorité créée avec succès.", "created": "Priorité créée avec succès.",
"updated": "Priorité mise à jour avec succès.", "updated": "Priorité mise à jour avec succès.",
"deleted": "Priorité supprimée avec succès." "deleted": "Priorité supprimée avec succès.",
"addPriority": "Ajouter une priorité",
"editPriority": "Modifier une priorité"
}, },
"taskTags": { "taskTags": {
"created": "Tag créé avec succès.", "created": "Tag créé avec succès.",
"updated": "Tag mis à jour avec succès.", "updated": "Tag mis à jour avec succès.",
"deleted": "Tag supprimé avec succès." "deleted": "Tag supprimé avec succès.",
"addTag": "Ajouter un tag",
"editTag": "Modifier un tag"
}, },
"taskGroups": { "taskGroups": {
"created": "Groupe créé avec succès.", "created": "Groupe créé avec succès.",
"updated": "Groupe mis à jour avec succès.", "updated": "Groupe mis à jour avec succès.",
"deleted": "Groupe supprimé avec succès.", "deleted": "Groupe supprimé avec succès.",
"archived": "Groupe archivé avec succès.", "archived": "Groupe archivé avec succès.",
"unarchived": "Groupe désarchivé avec succès." "unarchived": "Groupe désarchivé avec succès.",
"addGroup": "Ajouter un groupe",
"editGroup": "Modifier un groupe"
}, },
"taskDocuments": { "taskDocuments": {
"title": "Documents", "title": "Documents",
@@ -78,17 +101,24 @@
"archived": "Ticket archivé avec succès.", "archived": "Ticket archivé avec succès.",
"unarchived": "Ticket désarchivé avec succès.", "unarchived": "Ticket désarchivé avec succès.",
"deleteConfirmTitle": "Supprimer le ticket", "deleteConfirmTitle": "Supprimer le ticket",
"deleteConfirmMessage": "Êtes-vous sûr de vouloir supprimer ce ticket ? Cette action est irréversible." "deleteConfirmMessage": "Êtes-vous sûr de vouloir supprimer ce ticket ? Cette action est irréversible.",
"addTask": "Ajouter un ticket",
"editTask": "Modifier un ticket"
}, },
"users": { "users": {
"created": "Utilisateur créé avec succès.", "created": "Utilisateur créé avec succès.",
"updated": "Utilisateur mis à jour avec succès.", "updated": "Utilisateur mis à jour avec succès.",
"deleted": "Utilisateur supprimé avec succès." "deleted": "Utilisateur supprimé avec succès.",
"addUser": "Ajouter un utilisateur",
"editUser": "Modifier un utilisateur"
}, },
"timeEntries": { "timeEntries": {
"created": "Temps enregistré", "created": "Temps enregistré",
"updated": "Temps modifié", "updated": "Temps modifié",
"deleted": "Temps supprimé" "deleted": "Temps supprimé",
"noEntries": "Aucune activité pour cette période",
"addEntry": "Ajouter une Activité",
"editEntry": "Modifier un temps"
}, },
"archive": { "archive": {
"title": "Archives", "title": "Archives",
@@ -169,7 +199,12 @@
"cancel": "Annuler", "cancel": "Annuler",
"save": "Enregistrer", "save": "Enregistrer",
"edit": "Modifier", "edit": "Modifier",
"delete": "Supprimer",
"add": "Ajouter",
"loading": "Chargement...", "loading": "Chargement...",
"archived": "Archivé",
"noClient": "Aucun client",
"untitled": "Sans titre",
"dateFilter": "Date", "dateFilter": "Date",
"today": "Aujourd'hui", "today": "Aujourd'hui",
"thisWeek": "Cette semaine", "thisWeek": "Cette semaine",

View File

@@ -2,7 +2,7 @@
<div> <div>
<div class="sticky top-8 z-20 bg-white pb-4 sm:top-12"> <div class="sticky top-8 z-20 bg-white pb-4 sm:top-12">
<div class="flex flex-wrap items-center justify-between gap-3"> <div class="flex flex-wrap items-center justify-between gap-3">
<h1 class="text-xl font-bold text-primary-500 sm:text-2xl">Projets</h1> <h1 class="text-xl font-bold text-primary-500 sm:text-2xl">{{ $t('projects.title') }}</h1>
<div class="flex items-center gap-2 sm:gap-3"> <div class="flex items-center gap-2 sm:gap-3">
<button <button
class="flex items-center gap-1.5 rounded-md px-2 py-2 text-sm font-medium transition sm:px-3" class="flex items-center gap-1.5 rounded-md px-2 py-2 text-sm font-medium transition sm:px-3"
@@ -18,8 +18,8 @@
class="shrink-0 rounded-md bg-primary-500 px-3 py-2 text-xs font-semibold text-white hover:bg-secondary-500 sm:px-4 sm:text-sm" class="shrink-0 rounded-md bg-primary-500 px-3 py-2 text-xs font-semibold text-white hover:bg-secondary-500 sm:px-4 sm:text-sm"
@click="openCreate" @click="openCreate"
> >
<span class="hidden sm:inline">+ Ajouter un projet</span> <span class="hidden sm:inline">+ {{ $t('projects.addProject') }}</span>
<span class="sm:hidden">+ Projet</span> <span class="sm:hidden">+ {{ $t('projects.addProjectShort') }}</span>
</button> </button>
</div> </div>
</div> </div>
@@ -40,7 +40,7 @@
v-if="project.archived" v-if="project.archived"
class="rounded bg-amber-100 px-1.5 py-0.5 text-xs font-medium text-amber-700" class="rounded bg-amber-100 px-1.5 py-0.5 text-xs font-medium text-amber-700"
> >
Archivé {{ $t('common.archived') }}
</span> </span>
</div> </div>
<button <button
@@ -59,7 +59,7 @@
v-if="projects.length === 0 && !isLoading" v-if="projects.length === 0 && !isLoading"
class="col-span-full py-12 text-center text-neutral-400" class="col-span-full py-12 text-center text-neutral-400"
> >
{{ showArchived ? 'Aucun projet archivé.' : 'Aucun projet trouvé.' }} {{ showArchived ? $t('projects.noArchivedProjects') : $t('projects.noProjects') }}
</div> </div>
</div> </div>