import { useEffect, useRef } from 'react'; import { useVirtualizer } from '@tanstack/react-virtual'; import type { GridRow } from '@/api/types'; import { SourceText } from '@/components/SourceText'; import { StatusDot } from '@/components/Status'; import { TranslationInput } from '@/editor/TranslationInput'; interface Props { rows: GridRow[]; total: number; loading: boolean; locale: string; direction: 'ltr' | 'rtl'; pluralCategories: string[]; selectedId: string | null; onSelect: (id: string) => void; readOnly: boolean; gridKey: unknown; } /** * La grille de traduction. * * **Virtualisée** : un projet mûr compte des dizaines de milliers de clés, et * une liste non virtualisée effondre le navigateur bien avant. Seules les lignes * visibles existent dans le DOM. * * **Source et cible empilées, pas côte à côte.** Les colonnes obligent à couper * le texte long ; l'empilement laisse respirer les deux valeurs et fonctionne * sur un écran d'ordinateur portable, qui est la réalité d'une traductrice en * déplacement. */ export function TranslationGrid({ rows, total, loading, locale, direction, pluralCategories, selectedId, onSelect, readOnly, }: Props) { const parentRef = useRef(null); // Le panneau de contexte est inutile tant que rien n'est sélectionné, et // demander un clic pour l'activer, c'est afficher une colonne vide au // moment précis où l'utilisateur découvre l'écran. const first = rows[0]?.id; useEffect(() => { if (selectedId === null && first !== undefined) onSelect(first); }, [first, selectedId, onSelect]); const virtualizer = useVirtualizer({ count: rows.length, getScrollElement: () => parentRef.current, // Estimation volontairement basse : `measureElement` corrige au rendu. // Surestimer produirait des sauts de défilement bien plus gênants. estimateSize: () => 132, overscan: 8, getItemKey: (index) => rows[index]?.id ?? index, }); if (loading && rows.length === 0) { return (
Chargement des clés…
); } if (rows.length === 0) { return (

Aucune clé ne correspond.

Élargissez les filtres, ou vérifiez que la plateforme sélectionnée porte bien des clés.

); } return (
{rows.length < total ? `${rows.length} sur ${total}` : `${total}`} clé {total > 1 ? 's' : ''} {locale}
{virtualizer.getVirtualItems().map((item) => { const row = rows[item.index]; if (!row) return null; const selected = row.id === selectedId; return (
onSelect(row.id)} onClick={() => onSelect(row.id)} className={`border-b border-ink-100 px-4 py-3 transition-colors ${ selected ? 'bg-accent-soft/40' : 'bg-white hover:bg-ink-50' }`} >
{row.keyPath} {/* La source modifiée est LE signal qui compte : il désigne un texte peut-être faux en production. */} {row.isStale && ( source modifiée )} {row.platforms.map((slug) => ( {slug} ))} {row.platforms.length === 0 && ( non assignée )}

{row.sourceValue === null ? ( source absente ) : ( )}

{readOnly ? (

{row.targetValue ?? '—'}

) : ( )}
); })}
); }