Système de gestion de traductions pensé en CMS headless : un back-office pour ceux qui traduisent, une API pour ce qui consomme. Architecture - Symfony 7.4 / API Platform 4.3 / MariaDB 11.4, SPA React 19 servie en même origine — ce qui rend viable le cookie de session plutôt qu'un jeton en localStorage. - Deux APIs séparées : Management (session ou clé) et Delivery (stateless, clé seule). Les fusionner ferait porter à chaque lecture de bundle le coût de la session. - Stockage canonique en ICU MessageFormat, sérialisation par plateforme. Le format d'une plateforme ne contamine pas la base. - Publication par releases immuables ; le déploiement est un déplacement de pointeur, donc le rollback aussi. - Isolation multi-organisation par filtre Doctrine, avec un test d'architecture qui casse la CI si une entité échappe à l'invariant. Éditeur, deux vues - Par langue : source et cible, jamais douze colonnes. Grille virtualisée, saisie sans bouton « Enregistrer », panneau de contexte permanent. - Par clé : une clé, toutes ses langues empilées et repliées. Répond à « ce libellé est-il prêt partout ? ». - Mode Focus dans les deux : une file à vider, ⌘↵ pour enchaîner. - Le traducteur ne voit jamais d'ICU : pastilles de variables, un champ par catégorie CLDR de la langue cible. Administration - Deux niveaux : projet (membres, clés API, plateformes) et organisation (annuaire des comptes, création de projets). - Invitations par e-mail, jeton 256 bits stocké haché. - Désactiver un compte coupe les sessions en cours, pas seulement les connexions suivantes. - Les plateformes s'archivent ; ni elles ni les environnements ne se suppriment — la trace explique pourquoi telle clé existe. CLI tqs - PHAR autonome de 3 Mo, autoloader généré : le dépôt client ne dépend ni de Composer ni de la disponibilité de TQ-Slator. - init / push / pull / status ; le sync est non destructif par défaut et son prune est scopé plateforme. 126 tests, PHPStan niveau 8. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
107 lines
3.1 KiB
TypeScript
107 lines
3.1 KiB
TypeScript
import type { TranslationStatus } from '@/api/types';
|
|
|
|
/**
|
|
* Signalétique des statuts — une seule définition, utilisée partout.
|
|
*
|
|
* Centralisée volontairement : cinq couleurs qui varieraient d'un écran à
|
|
* l'autre obligeraient l'utilisateur à réapprendre le code à chaque page, ce
|
|
* qui annule tout l'intérêt d'un code couleur.
|
|
*/
|
|
export const STATUS_META: Record<
|
|
TranslationStatus,
|
|
{ label: string; dot: string; text: string; bg: string }
|
|
> = {
|
|
untranslated: {
|
|
label: 'À traduire',
|
|
dot: 'bg-ink-300',
|
|
text: 'text-ink-500',
|
|
bg: 'bg-ink-100',
|
|
},
|
|
draft: {
|
|
label: 'Brouillon',
|
|
dot: 'bg-amber-500',
|
|
text: 'text-amber-700',
|
|
bg: 'bg-amber-50',
|
|
},
|
|
// Le seul statut qui désigne un bug en production : il doit être le plus
|
|
// visible de l'interface, d'où le rouge, réservé à lui seul.
|
|
needs_review: {
|
|
label: 'À revoir',
|
|
dot: 'bg-red-500',
|
|
text: 'text-red-700',
|
|
bg: 'bg-red-50',
|
|
},
|
|
translated: {
|
|
label: 'Traduite',
|
|
dot: 'bg-blue-500',
|
|
text: 'text-blue-700',
|
|
bg: 'bg-blue-50',
|
|
},
|
|
reviewed: {
|
|
label: 'Validée',
|
|
dot: 'bg-emerald-500',
|
|
text: 'text-emerald-700',
|
|
bg: 'bg-emerald-50',
|
|
},
|
|
};
|
|
|
|
export function StatusDot({ status, title }: { status: TranslationStatus; title?: string }) {
|
|
const meta = STATUS_META[status];
|
|
|
|
return (
|
|
<span
|
|
className={`inline-block size-2 shrink-0 rounded-full ${meta.dot}`}
|
|
title={title ?? meta.label}
|
|
aria-label={meta.label}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function StatusBadge({ status }: { status: TranslationStatus }) {
|
|
const meta = STATUS_META[status];
|
|
|
|
return (
|
|
<span
|
|
className={`inline-flex items-center gap-1.5 rounded px-1.5 py-0.5 text-[11px] font-medium ${meta.bg} ${meta.text}`}
|
|
>
|
|
<span className={`size-1.5 rounded-full ${meta.dot}`} />
|
|
{meta.label}
|
|
</span>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Barre d'avancement segmentée.
|
|
*
|
|
* Une seule barre de pourcentage cacherait la répartition : « 60 % » ne dit pas
|
|
* s'il reste du travail de traduction ou de relecture, ni combien de traductions
|
|
* sont devenues obsolètes. Les segments répondent aux trois questions d'un coup.
|
|
*/
|
|
export function ProgressBar({
|
|
reviewed,
|
|
translated,
|
|
needsReview,
|
|
draft,
|
|
total,
|
|
}: {
|
|
reviewed: number;
|
|
translated: number;
|
|
needsReview: number;
|
|
draft: number;
|
|
total: number;
|
|
}) {
|
|
if (total === 0) {
|
|
return <div className="h-1.5 w-full rounded-full bg-ink-200" />;
|
|
}
|
|
|
|
const pct = (value: number) => `${(value / total) * 100}%`;
|
|
|
|
return (
|
|
<div className="flex h-1.5 w-full overflow-hidden rounded-full bg-ink-200">
|
|
<div className="bg-emerald-500" style={{ width: pct(reviewed) }} />
|
|
<div className="bg-blue-500" style={{ width: pct(translated) }} />
|
|
<div className="bg-red-500" style={{ width: pct(needsReview) }} />
|
|
<div className="bg-amber-500" style={{ width: pct(draft) }} />
|
|
</div>
|
|
);
|
|
}
|