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>
<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">
<MalioInputText
v-model="form.name"

View File

@@ -1,5 +1,5 @@
<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">
<MalioInputText
v-model="form.code"

View File

@@ -1,5 +1,5 @@
<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">
<MalioInputText
v-model="form.title"
@@ -267,7 +267,7 @@ async function handleArchive() {
if (timerStore.activeEntry?.task) {
const taskIri = typeof timerStore.activeEntry.task === 'string'
? 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}`) {
await timerStore.stop()
}

View File

@@ -1,5 +1,5 @@
<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">
<MalioInputText
v-model="form.label"

View File

@@ -1,5 +1,5 @@
<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">
<MalioInputText
v-model="form.title"

View File

@@ -24,7 +24,7 @@
{{ task.project.code }}-{{ task.number }}
</span>
<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>
</div>
<button
@@ -568,7 +568,7 @@ async function handleArchive() {
if (timerStore.activeEntry?.task) {
const taskIri = typeof timerStore.activeEntry.task === 'string'
? 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}`) {
await timerStore.stop()
}

View File

@@ -1,5 +1,5 @@
<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">
<MalioInputText
v-model="form.label"

View File

@@ -1,5 +1,5 @@
<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">
<MalioInputText
v-model="form.label"

View File

@@ -1,5 +1,5 @@
<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">
<MalioInputText
v-model="form.label"

View File

@@ -21,7 +21,7 @@
<!-- Full display: title + project + type dot + duration -->
<template v-if="sizeLevel >= 3">
<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>
</div>
<div v-if="entry.project" class="truncate text-[10px] opacity-80">{{ entry.project.name }}</div>
@@ -39,13 +39,13 @@
<!-- Medium: title + duration -->
<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>
</template>
<!-- Small: title only -->
<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>
<!-- Tiny: just a colored bar, no text -->

View File

@@ -1,5 +1,5 @@
<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">
<div>
<label class="mb-1 block text-sm font-semibold text-neutral-700">Titre</label>
@@ -117,7 +117,7 @@
</template>
<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 { Project } from '~/services/dto/project'
import type { TaskTag } from '~/services/dto/task-tag'
@@ -257,7 +257,7 @@ async function onSubmit() {
if (isEditing.value && props.entry) {
await update(props.entry.id, payload)
} else {
await create(payload as any)
await create(payload as TimeEntryWrite)
}
emit('saved')

View File

@@ -1,7 +1,7 @@
<template>
<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">
Aucune activité pour cette période
{{ $t('timeEntries.noEntries') }}
</div>
<div
@@ -20,7 +20,7 @@
<div class="min-w-0 flex-1">
<div class="flex items-center gap-2">
<span class="truncate text-sm font-semibold text-neutral-900">
{{ entry.title || 'Sans titre' }}
{{ entry.title || $t('common.untitled') }}
</span>
<span
v-for="tag in entry.tags"
@@ -56,7 +56,7 @@
<!-- Delete action -->
<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"
title="Supprimer"
:title="$t('common.delete')"
@click.stop="emit('deleteEntry', entry)"
>
<Icon name="mdi:delete-outline" size="18" />

View File

@@ -99,7 +99,7 @@
:style="{ backgroundColor: entry.project?.color ?? '#94a3b8' }"
/>
<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">
{{ formatTime(entry.startedAt) }} {{ entry.stoppedAt ? formatTime(entry.stoppedAt) : '...' }}
</div>
@@ -141,6 +141,8 @@
<script setup lang="ts">
import type { TimeEntry } from '~/services/dto/time-entry'
const { t } = useI18n()
const props = defineProps<{
entries: TimeEntry[]
startDate: Date
@@ -459,7 +461,7 @@ function onMoveStart(payload: { entry: TimeEntry; offsetY: number }, sourceDayIn
dragState.value = {
entryId: entry.id,
entry,
title: entry.title || 'Sans titre',
title: entry.title || t('common.untitled'),
color: entry.project?.color ?? '#94a3b8',
durationMinutes,
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="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">
<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">
{{ taskCount }} tâche{{ taskCount > 1 ? 's sont liées' : ' est liée' }} à ce statut.
Choisissez les déplacer :
{{ taskCount > 1 ? $t('taskStatuses.linkedTasksPlural', { count: taskCount }) : $t('taskStatuses.linkedTasks', { count: taskCount }) }}
</p>
<div class="mt-4">
<MalioSelect
v-model="targetStatusId"
:options="targetOptions"
label="Déplacer vers"
empty-option-label="Backlog (sans statut)"
:label="$t('taskStatuses.moveTo')"
:empty-option-label="$t('taskStatuses.backlog')"
min-width="w-full"
/>
</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"
@click="cancel"
>
Annuler
{{ $t('common.cancel') }}
</button>
<button
type="button"
@@ -35,7 +34,7 @@
:disabled="isProcessing"
@click="confirm"
>
Supprimer
{{ $t('common.delete') }}
</button>
</div>
</div>

View File

@@ -22,43 +22,66 @@
"clients": {
"created": "Client créé 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": {
"title": "Projets",
"created": "Projet créé avec succès.",
"updated": "Projet mis à jour avec succès.",
"deleted": "Projet supprimé avec succès.",
"archived": "Projet archivé avec succès.",
"unarchived": "Projet désarchivé avec succè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": {
"created": "Statut créé 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": {
"created": "Effort créé 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": {
"created": "Priorité créée 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": {
"created": "Tag créé 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": {
"created": "Groupe créé avec succès.",
"updated": "Groupe mis à jour avec succès.",
"deleted": "Groupe supprimé 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": {
"title": "Documents",
@@ -78,17 +101,24 @@
"archived": "Ticket archivé avec succès.",
"unarchived": "Ticket désarchivé avec succès.",
"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": {
"created": "Utilisateur créé 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": {
"created": "Temps enregistré",
"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": {
"title": "Archives",
@@ -169,7 +199,12 @@
"cancel": "Annuler",
"save": "Enregistrer",
"edit": "Modifier",
"delete": "Supprimer",
"add": "Ajouter",
"loading": "Chargement...",
"archived": "Archivé",
"noClient": "Aucun client",
"untitled": "Sans titre",
"dateFilter": "Date",
"today": "Aujourd'hui",
"thisWeek": "Cette semaine",

View File

@@ -2,7 +2,7 @@
<div>
<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">
<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">
<button
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"
@click="openCreate"
>
<span class="hidden sm:inline">+ Ajouter un projet</span>
<span class="sm:hidden">+ Projet</span>
<span class="hidden sm:inline">+ {{ $t('projects.addProject') }}</span>
<span class="sm:hidden">+ {{ $t('projects.addProjectShort') }}</span>
</button>
</div>
</div>
@@ -40,7 +40,7 @@
v-if="project.archived"
class="rounded bg-amber-100 px-1.5 py-0.5 text-xs font-medium text-amber-700"
>
Archivé
{{ $t('common.archived') }}
</span>
</div>
<button
@@ -59,7 +59,7 @@
v-if="projects.length === 0 && !isLoading"
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>