diff --git a/.impeccable/critique/2026-08-30T19-50-34Z__frontend-src.md b/.impeccable/critique/2026-08-30T19-50-34Z__frontend-src.md new file mode 100644 index 0000000..0ac20b5 --- /dev/null +++ b/.impeccable/critique/2026-08-30T19-50-34Z__frontend-src.md @@ -0,0 +1,73 @@ +--- +target: frontend/src (UI complète Wordly.art) +total_score: 24 +max_score: 40 +na_heuristics: +p0_count: 0 +p1_count: 3 +timestamp: 2026-08-30T19-50-34Z +slug: frontend-src +--- +# Critique de design — Wordly.art (`frontend/src`) — re-run post-correctif + +Method: dual-agent (A: agent_8b2492dd-bd0c-48a2-9293-d6c7960dc418 · B: agent_d8eeec19-1e9d-4df5-84a2-7eb2daf6beb0) + +## Design Health Score + +| # | Heuristique | Score | Problème clé | Δ | +|---|-------------|-------|--------------|---| +| 1 | Visibilité du statut | 3 | Progression aria-live, temps restant API affiché, skeletons ; mais « Tout approuver » boucle sans progression (`reviews/[jobId]/page.tsx:138-148`) et la sauvegarde des consignes simule un délai de 300 ms (`glossaries/page.tsx:64`). | +1 | +| 2 | Correspondance avec le réel | 2 | Jargon non défini : bouton nu « XLIFF » (`reviews/[jobId]/page.tsx:253`), « Phase 2 : Reconstruction contextuelle » (`fr/translate.json:70`) ; membre d'équipe affiché en `user_id.slice(0,8)…` (`teams/page.tsx:197`) ; « Standard » codé en dur EN (`ProviderSelector.tsx:161`). | = | +| 3 | Contrôle et liberté | 3 | Reprise du job après refresh (`useTranslationSubmit.ts:257-278`), sortie honnête « la traduction continue en arrière-plan » ; mais aucune annulation réelle d'un job, et le consentement cookies n'est plus rouvrable (`cookie-consent.tsx:14-26`). | +1 | +| 4 | Cohérence et standards | 2 | Trois systèmes visuels (éditorial serif / font-black pricing-settings / shadcn brut reviews-teams) ; `GlossaryCard.tsx` mort, jamais importé ; aria-label « Close notification » non traduit (`notification.tsx:184`). | +1 | +| 5 | Prévention des erreurs | 3 | Validation extension+taille, avertissements mismatch glossaire avant soumission, submit désactivé avec raison ; mais rien n'empêche source = cible (`useTranslationConfig.ts:233-237`). | +1 | +| 6 | Reconnaissance plutôt que rappel | 3 | Traductions récentes accessibles, récap config pendant l'attente ; mais actions de ligne icône-seule avec `title` seulement (`reviews/[jobId]/page.tsx:364-395`) et onglet « Consignes de contexte » caché sous Glossaires. | = | +| 7 | Flexibilité et efficacité | 1 | Aucun raccourci clavier global ; upload strictement mono-fichier (pas de `multiple`) ; historique plafonné à 8 jobs locaux, aucune page historique serveur ; pas de navigation fléchée dans le combobox langues. | = | +| 8 | Esthétique et minimalisme | 2 | Pricing = 5 plans + comparatif 10 lignes + 4 packs crédits + 4 trust + 2 modèles IA + 7 FAQ ; hiérarchie inversée sur translate — titre décoratif 5xl plus fort que le CTA 12px (`translate/page.tsx:230` vs `:351`). | = | +| 9 | Récupération d'erreurs | 3 | Humanisation en 9 familles, retry avec fichier conservé, cap polling réseau clair ; mais teams/reviews affichent `err.message` brut (`teams/page.tsx:102-105`). | = | +| 10 | Aide et documentation | 2 | Aides contextuelles réelles (glossaire, providers) mais zéro centre d'aide, zéro onboarding première connexion. | = | +| **Total** | | **24/40** | **Acceptable — en limite haute, la bande « Good » (28+) est atteignable** | **+4** | + +## Verdict de spécificité design + +**LLM assessment** : la landing est « presque signée » — le langage éditorial (Playfair/crème/encre/or) est tenu jusqu'au dashboard, et `SplitTitle` (base + accent italique, `translate/page.tsx:62-64`) est une vraie signature. Le dashboard translate est la meilleure surface : pipeline Upload→Search→Languages→Wrench→Check, glossaire avec paires de langues et ajout inline, providers thémés — rien de réutilisable tel quel par un autre produit. Restent trois failles d'identité : le hero landing (photo Unsplash + fausses cartes UI ne correspondant à aucun écran réel, `landing-page.tsx:139-167`), la page pricing qui parle une typographie opposée (`font-black uppercase tracking-tighter`, `pricing/page.tsx:504`), et reviews/teams jamais passés au design (shadcn brut). Occasion manquée majeure : aucun avant/après de document — LE visuel de la promesse « keep the format ». + +**Deterministic scan** : 3 findings (4 avant le correctif). Le `side-tab` du profil est corrigé. Restent 2 `overused-font` Inter (`globals.css:141,147`) — choix assumé « outil sobre » de l'utilisateur, documenté — et 1 `border-accent-on-rounded` (`globals.css:215`) : **faux positif manifeste** (spinner canonique). + +**Overlays navigateur** : indisponibles — « Browser is not available in subagent » (identique au run précédent, tentative réelle faite). Fallback : scan CLI. + +## Impression générale + +Le cœur produit est devenu solide : machine à états propre, reprise après refresh — « peu de SaaS gèrent le rafraîchissement en cours de traitement aussi proprement » —, erreurs humanisées, prévention au point de décision. La confiance n'est plus sabordée. Le plafond suivant se joue sur trois fronts : la conformité AA des micro-labels, l'unification des trois systèmes visuels, et les accélérateurs (batch, raccourcis, historique serveur) qui font la différence pour les utilisateurs intensifs. + +## Ce qui fonctionne + +1. **La machine à états + persistance du job** : cinq états nets, en-tête réécrit par état, progression réelle backend, localStorage avec TTL 24 h et reprise automatique (`useTranslationSubmit.ts:253-278`). +2. **L'identité éditoriale tenue de la landing au dashboard** : crème/encre/or + serif d'accent + pills, dark mode cohérent, `prefers-reduced-motion`/`prefers-contrast` gérés. +3. **La prévention et la récupération au point de décision** : mapping 9 familles d'erreurs + avertissements mismatch glossaire avant soumission (`GlossarySelector.tsx:340-357`). + +## Problèmes prioritaires + +1. **[P1] Contraste des micro-labels : illisibles et non-conformes AA.** Textes fonctionnels 9-11px en /20-/40 : nav sidebar `/40` (`DashboardSidebar.tsx:45-48`), stats pricing `text-[9px]/40` (`pricing/page.tsx:636`), « .DOCX » `/20` (`landing-page.tsx:307`). /40 sur crème ≈ 2.5:1. → Fix : plancher 11px, opacité ≥65 %, /20-/30 réservé à la décoration. **→ `$impeccable audit` + `$impeccable harden`** +2. **[P1] Trois systèmes de design qui ne se parlent pas.** Pricing/settings/services en `font-black uppercase` vs translate/glossaries en serif éditorial vs reviews/teams en shadcn brut ; `GlossaryCard.tsx` mort et divergent. → Fix : composant `PageHeader` unique (pill + titre base/accent) imposé partout ; supprimer GlossaryCard. **→ `$impeccable layout` + `$impeccable typeset`** +3. **[P1] UI non traduite dans un produit de traduction.** Chips de suggestions des consignes codées en dur FR (`glossaries/page.tsx:22-29`), « Standard » EN (`ProviderSelector.tsx:161`), « Close notification » (`notification.tsx:184`), strings FR sans accents dans `model-combobox.tsx:32,114,135`, littéraux `|| 'français'` résiduels. → Fix : extraire en clés, bannir le littéral JSX. **→ `$impeccable harden`** +4. **[P2] Actions icône-seule et identité technique exposée.** Éditer/approuver/reset en icônes 12px avec `title` seulement ; membres affichés `user_id.slice(0,8)…`. → Fix : boutons textuels compacts ou icône + libellé visible ; afficher e-mail/nom via l'API. **→ `$impeccable clarify`** +5. **[P2] Aucun accélérateur pour les utilisateurs intensifs.** Pas de Ctrl+Entrée, upload mono-fichier, historique 8 items locaux sans page serveur, pas de fléchées dans le combobox langues (40 options = 40 Tab). → Fix : raccourci, `multiple` + file d'attente, page Historique API, roving tabindex. **→ `$impeccable shape`** + +## Red flags personas + +**Alex (expert pressé)** : la config par défaut rend le flux rapide — mais 10 fichiers = 10 parcours complets (pas de `multiple`) ; « + Nouvelle traduction » réinitialise tout sans « re-traduire avec la même config » ; historique limité à 8 items locaux ; « Tout approuver » sans confirmation ni undo sur 400 segments. + +**Sam (accessibilité)** : les fondations sont désormais réelles — `:focus-visible` global, dropzone focusable avec rôle + Entrée/Espace, progression aria-live, `role="switch"` : du sérieux. Échecs restants : contrastes /40-/20 massifs, dropdowns glossaire sans Échap ni gestion de focus out (`GlossarySelector.tsx:121-129`), combobox langues sans navigation fléchée, « edited » signifié par la couleur seule dans la cellule (sauvé par le badge). + +**Jordan (premier usage)** : dropzone auto-explicative, chips rassurantes. Mais « XLIFF »/« Reconstruire et télécharger » sans explication, « Phase 2 : Reconstruction contextuelle » jargon interne, onglet « Consignes de contexte » introuvable par déduction, aucun onboarding première connexion, erreur 60 Mo sans limite affichée ni conseil. + +## Observations mineures + +Cookie consent non rouvrable + emoji 🍪 + chevauchement possible avec la barre mobile · hero Unsplash → vrai screenshot produit · mailto: brut pour Enterprise (sortie d'app) · switch « Traduire les images » fantôme pour les free · timer `formatElapsed` remis à 0 sur certains changements de statut · clés mortes fr/translate.json (« 100% SÉCURISÉ », « Qualité Maître », cancelProcess) · `model-combobox.tsx` référence des tokens inexistants (`bg-surface`, `border-border-subtle`) · nav dupliquée sidebar/header mobile · auto-checkout silencieux par paramètre d'URL `?plan=` sans re-confirmation (`pricing/page.tsx:339-349`). + +## Questions à considérer + +1. Pourquoi montrer six groupes de configuration à un free dont la moitié est verrouillée ? Que perdriez-vous à une page translate free de trois contrôles exactement ? +2. Où est le avant/après ? La promesse unique est « keep the format » et nulle part l'UI ne montre un document source à côté de sa traduction — surtout en RTL. +3. Pourquoi le paiement, moment de plus haute anxiété, est-il le seul sans écran de confirmation — et combien d'auto-checkouts silencieux par URL coûtent en litiges ? diff --git a/frontend/src/app/dashboard/DashboardHeader.tsx b/frontend/src/app/dashboard/DashboardHeader.tsx index 12cfbb5..abbcf5e 100644 --- a/frontend/src/app/dashboard/DashboardHeader.tsx +++ b/frontend/src/app/dashboard/DashboardHeader.tsx @@ -31,7 +31,7 @@ export function DashboardHeader() {
{/* Mobile menu button */} - - - - - - ); -}); diff --git a/frontend/src/app/dashboard/glossaries/page.tsx b/frontend/src/app/dashboard/glossaries/page.tsx index 2634a6e..980370e 100644 --- a/frontend/src/app/dashboard/glossaries/page.tsx +++ b/frontend/src/app/dashboard/glossaries/page.tsx @@ -19,13 +19,14 @@ import { SUPPORTED_LANGUAGES } from './types'; import { useTranslationStore } from '@/lib/store'; // ── Chips de suggestions pour les consignes de contexte ───────────────────── -const CONTEXT_SUGGESTIONS = [ - { label: 'Ton formel', value: 'Utilise toujours un ton formel et professionnel dans tes traductions.' }, - { label: 'Noms propres', value: 'Ne traduis pas les noms propres, marques et noms de personnes.' }, - { label: 'Chiffres', value: 'Garde tous les chiffres, pourcentages et montants tels quels sans les modifier.' }, - { label: 'Placeholders', value: 'Ne traduis pas les variables entre accolades comme {nom}, {date}, {montant}.' }, - { label: 'Termes techniques', value: 'Conserve les termes techniques en langue originale et ne les traduis pas.' }, - { label: 'Style concis', value: 'Préfère des formulations courtes et directes. Évite les périphrases.' }, +// Prompt bodies stay in French (LLM content); labels translate with the UI. +const CONTEXT_SUGGESTIONS: { labelKey: string; value: string }[] = [ + { labelKey: 'glossaries.suggestion.formal', value: 'Utilise toujours un ton formel et professionnel dans tes traductions.' }, + { labelKey: 'glossaries.suggestion.proprietary', value: 'Ne traduis pas les noms propres, marques et noms de personnes.' }, + { labelKey: 'glossaries.suggestion.numbers', value: 'Garde tous les chiffres, pourcentages et montants tels quels sans les modifier.' }, + { labelKey: 'glossaries.suggestion.placeholders', value: 'Ne traduis pas les variables entre accolades comme {nom}, {date}, {montant}.' }, + { labelKey: 'glossaries.suggestion.technical', value: 'Conserve les termes techniques en langue originale et ne les traduis pas.' }, + { labelKey: 'glossaries.suggestion.concise', value: 'Préfère des formulations courtes et directes. Évite les périphrases.' }, ]; export default function GlossariesPage() { @@ -225,11 +226,11 @@ export default function GlossariesPage() {
{CONTEXT_SUGGESTIONS.map((s) => ( ))}
diff --git a/frontend/src/app/dashboard/reviews/[jobId]/page.tsx b/frontend/src/app/dashboard/reviews/[jobId]/page.tsx index 65f15bc..41cd93d 100644 --- a/frontend/src/app/dashboard/reviews/[jobId]/page.tsx +++ b/frontend/src/app/dashboard/reviews/[jobId]/page.tsx @@ -17,6 +17,7 @@ import { Button } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { useToast } from '@/components/ui/toast'; import { useI18n } from '@/lib/i18n'; +import { PageHeader } from '@/components/layout/page-header'; import { cn } from '@/lib/utils'; interface Segment { @@ -77,6 +78,7 @@ export default function ReviewPage() { const [draft, setDraft] = useState(''); const [savingId, setSavingId] = useState(null); const [isRebuilding, setIsRebuilding] = useState(false); + const [approveArmed, setApproveArmed] = useState(false); const [isImporting, setIsImporting] = useState(false); const fileInputRef = useRef(null); @@ -136,6 +138,13 @@ export default function ReviewPage() { }; const approveAll = async () => { + // Two-step: the first click arms, the second confirms — no accidental bulk-approve. + if (!approveArmed) { + setApproveArmed(true); + setTimeout(() => setApproveArmed(false), 4000); + return; + } + setApproveArmed(false); const pending = segments.filter((s) => s.status === 'pending'); for (const seg of pending) { // sequential is fine — server-side each is a tiny PATCH @@ -232,10 +241,13 @@ export default function ReviewPage() { return (
-
+
-

{t('reviews.title')}

-

+ {t('reviews.pill')} +

+ {t('reviews.titleBase')} {t('reviews.titleAccent')} +

+

{t('reviews.subtitle', { file: fileName ?? jobId, total: counts.total, @@ -246,8 +258,11 @@ export default function ReviewPage() {

-
)} diff --git a/frontend/src/app/dashboard/services/page.tsx b/frontend/src/app/dashboard/services/page.tsx index 96ddbe9..cdb7293 100644 --- a/frontend/src/app/dashboard/services/page.tsx +++ b/frontend/src/app/dashboard/services/page.tsx @@ -4,6 +4,7 @@ import { useEffect, useState } from 'react'; import { Zap, CheckCircle2, Lock, Loader2, Globe, Brain } from 'lucide-react'; import { API_BASE } from '@/lib/config'; import { useI18n } from '@/lib/i18n'; +import { PageHeader } from '@/components/layout/page-header'; const FALLBACK_PROVIDERS_FACTORY = (t: (k: string) => string) => [ { id: "google", label: t('services.fallback.google.label'), description: t('services.fallback.google.desc'), mode: "classic" as const }, @@ -52,13 +53,13 @@ export default function TranslationServicesPage() { return (
{/* ── Editorial Header ───────────────────────────────────── */} -
- {t('services.pill')} -

- {t('services.title')} -

-

{t('services.subtitle')}

-
+ {isLoading ? (
diff --git a/frontend/src/app/dashboard/settings/page.tsx b/frontend/src/app/dashboard/settings/page.tsx index 99af279..2172ff5 100644 --- a/frontend/src/app/dashboard/settings/page.tsx +++ b/frontend/src/app/dashboard/settings/page.tsx @@ -5,6 +5,7 @@ import { Settings, Trash2, Loader2, FileSpreadsheet, FileText, Presentation, } from 'lucide-react'; import { useI18n } from '@/lib/i18n'; +import { PageHeader } from '@/components/layout/page-header'; export default function GeneralSettingsPage() { const { t } = useI18n(); @@ -38,13 +39,13 @@ export default function GeneralSettingsPage() { return (
{/* ── Editorial Header ───────────────────────────────────── */} -
- {t('settings.pill')} -

- {t('settings.title')} -

-

{t('settings.subtitle')}

-
+
{/* Supported Formats */} diff --git a/frontend/src/app/dashboard/teams/page.tsx b/frontend/src/app/dashboard/teams/page.tsx index c6841d2..e6b0057 100644 --- a/frontend/src/app/dashboard/teams/page.tsx +++ b/frontend/src/app/dashboard/teams/page.tsx @@ -16,6 +16,7 @@ import { } from '@/components/ui/select'; import { useToast } from '@/components/ui/toast'; import { useI18n } from '@/lib/i18n'; +import { PageHeader } from '@/components/layout/page-header'; interface WorkspaceMember { id: string; @@ -133,19 +134,12 @@ export default function TeamsPage() { return (
-
-
-
- -
-
-

{t('teams.title')}

-

- {t('teams.subtitle')} -

-
-
-
+ {needsBusiness ? (
diff --git a/frontend/src/app/dashboard/translate/LanguageSelector.tsx b/frontend/src/app/dashboard/translate/LanguageSelector.tsx index f3ee530..b828729 100644 --- a/frontend/src/app/dashboard/translate/LanguageSelector.tsx +++ b/frontend/src/app/dashboard/translate/LanguageSelector.tsx @@ -70,16 +70,34 @@ function Combobox({ const label = value === 'auto' ? autoLabel : allOptions.find(l => l.code === value)?.name ?? value; + const handleListboxKeys = (e: React.KeyboardEvent) => { + if (e.key === 'Escape' && open) { + e.stopPropagation(); + closeAndRefocus(); + return; + } + if (!open || (e.key !== 'ArrowDown' && e.key !== 'ArrowUp')) return; + e.preventDefault(); + const options = Array.from( + ref.current?.querySelectorAll('[role="option"]') ?? [] + ); + if (options.length === 0) return; + const idx = options.indexOf(document.activeElement as HTMLButtonElement); + const next = e.key === 'ArrowDown' + ? options[Math.min(options.length - 1, idx + 1)] + : options[Math.max(0, idx - 1)]; + // from the search input, ArrowDown goes to the first option + (idx === -1 && document.activeElement === inputRef.current + ? options[e.key === 'ArrowDown' ? 0 : options.length - 1] + : next + ).focus(); + }; + return (
{ - if (e.key === 'Escape' && open) { - e.stopPropagation(); - closeAndRefocus(); - } - }} + onKeyDown={handleListboxKeys} > + + {t('translate.recent.review')} + + ))} @@ -446,21 +468,18 @@ export default function TranslatePage() {

- {t('translate.header.completedTitle')} + {t('translate.header.completedTitleBase')} {t('translate.header.completedTitleAccent')}

{submit.fileName}

- - {t('translate.complete.masterQuality')} -
+ {/* ── Checkout confirmation — no Stripe redirect without an explicit confirm ── */} + { if (!open) setConfirmPlan(null); }}> + + + {t('pricing.confirm.title')} + {t('pricing.confirm.subtitle')} + + {confirmPlan && ( +
+
+ {t(confirmPlan.name)} + + {isYearly + ? `${Number(confirmPlan.price_yearly.toFixed(2))} € / ${t('pricing.confirm.year')}` + : `${Number(confirmPlan.price_monthly.toFixed(2))} € / ${t('pricing.confirm.month')}`} + +
+ {isYearly && confirmPlan.price_yearly > 0 && ( +

+ {t('pricing.confirm.monthlyEquivalent', { price: (confirmPlan.price_yearly / 12).toFixed(2) })} +

+ )} +

+ {t('pricing.confirm.secureNote')} +

+
+ )} + + + + +
+
+ {/* ── Toast notification ── */} {toastMsg && (
- -
- {t('pricing.header.badge')} + + -

+

{t('pricing.header.titleBase')}{" "} - + {t('pricing.header.titleAccent')}

-

+

{t('pricing.header.subtitle')}

@@ -560,12 +605,12 @@ export default function PricingPage() { {plan.popular && (
{plan.badge && ( - + {t(plan.badge)} )} {isCurrent && ( - +
{t('pricing.card.myPlan')} )} @@ -573,7 +618,7 @@ export default function PricingPage() { )} {plan.badge && !plan.popular && (
- + {t(plan.badge)}
@@ -581,7 +626,7 @@ export default function PricingPage() { {/* "My Plan" badge for current non-popular plan */} {isCurrent && !plan.popular && (
- +
{t('pricing.card.myPlan')}
@@ -608,7 +653,7 @@ export default function PricingPage() { ) : ( <>

{price} €

- {t('pricing.card.perMonth')} + {t('pricing.card.perMonth')} )}
@@ -632,8 +677,8 @@ export default function PricingPage() {
- - {t('pricing.card.documents')} + + {t('pricing.card.documents')}
{plan.docs_per_month === -1 ? t('pricing.card.unlimited') : `${plan.docs_per_month} ${t('pricing.card.perMonthStat')}`} @@ -641,8 +686,8 @@ export default function PricingPage() {
- - {t('pricing.card.pagesMax')} + + {t('pricing.card.pagesMax')}
{plan.max_pages_per_doc === -1 ? t('pricing.card.unlimited') : `${plan.max_pages_per_doc} ${t('pricing.card.perDoc')}`} @@ -658,7 +703,7 @@ export default function PricingPage() {
) : (
@@ -222,9 +222,9 @@ const FeaturesContext = () => {
{[ - { icon: , title: t('landing.features.context.title'), desc: t('landing.features.context.desc') }, - { icon: , title: t('landing.features.glossary.title'), desc: t('landing.features.glossary.desc') }, - { icon: , title: t('landing.features.vision.title'), desc: t('landing.features.vision.desc') }, + { icon: , title: t('landing.features.context.title'), desc: t('landing.features.context.desc') }, + { icon: , title: t('landing.features.glossary.title'), desc: t('landing.features.glossary.desc') }, + { icon: , title: t('landing.features.vision.title'), desc: t('landing.features.vision.desc') }, ].map((item, i) => (
@@ -232,7 +232,7 @@ const FeaturesContext = () => {

{item.title}

-

{item.desc}

+

{item.desc}

))} @@ -283,10 +283,10 @@ const LayoutFeatures = () => { const FormatsSection = () => { const { t } = useI18n(); const formats = [ - { id: ".DOCX", name: t('landing.formats.word.name'), icon: , items: [t('landing.formats.word.i1'), t('landing.formats.word.i2'), t('landing.formats.word.i3'), t('landing.formats.word.i4')] }, - { id: ".XLSX", name: t('landing.formats.excel.name'), icon: , items: [t('landing.formats.excel.i1'), t('landing.formats.excel.i2'), t('landing.formats.excel.i3'), t('landing.formats.excel.i4')] }, - { id: ".PPTX", name: t('landing.formats.pptx.name'), icon: , items: [t('landing.formats.pptx.i1'), t('landing.formats.pptx.i2'), t('landing.formats.pptx.i3'), t('landing.formats.pptx.i4')] }, - { id: ".PDF", name: t('landing.formats.pdf.name'), icon: , items: [t('landing.formats.pdf.i1'), t('landing.formats.pdf.i2'), t('landing.formats.pdf.i3'), t('landing.formats.pdf.i4')] }, + { id: ".DOCX", name: t('landing.formats.word.name'), icon: , items: [t('landing.formats.word.i1'), t('landing.formats.word.i2'), t('landing.formats.word.i3'), t('landing.formats.word.i4')] }, + { id: ".XLSX", name: t('landing.formats.excel.name'), icon: , items: [t('landing.formats.excel.i1'), t('landing.formats.excel.i2'), t('landing.formats.excel.i3'), t('landing.formats.excel.i4')] }, + { id: ".PPTX", name: t('landing.formats.pptx.name'), icon: , items: [t('landing.formats.pptx.i1'), t('landing.formats.pptx.i2'), t('landing.formats.pptx.i3'), t('landing.formats.pptx.i4')] }, + { id: ".PDF", name: t('landing.formats.pdf.name'), icon: , items: [t('landing.formats.pdf.i1'), t('landing.formats.pdf.i2'), t('landing.formats.pdf.i3'), t('landing.formats.pdf.i4')] }, ]; return ( @@ -295,7 +295,7 @@ const FormatsSection = () => {
{t('landing.formats.pill')}

{t('landing.formats.title')} {t('landing.formats.title2')}

-

{t('landing.formats.subtitle')}

+

{t('landing.formats.subtitle')}

{formats.map((f, i) => ( @@ -304,12 +304,12 @@ const FormatsSection = () => {
{f.icon}
- {f.id} + {f.id}

{f.name}

    {f.items.map((item, j) => ( -
  • +
  • {item}
  • ))} @@ -337,14 +337,14 @@ const PricingSection = () => {

    {t('landing.pricing.title')}

    -

    {t('landing.pricing.subtitle')}

    +

    {t('landing.pricing.subtitle')}

    - -
    ); @@ -395,7 +395,7 @@ const CTASection = () => {

    {t('landing.cta.title')}

    -

    +

    {t('landing.cta.subtitle')}

    @@ -416,14 +416,14 @@ const Footer = () => {
    -

    +

    {t('landing.footer.desc')}

    {t('landing.footer.product')}

    -
    -
    +
    {t('landing.footer.rights')}
    -
    - wordly.art +
    + +
    + wordly.art +
    diff --git a/frontend/src/components/layout/page-header.tsx b/frontend/src/components/layout/page-header.tsx new file mode 100644 index 0000000..3dfb44f --- /dev/null +++ b/frontend/src/components/layout/page-header.tsx @@ -0,0 +1,40 @@ +'use client'; + +import type { ReactNode } from 'react'; + +/** + * Single editorial page header: accent pill + serif title (base + italic accent) + * + subtitle. Every dashboard surface uses this so the product speaks one voice. + */ +export function PageHeader({ + pill, + titleBase, + titleAccent, + subtitle, + action, + className = '', +}: { + pill: string; + titleBase: string; + titleAccent: string; + subtitle?: string; + action?: ReactNode; + className?: string; +}) { + return ( +
    +
    + {pill} +

    + {titleBase} {titleAccent} +

    + {subtitle && ( +

    + {subtitle} +

    + )} +
    + {action &&
    {action}
    } +
    + ); +} diff --git a/frontend/src/components/ui/cookie-consent.tsx b/frontend/src/components/ui/cookie-consent.tsx index bda92e3..2930043 100644 --- a/frontend/src/components/ui/cookie-consent.tsx +++ b/frontend/src/components/ui/cookie-consent.tsx @@ -6,6 +6,13 @@ import { motion, AnimatePresence } from "framer-motion"; import { Button } from "@/components/ui/button"; const STORAGE_KEY = "cookie-consent"; +const REOPEN_EVENT = "wordly:reopen-cookie-consent"; + +/** Reopen the banner after a choice was made (footer link uses this). */ +export function reopenCookieConsent() { + localStorage.removeItem(STORAGE_KEY); + window.dispatchEvent(new Event(REOPEN_EVENT)); +} export function CookieConsent() { const { t } = useI18n(); @@ -13,6 +20,9 @@ export function CookieConsent() { useEffect(() => { setVisible(!localStorage.getItem(STORAGE_KEY)); + const onReopen = () => setVisible(true); + window.addEventListener(REOPEN_EVENT, onReopen); + return () => window.removeEventListener(REOPEN_EVENT, onReopen); }, []); function acceptAll() { @@ -38,7 +48,7 @@ export function CookieConsent() {

    @@ -63,3 +73,16 @@ export function CookieConsent() { ); } + +/** Drawn icon instead of an emoji — keeps one consistent icon system. */ +function CookieIcon() { + return ( + + ); +} diff --git a/frontend/src/components/ui/model-combobox.tsx b/frontend/src/components/ui/model-combobox.tsx index b166957..f73bed0 100644 --- a/frontend/src/components/ui/model-combobox.tsx +++ b/frontend/src/components/ui/model-combobox.tsx @@ -3,6 +3,7 @@ import React, { useState, useRef, useEffect, useCallback } from "react"; import { Search, List, Loader2, Check } from "lucide-react"; import { Input } from "@/components/ui/input"; +import { useI18n } from "@/lib/i18n"; export interface ModelOption { id: string; @@ -31,6 +32,7 @@ export function ModelCombobox({ providerLabel, placeholder = "nom-du-modele", }: ModelComboboxProps) { + const { t } = useI18n(); const [isOpen, setIsOpen] = useState(false); const [filter, setFilter] = useState(""); const containerRef = useRef(null); @@ -108,17 +110,17 @@ export function ModelCombobox({ type="button" onClick={handleBrowse} disabled={isLoading} - className="flex items-center gap-1.5 px-3 h-10 rounded-lg border border-border-subtle bg-surface text-sm text-muted-foreground hover:bg-surface-elevated hover:text-foreground transition-colors disabled:opacity-50" + className="flex items-center gap-1.5 px-3 h-10 rounded-lg border border-border bg-card text-sm text-muted-foreground hover:bg-card hover:text-foreground transition-colors disabled:opacity-50" > {isLoading ? : } - Parcourir + {t('common.browse')}

    {isOpen && ( -
    +
    {/* Filter */} -
    +
    {filtered.length === 0 ? (
  • - {models.length === 0 ? "Aucun modele recupere" : "Aucun modele ne correspond"} + {models.length === 0 ? t('common.noModels') : t('common.noModelMatch')}
  • ) : ( filtered.map((model, i) => ( @@ -141,7 +143,7 @@ export function ModelCombobox({ onClick={() => handleSelect(model)} onMouseEnter={() => setHighlightIndex(i)} className={`flex items-center justify-between px-3 py-2 text-xs cursor-pointer transition-colors ${ - i === highlightIndex ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-surface-elevated" + i === highlightIndex ? "bg-primary/10 text-foreground" : "text-muted-foreground hover:bg-card" } ${value === model.id ? "bg-primary/5" : ""}`} >
    @@ -160,7 +162,7 @@ export function ModelCombobox({
{models.length > 0 && ( -
+
{filtered.length} modele{filtered.length !== 1 ? "s" : ""} {filter ? ` sur ${models.length}` : ""} — nom libre possible
diff --git a/frontend/src/components/ui/notification.tsx b/frontend/src/components/ui/notification.tsx index dc2650c..c76da76 100644 --- a/frontend/src/components/ui/notification.tsx +++ b/frontend/src/components/ui/notification.tsx @@ -4,6 +4,7 @@ import * as React from "react" import { cva, type VariantProps } from "class-variance-authority" import { X, CheckCircle, AlertCircle, AlertTriangle, Info, Loader2 } from "lucide-react" import { cn } from "@/lib/utils" +import { useI18n } from "@/lib/i18n" const notificationVariants = cva( "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-lg border p-4 pe-8 shadow-lg transition-all duration-300 ease-out", @@ -74,6 +75,7 @@ const Notification = React.forwardRef( children, ...props }, ref) => { + const { t } = useI18n() const [visible, setVisible] = React.useState(true) const [progress, setProgress] = React.useState(100) @@ -181,7 +183,7 @@ const Notification = React.forwardRef( type="button" onClick={handleClose} className="absolute right-2 top-2 flex-shrink-0 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:opacity-100 focus-visible:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2" - aria-label="Close notification" + aria-label={t("common.closeNotification")} > diff --git a/frontend/src/lib/i18n/messages/en/common.json b/frontend/src/lib/i18n/messages/en/common.json index a102dd3..22e4a37 100644 --- a/frontend/src/lib/i18n/messages/en/common.json +++ b/frontend/src/lib/i18n/messages/en/common.json @@ -1,4 +1,8 @@ { "common.loading": "Loading...", - "common.backToHome": "Back to home" + "common.backToHome": "Back to home", + "common.browse": "Browse", + "common.noModels": "No models retrieved", + "common.noModelMatch": "No model matches your search", + "common.closeNotification": "Close notification" } diff --git a/frontend/src/lib/i18n/messages/en/cookieConsent.json b/frontend/src/lib/i18n/messages/en/cookieConsent.json index a0eb822..ed418cc 100644 --- a/frontend/src/lib/i18n/messages/en/cookieConsent.json +++ b/frontend/src/lib/i18n/messages/en/cookieConsent.json @@ -3,5 +3,6 @@ "cookieConsent.description": "We use essential cookies so the app works (session, security, language). With your permission, we also use optional cookies to measure traffic and improve the product.", "cookieConsent.acceptAll": "Accept all", "cookieConsent.essentialOnly": "Essential only", - "cookieConsent.learnMore": "Learn more" + "cookieConsent.learnMore": "Learn more", + "cookieConsent.reopen": "Cookie preferences" } diff --git a/frontend/src/lib/i18n/messages/en/glossaries.json b/frontend/src/lib/i18n/messages/en/glossaries.json index c43f647..25abd6a 100644 --- a/frontend/src/lib/i18n/messages/en/glossaries.json +++ b/frontend/src/lib/i18n/messages/en/glossaries.json @@ -220,5 +220,11 @@ "glossaries.card.use": "Use", "glossaries.createFirst": "Create my first glossary", "glossaries.noResults": "No results for “{query}”", - "glossaries.card.terms": "terms" + "glossaries.card.terms": "terms", + "glossaries.suggestion.formal": "Formal tone", + "glossaries.suggestion.proprietary": "Proper nouns", + "glossaries.suggestion.numbers": "Numbers", + "glossaries.suggestion.placeholders": "Placeholders", + "glossaries.suggestion.technical": "Technical terms", + "glossaries.suggestion.concise": "Concise style" } diff --git a/frontend/src/lib/i18n/messages/en/pricing.json b/frontend/src/lib/i18n/messages/en/pricing.json index 7eb7caf..3d1d1ca 100644 --- a/frontend/src/lib/i18n/messages/en/pricing.json +++ b/frontend/src/lib/i18n/messages/en/pricing.json @@ -152,5 +152,13 @@ "pricing.enterprise.subject": "Enterprise plan enquiry", "pricing.toast.close": "Close", "pricing.header.titleBase": "A plan for", - "pricing.header.titleAccent": "every need" + "pricing.header.titleAccent": "every need", + "pricing.confirm.title": "Confirm your subscription", + "pricing.confirm.subtitle": "You will be redirected to our secure payment provider.", + "pricing.confirm.month": "month", + "pricing.confirm.year": "year", + "pricing.confirm.monthlyEquivalent": "Billed yearly — {price} € / month equivalent.", + "pricing.confirm.secureNote": "You can cancel anytime from your profile. Payment is processed by Stripe; your card details never touch our servers.", + "pricing.confirm.cancel": "Cancel", + "pricing.confirm.cta": "Continue to payment" } diff --git a/frontend/src/lib/i18n/messages/en/providerSelector.json b/frontend/src/lib/i18n/messages/en/providerSelector.json index 7839c28..811d620 100644 --- a/frontend/src/lib/i18n/messages/en/providerSelector.json +++ b/frontend/src/lib/i18n/messages/en/providerSelector.json @@ -3,5 +3,6 @@ "providerSelector.noLlm": "No AI model configured.", "providerSelector.costOne": "Cost: 1 credit per page", "providerSelector.costFive": "Cost: 5 credits per page (Premium factor)", - "providerSelector.unlockContextual": "Unlock premium contextual translation for your entire documents." + "providerSelector.unlockContextual": "Unlock premium contextual translation for your entire documents.", + "providerSelector.standard": "Standard" } diff --git a/frontend/src/lib/i18n/messages/en/reviews.json b/frontend/src/lib/i18n/messages/en/reviews.json index 2bf074a..5c30ead 100644 --- a/frontend/src/lib/i18n/messages/en/reviews.json +++ b/frontend/src/lib/i18n/messages/en/reviews.json @@ -28,5 +28,11 @@ "reviews.error.title": "Error", "reviews.error.load": "Loading failed", "reviews.error.update": "Could not update", - "reviews.error.download": "Download failed (HTTP {status})" + "reviews.error.download": "Download failed (HTTP {status})", + "reviews.pill": "Quality control", + "reviews.titleBase": "Translation", + "reviews.titleAccent": "review", + "reviews.approveAllConfirm": "Confirm ({count})", + "reviews.action.approveShort": "OK", + "reviews.action.resetShort": "Re-review" } diff --git a/frontend/src/lib/i18n/messages/en/services.json b/frontend/src/lib/i18n/messages/en/services.json index b373fbb..1afd094 100644 --- a/frontend/src/lib/i18n/messages/en/services.json +++ b/frontend/src/lib/i18n/messages/en/services.json @@ -11,5 +11,6 @@ "services.adminOnly.desc": "API keys, model selection, and provider activation are managed exclusively by the administrator in the admin panel. You never need to enter an API key.", "services.fallback.google.label": "Google Translate", "services.fallback.google.desc": "Fast translation, 130+ languages", - "services.pill": "Engines" + "services.pill": "Engines", + "services.titleAccent": "status" } diff --git a/frontend/src/lib/i18n/messages/en/settings.json b/frontend/src/lib/i18n/messages/en/settings.json index fa66875..f23728a 100644 --- a/frontend/src/lib/i18n/messages/en/settings.json +++ b/frontend/src/lib/i18n/messages/en/settings.json @@ -19,5 +19,6 @@ "settings.formats.powerpoint.name": "PowerPoint", "settings.pill": "Configuration", "settings.cache.confirm": "Confirm and reload", - "settings.cache.cancelClear": "Cancel" + "settings.cache.cancelClear": "Cancel", + "settings.titleAccent": "presets" } diff --git a/frontend/src/lib/i18n/messages/en/teams.json b/frontend/src/lib/i18n/messages/en/teams.json index 63156b1..9561bfd 100644 --- a/frontend/src/lib/i18n/messages/en/teams.json +++ b/frontend/src/lib/i18n/messages/en/teams.json @@ -22,5 +22,8 @@ "teams.invite": "Invite", "teams.empty": "No workspace yet — create the first one above.", "teams.error.title": "Error", - "teams.error.load": "Could not load" + "teams.error.load": "Could not load", + "teams.pill": "Collaboration", + "teams.titleBase": "Team", + "teams.titleAccent": "workspaces" } diff --git a/frontend/src/lib/i18n/messages/en/translate.json b/frontend/src/lib/i18n/messages/en/translate.json index 1716c10..e03540b 100644 --- a/frontend/src/lib/i18n/messages/en/translate.json +++ b/frontend/src/lib/i18n/messages/en/translate.json @@ -49,12 +49,8 @@ "translate.glossary.targetWarningBut": "but your document targets", "translate.glossary.targetWarningEnd": "Terms may not be relevant.", "translate.header.processing": "Processing in progress", - "translate.header.aiActive": "AI analysis active", "translate.header.aiActiveDesc": "Your layout is being preserved by our contextual engine.", "translate.header.completed": "Completed", - "translate.header.completedTitle": "Translation completed", - "translate.header.proSpace": "Pro space", - "translate.header.translateDoc": "Translate a document", "translate.header.translateDocDesc": "Preserve the original layout with our ultra-high-fidelity translation engine.", "translate.upload.nativeFormat": "Native format", "translate.fileType.word": "Word (.docx)", @@ -68,12 +64,7 @@ "translate.contextEngineActive": "Contextual engine active", "translate.phase1": "Phase 1: Initialisation", "translate.phase2": "Phase 2: Contextual reconstruction", - "translate.stat.segments": "segments", - "translate.stat.precision": "precision", - "translate.stat.speedLabel": "speed", - "translate.stat.turbo": "Turbo", "translate.stat.time": "time", - "translate.complete.masterQuality": "✓ Master quality", "translate.download": "Download", "translate.newTranslation": "+ New translation", "translate.failedTitle": "Translation error", @@ -81,10 +72,6 @@ "translate.uploadAnother": "Upload another file", "translate.monitor": "AI monitor", "translate.summary": "Summary", - "translate.cancelProcess": "⟳ Cancel the process", - "translate.layoutIntegrity": "Layout integrity", - "translate.secureHundred": "100% SECURE", - "translate.okHundred": "100% OK", "translate.preserveLayout": "Preserve layout", "translate.preserveLayoutDesc": "Preserve the layout", "translate.textOnly": "Text only", diff --git a/frontend/src/lib/i18n/messages/fr/common.json b/frontend/src/lib/i18n/messages/fr/common.json index ab4bd48..aeb0b99 100644 --- a/frontend/src/lib/i18n/messages/fr/common.json +++ b/frontend/src/lib/i18n/messages/fr/common.json @@ -1,4 +1,8 @@ { "common.loading": "Chargement...", - "common.backToHome": "Retour à l'accueil" + "common.backToHome": "Retour à l'accueil", + "common.browse": "Parcourir", + "common.noModels": "Aucun modèle récupéré", + "common.noModelMatch": "Aucun modèle ne correspond", + "common.closeNotification": "Fermer la notification" } diff --git a/frontend/src/lib/i18n/messages/fr/cookieConsent.json b/frontend/src/lib/i18n/messages/fr/cookieConsent.json index 70247bf..f8db8af 100644 --- a/frontend/src/lib/i18n/messages/fr/cookieConsent.json +++ b/frontend/src/lib/i18n/messages/fr/cookieConsent.json @@ -3,5 +3,6 @@ "cookieConsent.description": "Nous utilisons des cookies strictement nécessaires au fonctionnement du service (session, sécurité, langue). Avec votre accord, nous pouvons aussi utiliser des cookies optionnels pour mesurer l'audience et améliorer le produit.", "cookieConsent.acceptAll": "Tout accepter", "cookieConsent.essentialOnly": "Nécessaires uniquement", - "cookieConsent.learnMore": "En savoir plus" + "cookieConsent.learnMore": "En savoir plus", + "cookieConsent.reopen": "Préférences cookies" } diff --git a/frontend/src/lib/i18n/messages/fr/glossaries.json b/frontend/src/lib/i18n/messages/fr/glossaries.json index 1a67e62..6968dec 100644 --- a/frontend/src/lib/i18n/messages/fr/glossaries.json +++ b/frontend/src/lib/i18n/messages/fr/glossaries.json @@ -220,5 +220,11 @@ "glossaries.card.use": "Utiliser", "glossaries.createFirst": "Créer mon premier glossaire", "glossaries.noResults": "Aucun résultat pour « {query} »", - "glossaries.card.terms": "termes" + "glossaries.card.terms": "termes", + "glossaries.suggestion.formal": "Ton formel", + "glossaries.suggestion.proprietary": "Noms propres", + "glossaries.suggestion.numbers": "Chiffres", + "glossaries.suggestion.placeholders": "Placeholders", + "glossaries.suggestion.technical": "Termes techniques", + "glossaries.suggestion.concise": "Style concis" } diff --git a/frontend/src/lib/i18n/messages/fr/pricing.json b/frontend/src/lib/i18n/messages/fr/pricing.json index 01986f6..c4b48fa 100644 --- a/frontend/src/lib/i18n/messages/fr/pricing.json +++ b/frontend/src/lib/i18n/messages/fr/pricing.json @@ -152,5 +152,13 @@ "pricing.enterprise.subject": "Offre Enterprise", "pricing.toast.close": "Fermer", "pricing.header.titleBase": "Un forfait pour", - "pricing.header.titleAccent": "chaque besoin" + "pricing.header.titleAccent": "chaque besoin", + "pricing.confirm.title": "Confirmez votre abonnement", + "pricing.confirm.subtitle": "Vous serez redirigé vers notre prestataire de paiement sécurisé.", + "pricing.confirm.month": "mois", + "pricing.confirm.year": "an", + "pricing.confirm.monthlyEquivalent": "Facturation annuelle — soit {price} € / mois.", + "pricing.confirm.secureNote": "Résiliable à tout moment depuis votre profil. Paiement traité par Stripe ; vos données bancaires ne passent jamais par nos serveurs.", + "pricing.confirm.cancel": "Annuler", + "pricing.confirm.cta": "Continuer vers le paiement" } diff --git a/frontend/src/lib/i18n/messages/fr/providerSelector.json b/frontend/src/lib/i18n/messages/fr/providerSelector.json index fd4865e..57ef69d 100644 --- a/frontend/src/lib/i18n/messages/fr/providerSelector.json +++ b/frontend/src/lib/i18n/messages/fr/providerSelector.json @@ -3,5 +3,6 @@ "providerSelector.noLlm": "Aucun modèle IA configuré.", "providerSelector.costOne": "Coût : 1 crédit par page", "providerSelector.costFive": "Coût : 5 crédits par page (Facteur Premium)", - "providerSelector.unlockContextual": "Débloquez la traduction contextuelle haut de gamme pour vos documents entiers." + "providerSelector.unlockContextual": "Débloquez la traduction contextuelle haut de gamme pour vos documents entiers.", + "providerSelector.standard": "Standard" } diff --git a/frontend/src/lib/i18n/messages/fr/reviews.json b/frontend/src/lib/i18n/messages/fr/reviews.json index 898c17f..668360b 100644 --- a/frontend/src/lib/i18n/messages/fr/reviews.json +++ b/frontend/src/lib/i18n/messages/fr/reviews.json @@ -28,5 +28,11 @@ "reviews.error.title": "Erreur", "reviews.error.load": "Erreur de chargement", "reviews.error.update": "Mise à jour impossible", - "reviews.error.download": "Téléchargement échoué (HTTP {status})" + "reviews.error.download": "Téléchargement échoué (HTTP {status})", + "reviews.pill": "Contrôle qualité", + "reviews.titleBase": "Relecture de la", + "reviews.titleAccent": "traduction", + "reviews.approveAllConfirm": "Confirmer ({count})", + "reviews.action.approveShort": "OK", + "reviews.action.resetShort": "À relire" } diff --git a/frontend/src/lib/i18n/messages/fr/services.json b/frontend/src/lib/i18n/messages/fr/services.json index 836de8b..7900652 100644 --- a/frontend/src/lib/i18n/messages/fr/services.json +++ b/frontend/src/lib/i18n/messages/fr/services.json @@ -11,5 +11,6 @@ "services.adminOnly.desc": "Les clés API, la sélection de modèle et l'activation des fournisseurs sont gérées exclusivement par l'administrateur. Vous n'avez jamais besoin de saisir de clé API.", "services.fallback.google.label": "Google Traduction", "services.fallback.google.desc": "Traduction rapide, 130+ langues", - "services.pill": "Moteurs" + "services.pill": "Moteurs", + "services.titleAccent": "disponibles" } diff --git a/frontend/src/lib/i18n/messages/fr/settings.json b/frontend/src/lib/i18n/messages/fr/settings.json index 7465614..98492db 100644 --- a/frontend/src/lib/i18n/messages/fr/settings.json +++ b/frontend/src/lib/i18n/messages/fr/settings.json @@ -19,5 +19,6 @@ "settings.formats.powerpoint.name": "PowerPoint", "settings.pill": "Configuration", "settings.cache.confirm": "Confirmer et recharger", - "settings.cache.cancelClear": "Annuler" + "settings.cache.cancelClear": "Annuler", + "settings.titleAccent": "préréglages" } diff --git a/frontend/src/lib/i18n/messages/fr/teams.json b/frontend/src/lib/i18n/messages/fr/teams.json index 94cbac4..79f0ecb 100644 --- a/frontend/src/lib/i18n/messages/fr/teams.json +++ b/frontend/src/lib/i18n/messages/fr/teams.json @@ -22,5 +22,8 @@ "teams.invite": "Inviter", "teams.empty": "Aucun espace pour l'instant — créez le premier ci-dessus.", "teams.error.title": "Erreur", - "teams.error.load": "Chargement impossible" + "teams.error.load": "Chargement impossible", + "teams.pill": "Collaboration", + "teams.titleBase": "Espaces de", + "teams.titleAccent": "travail" } diff --git a/frontend/src/lib/i18n/messages/fr/translate.json b/frontend/src/lib/i18n/messages/fr/translate.json index 3959311..8a4bf8b 100644 --- a/frontend/src/lib/i18n/messages/fr/translate.json +++ b/frontend/src/lib/i18n/messages/fr/translate.json @@ -49,12 +49,8 @@ "translate.glossary.targetWarningBut": "mais votre document cible", "translate.glossary.targetWarningEnd": "Les termes risquent de ne pas être pertinents.", "translate.header.processing": "Traitement en cours", - "translate.header.aiActive": "Analyse IA active", "translate.header.aiActiveDesc": "Votre mise en page est en cours de préservation par notre moteur contextuel.", "translate.header.completed": "Complété", - "translate.header.completedTitle": "Traduction terminée", - "translate.header.proSpace": "Espace Pro", - "translate.header.translateDoc": "Traduire un document", "translate.header.translateDocDesc": "Conservez la mise en page d'origine grâce au moteur de traduction ultra-haute fidélité.", "translate.upload.nativeFormat": "Format natif", "translate.fileType.word": "Word (.docx)", @@ -68,12 +64,7 @@ "translate.contextEngineActive": "Moteur contextuel actif", "translate.phase1": "Phase 1 : Initialisation", "translate.phase2": "Phase 2 : Reconstruction contextuelle", - "translate.stat.segments": "segments", - "translate.stat.precision": "précision", - "translate.stat.speedLabel": "vitesse", - "translate.stat.turbo": "Turbo", "translate.stat.time": "temps", - "translate.complete.masterQuality": "✓ Qualité Maître", "translate.download": "Télécharger", "translate.newTranslation": "+ Nouvelle traduction", "translate.failedTitle": "Erreur lors de la traduction", @@ -81,10 +72,6 @@ "translate.uploadAnother": "Téléverser un autre fichier", "translate.monitor": "Moniteur IA", "translate.summary": "Récapitulatif", - "translate.cancelProcess": "⟳ Annuler le processus", - "translate.layoutIntegrity": "Intégrité Layout", - "translate.secureHundred": "100% SÉCURISÉ", - "translate.okHundred": "100% OK", "translate.preserveLayout": "Conserver la mise en page", "translate.preserveLayoutDesc": "Conserver la mise en page", "translate.textOnly": "Texte brut",