diff --git a/.impeccable/critique/2026-08-30T21-23-40Z__frontend-src.md b/.impeccable/critique/2026-08-30T21-23-40Z__frontend-src.md new file mode 100644 index 0000000..1201125 --- /dev/null +++ b/.impeccable/critique/2026-08-30T21-23-40Z__frontend-src.md @@ -0,0 +1,65 @@ +--- +target: frontend/src (UI complète Wordly.art) +total_score: 28 +max_score: 40 +na_heuristics: +p0_count: 0 +p1_count: 2 +timestamp: 2026-08-30T21-23-40Z +slug: frontend-src +--- +# Évaluation de design — Wordly.art (`frontend/src`) — 3ᵉ passage + +Méthode : double agent isolé (A : agent_a56a818e · B : agent_fa9c4f90) + +## Score de santé (10 règles d'ergonomie, 0-4 chacune) + +| # | Règle | Note | Problème principal | +|---|-------|------|--------------------| +| 1 | L'utilisateur sait toujours où il en est | 3 | Aucun retour visuel quand on survole la zone de dépôt avec un fichier (`isDragOver` calculé dans `useFileUpload.ts:69-77` mais jamais affiché, `translate/page.tsx:359-415`) ; « Tout approuver » enchaîne les demandes sans progression (`reviews/[jobId]/page.tsx:159-167`). | +| 2 | Le produit parle la langue de ses utilisateurs | 3 | Restes de jargon : « Le job a été arrêté » (`fr/translate.json:102`), « Segments », « Moteur contextuel » — globalement bien vulgarisé (XLIFF expliqué). | +| 3 | L'utilisateur garde le contrôle | 3 | Retrait d'un membre d'équipe sans confirmation (`teams/page.tsx:195-205`) ; ailleurs excellent (annulation réelle, reprise après rafraîchissement). | +| 4 | Cohérence de l'ensemble | 2 | Page Glossaires en couleurs brutes hors système (`glossaries/page.tsx:93,115,130`), n'utilise pas l'en-tête commun `page-header.tsx` ; émojis ⚠️🎯🌐 dans les avertissements (`GlossarySelector.tsx:342,352,379`) ; achat de crédits sans confirmation alors que les abonnements en ont une. | +| 5 | Prévention des erreurs | 3 | Bouton bloqué sans explication quand langue source = langue cible (`useTranslationConfig.ts:237`, aides incomplètes `translate/page.tsx:617-622`). | +| 6 | Reconnaissance plutôt que mémoire | 3 | Récapitulatif pendant et après le traitement, historique récent avec liens de relecture. | +| 7 | Vitesse pour les utilisateurs intensifs | 3 | Ctrl+Entrée existe mais n'est jamais annoncé à l'écran ; pas de filtre « à relire uniquement » dans la table de relecture. | +| 8 | Sobriété visuelle | 2 | Micro-textes 8,5-10 px par centaines (`ProviderSelector.tsx:201`, `landing-page.tsx:278,350`) ; 5 plans × 5-10 fonctions par carte. | +| 9 | Récupération après erreur | 3 | Erreurs traduites en actions (`translate/page.tsx:91-112`), fichier conservé + Réessayer. | +| 10 | Aide disponible | 3 | Aides contextuelles réelles ; pas de centre d'aide. | +| **Total** | | **28/40** | **Palier « bonne interface » (28-35)** | + +## Verdict de spécificité + +**Revue** : l'accueil devient « parfois précieux » — le dessin animé du hero (`hero-visual.tsx`) est jugé « la meilleure pièce du site » : la promesse produit est dessinée, pas photographiée, avec le balayage doré « détail d'auteur ». L'outil Traduire est « la surface la plus authentique » (pipeline dédié, avertissements de glossaire, garde source = cible) ; la relecture parle « le vocabulaire de la localisation, pas du SaaS générique ». Restent deux plaques de modèle tout fait : le mur de 6 cartes de fonctionnalités de l'accueil, et les « signaux de confiance » de la page tarifs. + +**Scan automatique** : 3 constats inchangés — les 2 Inter (choix assumé « outil sobre ») et le faux positif du rond de chargement. + +## Ce qui marche + +1. **L'état d'attente comme pièce maîtresse** : progression, phases nommées, temps restant estimé honnêtement, annulation serveur réelle, survie au rafraîchissement. « Peu de produits d'IA assument l'attente avec cette précision. » +2. **Le langage du métier assumé** : XLIFF expliqué, avertissements glossaire précis, garde anti source = cible. +3. **Les garde-fous des gestes irréversibles** : approbation en deux clics, confirmation avant paiement, confirmation de purge. + +## Problèmes prioritaires + +1. **[P1] La zone de dépôt est muette au survol.** Le premier geste du produit n'a aucun retour. → Afficher un contour doré et un léger agrandissement pendant le survol (`translate/page.tsx:361`). +2. **[P1] L'or des petits textes est illisible.** `#C5A17A` sur crème ≈ 2,3:1, et des textes à 8,5-10 px partout. → Assombrir l'or des textes à `#8B6F47`, plancher 11 px (`globals.css:196`, `ProviderSelector.tsx:201`). +3. **[P2] « Tout approuver » sans progression.** Sur 500 segments : 500 demandes enchaînées sans compteur ni arrêt. → Compteur dans le bouton + arrêt possible (`reviews/[jobId]/page.tsx:151-168`). +4. **[P2] Bouton bloqué sans raison quand source = cible.** → Ajouter l'explication manquante (`translate/page.tsx:617-622`). +5. **[P2] Page Glossaires hors système + sélecteur de glossaire trop chargé.** Couleurs brutes, pas d'en-tête commun ; dans le flux de traduction, le sélecteur empile 6 décisions (choix, filtre, avertissements, aperçu, ajout de terme). → Migrer la page aux couleurs du système ; réduire le sélecteur au choix + avertissements, l'ajout de terme derrière un lien dépliable (`glossaries/page.tsx:107`, `GlossarySelector.tsx:578-614`). + +## Drapeaux rouges par persona + +**Alex (expert pressé)** : le raccourci Ctrl+Entrée n'est annoncé nulle part ; pas de filtre « à relire uniquement » sur 800 segments ; les icônes du pipeline et les libellés d'étapes suivent deux seuils contradictoires (`translate/page.tsx:660` vs `:42`). +**Sam (accessibilité)** : or clair illisible + micro-textes ; sévérité des avertissements glossaire portée par des émojis et la seule couleur ; le bandeau d'annonce de la page tarifs n'est pas lu aux lecteurs d'écran (contrairement aux notifications). +**Jordan (premier usage)** : la mention légale à l'inscription n'est pas cliquable ; l'annulation réussie d'une traduction s'affiche en rouge comme une erreur ; membres d'équipe en codes illisibles. + +## Observations mineures + +Restes de replis français codés en dur (`translate/page.tsx:861,868,881`) ; le lien tarifs de l'accueil construit l'adresse avec le nom traduit du plan (cassera le jour où le nom est traduit) ; l'accueil montre 3 plans, la page tarifs 5 sans le gratuit en haut ; « document réservé libéré » formulation opaque ; double barre de défilement dans la liste des langues ; page « Réglages » sans aucun réglage ; bouton d'échange de langues mort mais visible ; titres par défaut en Inter alors que la charte repose sur le serif posé à la main ; clé dupliquée preserveLayout. + +## Questions à se poser + +1. La grille tarifaire doit-elle vraiment montrer les 5 plans en même temps ? Free et Enterprise en lignes secondaires rendraient la décision binaire. +2. Pourquoi l'ajout de termes existe-t-il à la fois dans le sélecteur de traduction ET dans la page glossaires ? Laquelle est la vraie maison du geste ? +3. Que gagnerait l'écran d'attente à montrer le document lui-même, comme le dessin animé de l'accueil — une continuité entre la promesse et l'exécution ? diff --git a/frontend/src/app/dashboard/DashboardSidebar.tsx b/frontend/src/app/dashboard/DashboardSidebar.tsx index fcefd68..fbea933 100644 --- a/frontend/src/app/dashboard/DashboardSidebar.tsx +++ b/frontend/src/app/dashboard/DashboardSidebar.tsx @@ -39,7 +39,7 @@ export function DashboardSidebar() {
{t('dashboard.sidebar.upgradeToPro', { defaultValue: 'Passer Pro →' })} @@ -64,7 +64,7 @@ export function DashboardSidebar() {
- + {translateTier(t, user.tier)}
diff --git a/frontend/src/app/dashboard/translate/GlossarySelector.tsx b/frontend/src/app/dashboard/translate/GlossarySelector.tsx index 1b0b164..e98d363 100644 --- a/frontend/src/app/dashboard/translate/GlossarySelector.tsx +++ b/frontend/src/app/dashboard/translate/GlossarySelector.tsx @@ -272,7 +272,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary >
- + {t('translate.glossary.title') || 'Glossaire & Terminologie'} @@ -403,7 +403,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary onClick={() => setFilterByLang(!filterByLang)} className={cn( "text-[10px] font-bold px-2 py-0.5 rounded transition-colors cursor-pointer", - filterByLang ? "bg-brand-accent/10 text-brand-accent" : "bg-brand-dark/10 dark:bg-white/10 text-brand-dark/50 dark:text-white/50" + filterByLang ? "bg-brand-accent/10 text-brand-goldink dark:text-brand-accent" : "bg-brand-dark/10 dark:bg-white/10 text-brand-dark/50 dark:text-white/50" )} > {filterByLang ? t('translate.glossary.active') || 'Actif' : t('translate.glossary.inactive') || 'Inactif'} @@ -431,7 +431,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary className={cn( "w-full text-left px-2.5 py-2 rounded-lg transition-colors flex items-center justify-between cursor-pointer", isSelected - ? 'bg-brand-accent/5 text-brand-accent' + ? 'bg-brand-accent/5 text-brand-goldink dark:text-brand-accent' : 'hover:bg-brand-muted dark:hover:bg-white/5 text-brand-dark/70 dark:text-white/70' )} > @@ -441,7 +441,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary {flag} ➜ {g.target_language === 'multi' ? `🌐 ${t('translate.glossary.multilingual') || 'MULTILINGUE'}` : getFlag(g.target_language || targetLang)} • {g.terms_count} {t('translate.glossary.terms') || 'termes'}
- {isSelected && } + {isSelected && } ); })} @@ -480,7 +480,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary className={cn( "w-full text-left px-2.5 py-2 rounded-lg transition-colors flex items-center justify-between cursor-pointer", isAlreadySelected - ? 'bg-brand-accent/5 text-brand-accent' + ? 'bg-brand-accent/5 text-brand-goldink dark:text-brand-accent' : 'hover:bg-brand-muted dark:hover:bg-white/5 text-brand-dark/70 dark:text-white/70' )} > @@ -493,9 +493,9 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary
{isImporting ? ( - + ) : isAlreadySelected ? ( - + ) : null} ); @@ -547,7 +547,7 @@ export function GlossarySelector({ sourceLang, targetLang, isPro, mode, glossary {isLoadingDetail ? (
- {t('translate.glossary.loading') || 'Chargement...'} + {t('translate.glossary.loading') || 'Chargement...'}
) : selectedGlossaryDetail?.terms && selectedGlossaryDetail.terms.length > 0 ? (
diff --git a/frontend/src/app/dashboard/translate/LanguageSelector.tsx b/frontend/src/app/dashboard/translate/LanguageSelector.tsx index b828729..62f74bc 100644 --- a/frontend/src/app/dashboard/translate/LanguageSelector.tsx +++ b/frontend/src/app/dashboard/translate/LanguageSelector.tsx @@ -112,7 +112,7 @@ function Combobox({ )} > {label || placeholder} - + {open && (
{lang.name} - {value === lang.code && } + {value === lang.code && } ))}
@@ -213,7 +213,7 @@ export default function LanguageSelector({ className={cn( 'flex size-7 items-center justify-center rounded-xl transition-all cursor-pointer', canSwap - ? 'text-brand-dark/50 dark:text-white/50 hover:text-brand-accent hover:bg-brand-muted/50 dark:hover:bg-white/5' + ? 'text-brand-dark/50 dark:text-white/50 hover:text-brand-goldink dark:text-brand-accent hover:bg-brand-muted/50 dark:hover:bg-white/5' : 'cursor-not-allowed opacity-30' )} title={t('langSelector.swap')} diff --git a/frontend/src/app/dashboard/translate/ProviderSelector.tsx b/frontend/src/app/dashboard/translate/ProviderSelector.tsx index 9c98cc1..b2254cd 100644 --- a/frontend/src/app/dashboard/translate/ProviderSelector.tsx +++ b/frontend/src/app/dashboard/translate/ProviderSelector.tsx @@ -70,7 +70,7 @@ const LLM_THEMES_FACTORY = (t: (k: string) => string): Record const makeDefaultLlmTheme = (t: (k: string) => string): CardTheme => ({ badge: t('providerTheme.default.badge'), subBadge: t('providerTheme.default.subBadge'), - accentClass: 'border-brand-accent/30 text-brand-accent bg-brand-accent/5', + accentClass: 'border-brand-accent/30 text-brand-goldink dark:text-brand-accent bg-brand-accent/5', glowClass: 'from-brand-accent/10 to-transparent', descriptionOverride: t('providerTheme.default.desc') }); @@ -118,7 +118,7 @@ export function ProviderSelector({ if (isLoadingProviders) { return (
- + {t('dashboard.translate.provider.loading')}
); @@ -152,7 +152,7 @@ export function ProviderSelector({
{t('providerSelector.standard')} @@ -188,7 +188,7 @@ export function ProviderSelector({
{theme.badge} @@ -198,7 +198,7 @@ export function ProviderSelector({ {displayLabel} - + {theme.subBadge} @@ -238,7 +238,7 @@ export function ProviderSelector({ : 'text-brand-dark/50 dark:text-white/40 hover:text-brand-dark dark:hover:text-white' )} > - + {t('dashboard.translate.provider.tabLLM')} {!isPro && } @@ -279,7 +279,7 @@ export function ProviderSelector({

{meta?.labelOverride || activeP.label} : {meta?.descriptionOverride || activeP.description} - {t('providerSelector.costOne')} + {t('providerSelector.costOne')}

) : null; })() @@ -291,7 +291,7 @@ export function ProviderSelector({

{activeP.label} : {theme.descriptionOverride} ({activeP.model || 'default'}) - + {activeP.id === 'openrouter_premium' ? t('providerSelector.costFive') : t('providerSelector.costOne')}

@@ -305,7 +305,7 @@ export function ProviderSelector({ {/* Pro upgrade banner when llm is active and user is not pro */} {!isPro && activeTab === 'llm' && (
- + {t('dashboard.translate.provider.llmDivider')} diff --git a/frontend/src/app/dashboard/translate/page.tsx b/frontend/src/app/dashboard/translate/page.tsx index 36f8c01..d3b1c9c 100644 --- a/frontend/src/app/dashboard/translate/page.tsx +++ b/frontend/src/app/dashboard/translate/page.tsx @@ -358,7 +358,10 @@ export default function TranslatePage() { {/* ── UPLOAD STATE: Editorial Dropzone ──────────────── */} {showUpload && (
-
+
@@ -444,7 +447,7 @@ export default function TranslatePage() { @@ -465,7 +468,7 @@ export default function TranslatePage() { className="flex items-center gap-3 rounded-2xl border border-black/5 bg-brand-muted/30 px-4 py-3 dark:border-white/5 dark:bg-white/5" > {item.status === 'processing' ? ( - + ) : item.status === 'completed' ? ( ) : item.status === 'failed' ? ( @@ -501,7 +504,7 @@ export default function TranslatePage() { {t('translate.recent.review')} @@ -531,7 +534,7 @@ export default function TranslatePage() { {recentJobs.slice(0, 6).map((job) => (
  • - + {job.fileName || job.jobId} @@ -544,7 +547,7 @@ export default function TranslatePage() { {t('translate.recent.review')} @@ -608,9 +611,9 @@ export default function TranslatePage() { {submit.isSubmitting ? ( <> {t('translate.submit')} ) : upload.files.length > 1 ? ( - <>{t('translate.startBatch', { count: upload.files.length })} + <>{t('translate.startBatch', { count: upload.files.length })} ) : ( - <>{t('translate.startTranslation')} 0 ? 'text-brand-accent' : 'opacity-20'} /> + <>{t('translate.startTranslation')} 0 ? 'text-brand-goldink dark:text-brand-accent' : 'opacity-20'} /> )} @@ -623,10 +626,10 @@ export default function TranslatePage() {
    - {t('landing.translate.zeroRetention') || 'Rétention Zéro'} + {t('landing.translate.zeroRetention') || 'Rétention Zéro'} - {t('landing.translate.filesDeleted') || 'Fichiers supprimés post-traitement'} + {t('landing.translate.filesDeleted') || 'Fichiers supprimés post-traitement'}
  • @@ -636,7 +639,7 @@ export default function TranslatePage() { {showProcessing && (
    -
    +
    @@ -718,7 +721,7 @@ export default function TranslatePage() { {t('translate.reviewCta')} @@ -810,7 +813,7 @@ export default function TranslatePage() { {config.mode === 'llm' ? ( @@ -831,7 +834,7 @@ export default function TranslatePage() { {config.mode === 'llm' && !!systemPrompt?.trim() && ( {t('translate.contextActive')} @@ -856,7 +859,7 @@ export default function TranslatePage() {
    - + {t('dashboard.translate.translateImages') || "Traduire les images"} @@ -905,7 +908,7 @@ export default function TranslatePage() { )} >
    - + {t('dashboard.translate.pdfMode.preserveLayout') || 'Mise en page'}

    @@ -924,7 +927,7 @@ export default function TranslatePage() { )} >

    - + {t('translate.textOnly')}

    @@ -950,7 +953,7 @@ export default function TranslatePage() { {/* File summary */} {(submit.fileName || upload.file?.name) && (

    -
    +
    {(() => { const name = submit.fileName || upload.file?.name || ''; const ext = name.split('.').pop()?.toLowerCase() ?? ''; @@ -977,7 +980,7 @@ export default function TranslatePage() {
    {t('translate.monitor.target')} - {tgtLangName} + {tgtLangName}
    {currentProvider && (
    @@ -1019,7 +1022,7 @@ export default function TranslatePage() {
    {t('translate.monitor.target')} - {tgtLangName} + {tgtLangName}
    {currentProvider && (
    @@ -1032,7 +1035,7 @@ export default function TranslatePage() {
    - {t('landing.translate.zeroRetention') || 'Rétention Zéro'} + {t('landing.translate.zeroRetention') || 'Rétention Zéro'}
    @@ -1057,7 +1060,7 @@ export default function TranslatePage() { {submit.isSubmitting ? ( <> {t('translate.submit')} ) : ( - <>{t('translate.startTranslation')} + <>{t('translate.startTranslation')} )}
    @@ -1095,7 +1098,7 @@ function FileStrip({ file, onRemove, onReplace, t }: { file: File; onRemove: () function StatBox({ icon, value, label }: { icon: React.ReactNode; value: string; label: string }) { return (
    -
    {icon}
    +
    {icon}

    {value}

    {label}

    diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index dcdcc31..6aa8d1d 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -14,6 +14,10 @@ --color-brand-bg: #FDFCF9; --color-brand-dark: #1A1A1A; --color-brand-accent: #C5A17A; + /* Or « encre » : même famille, assombri pour que les petits textes dorés + restent lisibles sur fond clair (≈ 5,4:1). L'or clair reste pour les + traits, fonds et le mode sombre. */ + --color-brand-goldink: #8B6F47; --color-brand-muted: #F3F1ED; /* shadcn compat */ @@ -194,7 +198,10 @@ } .accent-pill { - @apply px-4 py-1.5 rounded-full border border-brand-accent/30 text-brand-accent text-[11px] font-bold uppercase tracking-widest bg-brand-accent/5; + @apply px-4 py-1.5 rounded-full border border-brand-accent/40 text-brand-goldink text-[11px] font-bold uppercase tracking-widest bg-brand-accent/10; +} +.dark .accent-pill { + @apply text-brand-accent bg-brand-accent/5 border-brand-accent/20; } .dark .accent-pill { @apply border-brand-accent/20; diff --git a/frontend/src/app/pricing/page.tsx b/frontend/src/app/pricing/page.tsx index 5d77beb..a6d4c3c 100644 --- a/frontend/src/app/pricing/page.tsx +++ b/frontend/src/app/pricing/page.tsx @@ -574,8 +574,21 @@ export default function PricingPage() { ) : ( <> -
    - {plans.map((plan) => { + {/* Essai gratuit : une ligne discrète au-dessus du choix payant */} +
    +

    + {t('pricing.freeBand.text')} +

    + + {t('pricing.freeBand.cta')} + +
    + +
    + {plans.filter((p) => !['free', 'enterprise'].includes(p.id)).map((plan) => { const Icon = PLAN_ICONS[plan.id] ?? Sparkles; const price = displayPrice(plan); const isCurrent = currentPlan === plan.id; @@ -595,7 +608,7 @@ export default function PricingPage() { {(plan.popular || isCurrent) && (
    {plan.badge && ( - + {t(plan.badge)} )} @@ -614,7 +627,7 @@ export default function PricingPage() { {/* Icon + plan name */}
    -
    +
    @@ -770,6 +783,19 @@ export default function PricingPage() { })}
    + {/* Sur mesure : une ligne discrète sous le choix payant */} +
    +

    + {t('pricing.enterpriseBand.text')} +

    + + {t('pricing.enterpriseBand.cta')} + +
    + {/* ── Feature comparison table ── */}

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

    diff --git a/frontend/src/components/landing/hero-visual.tsx b/frontend/src/components/landing/hero-visual.tsx index c436610..cfdc555 100644 --- a/frontend/src/components/landing/hero-visual.tsx +++ b/frontend/src/components/landing/hero-visual.tsx @@ -88,7 +88,7 @@ function DocumentCard({ 'absolute right-[4%] top-[4%] rounded-full px-2 py-0.5 text-[9px] font-black tracking-widest', side === 'source' ? 'border border-black/10 text-brand-dark/50 dark:border-white/15 dark:text-white/50' - : 'border border-brand-accent/40 bg-brand-accent/10 text-brand-accent' + : 'border border-brand-accent/40 bg-brand-accent/10 text-brand-goldink dark:text-brand-accent' )} > {tag} @@ -172,7 +172,7 @@ export function HeroVisual() { animate={{ opacity: 1, y: 0 }} exit={{ opacity: 0, y: -4 }} transition={{ duration: 0.25 }} - className="absolute bottom-[14%] rounded-full border border-brand-accent/30 px-2 py-0.5 text-[9px] font-black tracking-widest text-brand-accent" + className="absolute bottom-[14%] rounded-full border border-brand-accent/30 px-2 py-0.5 text-[9px] font-black tracking-widest text-brand-goldink dark:text-brand-accent" > {target.code} diff --git a/frontend/src/components/landing/landing-page.tsx b/frontend/src/components/landing/landing-page.tsx index 881f9ca..4c3479c 100644 --- a/frontend/src/components/landing/landing-page.tsx +++ b/frontend/src/components/landing/landing-page.tsx @@ -7,7 +7,6 @@ import { Layers, MousePointer2, Download, - Globe, Cpu, CheckCircle2, ArrowRight, @@ -17,7 +16,6 @@ import { Presentation, MessageSquare, ShieldCheck, - Zap, Menu, X, Languages, @@ -36,7 +34,7 @@ const Logo = () => ( W
    - Wordly.art + Wordly.art
    ); @@ -215,34 +213,91 @@ const FeaturesContext = () => { const LayoutFeatures = () => { const { t } = useI18n(); - const points = [ - { title: t('landing.layout.p1.title'), desc: t('landing.layout.p1.desc'), icon: }, - { title: t('landing.layout.p2.title'), desc: t('landing.layout.p2.desc'), icon: }, - { title: t('landing.layout.p3.title'), desc: t('landing.layout.p3.desc'), icon: }, - { title: t('landing.layout.p4.title'), desc: t('landing.layout.p4.desc'), icon: }, - { title: t('landing.layout.p5.title'), desc: t('landing.layout.p5.desc'), icon: }, - { title: t('landing.layout.p6.title'), desc: t('landing.layout.p6.desc'), icon: }, - ]; + + // Deux vraies pages de document : même structure, deux langues. + // Le terme en gras montre le respect de la terminologie (glossaire). + const DocPage = ({ side }: { side: "source" | "target" }) => ( +
    + + {side === "source" ? "FR — original" : "EN — traduit"} + +

    + {side === "source" ? t("landing.beforeAfter.sourceTitle") : t("landing.beforeAfter.targetTitle")} +

    +

    + {side === "source" ? t("landing.beforeAfter.sourcePara1") : t("landing.beforeAfter.targetPara1")} +

    +

    + {side === "source" ? ( + <> + {t("landing.beforeAfter.sourcePara2Before")}{" "} + {t("landing.beforeAfter.sourceTerm")}{" "} + {t("landing.beforeAfter.sourcePara2After")} + + ) : ( + <> + {t("landing.beforeAfter.targetPara2Before")}{" "} + {t("landing.beforeAfter.targetTerm")}{" "} + {t("landing.beforeAfter.targetPara2After")} + + )} +

    +
      +
    • + + {side === "source" ? t("landing.beforeAfter.sourceItem1") : t("landing.beforeAfter.targetItem1")} +
    • +
    • + + {side === "source" ? t("landing.beforeAfter.sourceItem2") : t("landing.beforeAfter.targetItem2")} +
    • +
    +
    + cahier-des-charges.docx + 3 pages · 2 481 mots +
    +
    + ); return ( -
    -
    -
    -

    - {t('landing.layout.title')}
    {t('landing.layout.title2')} +
    +
    +
    +

    + {t("landing.layout.title")}
    {t("landing.layout.title2")}

    -

    - {t('landing.layout.subtitle')} +

    + {t("landing.layout.subtitle")}

    -
    - {points.map((p, i) => ( -
    -
    - {p.icon} -
    -

    {p.title}

    -

    {p.desc}

    + +
    + + + + {/* sceau central : la promesse, entre les deux pages */} +
    +
    + + + {t("landing.beforeAfter.seal")} + +
    +
    +
    + + {/* trois preuves précises, pas des promesses vagues */} +
    + {[ + { icon: , label: t("landing.beforeAfter.proof1") }, + { icon: , label: t("landing.beforeAfter.proof2") }, + { icon: , label: t("landing.beforeAfter.proof3") }, + ].map((proof, i) => ( +
    + + {proof.icon} + + {proof.label}
    ))}
    @@ -275,7 +330,7 @@ const FormatsSection = () => {
    {f.icon}
    - {f.id} + {f.id}

    {f.name}

      @@ -347,7 +402,7 @@ const PricingSection = () => { diff --git a/frontend/src/lib/i18n/messages/en/landing.json b/frontend/src/lib/i18n/messages/en/landing.json index b9f5d66..cef46a9 100644 --- a/frontend/src/lib/i18n/messages/en/landing.json +++ b/frontend/src/lib/i18n/messages/en/landing.json @@ -150,5 +150,23 @@ "landing.translate.processing": "Processing", "landing.translate.preservingLayout": "Your layout is being preserved", "landing.formats.pill": "COMPATIBILITY", - "landing.hero.visualCaption": "Same layout, new language — nothing else moves" + "landing.hero.visualCaption": "Same layout, new language — nothing else moves", + "landing.beforeAfter.sourceTitle": "Cahier des charges — Traitement d'air", + "landing.beforeAfter.targetTitle": "Technical specification — Air handling", + "landing.beforeAfter.sourcePara1": "L'installation de ventilation doit être mise en service avant la fin du deuxième trimestre.", + "landing.beforeAfter.targetPara1": "The ventilation system must be commissioned before the end of the second quarter.", + "landing.beforeAfter.sourcePara2Before": "Le", + "landing.beforeAfter.sourceTerm": "groupe de traitement d'air", + "landing.beforeAfter.sourcePara2After": "doit respecter la classe de filtration F7 conformément au planning.", + "landing.beforeAfter.targetPara2Before": "The", + "landing.beforeAfter.targetTerm": "air handling unit", + "landing.beforeAfter.targetPara2After": "must comply with filter class F7 as specified in the schedule.", + "landing.beforeAfter.sourceItem1": "Charge thermique : 42 kW au débit nominal", + "landing.beforeAfter.targetItem1": "Thermal load: 42 kW at nominal flow", + "landing.beforeAfter.sourceItem2": "Niveau sonore inférieur à 45 dB(A) à 3 m", + "landing.beforeAfter.targetItem2": "Sound level below 45 dB(A) at 3 m", + "landing.beforeAfter.seal": "Same layout, word for word", + "landing.beforeAfter.proof1": "SmartArt diagrams rebuilt, not flattened", + "landing.beforeAfter.proof2": "Chart series and axes translated", + "landing.beforeAfter.proof3": "Tables of contents regenerated in the target language" } diff --git a/frontend/src/lib/i18n/messages/en/pricing.json b/frontend/src/lib/i18n/messages/en/pricing.json index d80e49a..92853bb 100644 --- a/frontend/src/lib/i18n/messages/en/pricing.json +++ b/frontend/src/lib/i18n/messages/en/pricing.json @@ -159,5 +159,9 @@ "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" + "pricing.confirm.cta": "Continue to payment", + "pricing.freeBand.text": "Just want to try? Start free — 5 documents per month, no card required.", + "pricing.freeBand.cta": "Start free", + "pricing.enterpriseBand.text": "Volume, dedicated engines, on-premise options — let's talk.", + "pricing.enterpriseBand.cta": "Contact us" } diff --git a/frontend/src/lib/i18n/messages/fr/landing.json b/frontend/src/lib/i18n/messages/fr/landing.json index b87fcf4..5836fbc 100644 --- a/frontend/src/lib/i18n/messages/fr/landing.json +++ b/frontend/src/lib/i18n/messages/fr/landing.json @@ -150,5 +150,23 @@ "landing.translate.processing": "Traitement en cours", "landing.translate.preservingLayout": "Votre mise en page est en cours de préservation", "landing.formats.pill": "COMPATIBILITÉ", - "landing.hero.visualCaption": "Même mise en page, nouvelle langue — rien d'autre ne bouge" + "landing.hero.visualCaption": "Même mise en page, nouvelle langue — rien d'autre ne bouge", + "landing.beforeAfter.sourceTitle": "Cahier des charges — Traitement d'air", + "landing.beforeAfter.targetTitle": "Technical specification — Air handling", + "landing.beforeAfter.sourcePara1": "L'installation de ventilation doit être mise en service avant la fin du deuxième trimestre.", + "landing.beforeAfter.targetPara1": "The ventilation system must be commissioned before the end of the second quarter.", + "landing.beforeAfter.sourcePara2Before": "Le", + "landing.beforeAfter.sourceTerm": "groupe de traitement d'air", + "landing.beforeAfter.sourcePara2After": "doit respecter la classe de filtration F7 conformément au planning.", + "landing.beforeAfter.targetPara2Before": "The", + "landing.beforeAfter.targetTerm": "air handling unit", + "landing.beforeAfter.targetPara2After": "must comply with filter class F7 as specified in the schedule.", + "landing.beforeAfter.sourceItem1": "Charge thermique : 42 kW au débit nominal", + "landing.beforeAfter.targetItem1": "Thermal load: 42 kW at nominal flow", + "landing.beforeAfter.sourceItem2": "Niveau sonore inférieur à 45 dB(A) à 3 m", + "landing.beforeAfter.targetItem2": "Sound level below 45 dB(A) at 3 m", + "landing.beforeAfter.seal": "Même mise en page, mot pour mot", + "landing.beforeAfter.proof1": "Les SmartArt sont reconstruits, pas aplatis", + "landing.beforeAfter.proof2": "Séries et axes des graphiques traduits", + "landing.beforeAfter.proof3": "Tables des matières régénérées dans la langue cible" } diff --git a/frontend/src/lib/i18n/messages/fr/pricing.json b/frontend/src/lib/i18n/messages/fr/pricing.json index 854a657..e24bba5 100644 --- a/frontend/src/lib/i18n/messages/fr/pricing.json +++ b/frontend/src/lib/i18n/messages/fr/pricing.json @@ -159,5 +159,9 @@ "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" + "pricing.confirm.cta": "Continuer vers le paiement", + "pricing.freeBand.text": "Juste envie d'essayer ? Commencez gratuitement — 5 documents par mois, sans carte.", + "pricing.freeBand.cta": "Commencer gratuitement", + "pricing.enterpriseBand.text": "Volume, moteurs dédiés, hébergement spécifique — parlons-en.", + "pricing.enterpriseBand.cta": "Nous contacter" }