Files
decision/frontend/app/lexicon.ts
T
YvvandClaude Fable 5 3301b764f7 v2 : correctifs QA — overlays posés, accueil jamais orphelin, finitions mobiles
- overlays Nuxt UI (UModal/USlideover) : géométrie posée en CSS
  (assets/css/overlays.css) — les utilitaires tailwind attendus par @nuxt/ui
  ne sont pas générés (UnoCSS ne scanne pas node_modules) : drawer mobile et
  modales de confirmation se rendaient hors écran
- a11y : title/description sur les 3 overlays (DialogTitle/Description
  rendus, warnings reka-ui éteints), fermeture du drawer ancrée et testée
- suppression du collectif actif : bascule sur un collectif restant —
  jamais d'accueil orphelin (test store ajouté, 343 vitest verts)
- observatoire : la route « transmis » entre dans les barres (base 100 %)
- overflows mobiles à zéro : formules KaTeX (défilement), table vigueur
  (défilement < 768px), pill cible (retour à la ligne), inputs decider et
  mandat (border-box)
- vérifié navigateur : drawer, modale, 14 routes sans débordement,
  zéro erreur console

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-11 20:11:54 +02:00

253 lines
10 KiB
TypeScript

// ─────────────────────────────────────────────────────────────
// libreDecision v2 — lexicon: SINGLE source of French UI labels.
// Tone rules (BLUEPRINT-V2.md):
// 1. tutoyer la personne dans ses gestes ;
// 2. actes collectifs à l'infinitif (« Adopter notre Pacte ») ;
// 3. sujet grammatical des phrases générées = les personnes ou le collectif,
// JAMAIS la formule ou le système.
// The anti-lexicon vitest test (tests/lexicon.spec.ts) enforces FORBIDDEN_UI_TERMS
// over every UI-visible string (lexicon + .vue templates).
// ─────────────────────────────────────────────────────────────
import type {
DecisionRoute,
DecisionStatus,
InertiaPreset,
NominationMethod,
NuancedValue,
VoteMethod,
Weight,
Reversibility,
} from '~/types/domain'
/** Words that must NEVER appear in UI-visible text (case-insensitive, accent-aware).
* They may exist as code identifiers — the test only scans UI strings. */
export const FORBIDDEN_UI_TERMS = [
'déléguer',
'délégation',
'délégué',
'dette',
'retard',
'en souffrance',
'corpus',
'snapshot',
'verdict',
'triage',
'oligarchie',
'peuple adulte',
] as const
// ── Routes — first person: the path is a gesture, not a category ──
export const ROUTE_LABELS: Record<DecisionRoute, string> = {
solo: 'Je décide',
mandate: 'Je décide, sous mandat',
transmit: 'Je transmets',
advice: "J'écoute, puis je décide",
collective: 'Nous décidons',
record: 'Je le consigne',
}
/** Short pill forms (registry filters). */
export const ROUTE_SHORT: Record<DecisionRoute, string> = {
solo: 'seul',
mandate: 'sous mandat',
transmit: 'transmis',
advice: 'sur avis',
collective: 'ensemble',
record: 'consigné',
}
export const ROUTE_ICONS: Record<DecisionRoute, string> = {
solo: 'i-lucide-zap',
mandate: 'i-lucide-key-round',
transmit: 'i-lucide-send',
advice: 'i-lucide-message-circle',
collective: 'i-lucide-users',
record: 'i-lucide-notebook-pen',
}
export const ICONS = {
urgent: 'i-lucide-siren',
alreadyDecided: 'i-lucide-book-open-check',
recurrence: 'i-lucide-repeat',
parametric: 'i-lucide-sliders-horizontal',
crystallize: 'i-lucide-stamp',
} as const
// ── Decision states — plain French pills ──
export const STATUS_LABELS: Record<DecisionStatus, string> = {
draft: 'préparation',
advice: 'avis',
objection: 'objection',
framing: 'formulation',
voting: 'vote',
adopted: 'en vigueur',
rejected: 'rejeté',
revoked: 'révoqué',
transmitted: 'transmis',
closed: 'clos',
}
/** Session display state (frozen lives on VoteSession, shown as a decision badge). */
export const FROZEN_LABEL = 'figé'
export const FROZEN_BANNER = 'Votes figés — en attente de cristallisation'
// ── Weight & reversibility — the tunnel chips ──
export const WEIGHT_LABELS: Record<Weight, string> = {
light: 'rien de lourd',
binding: 'du temps ou des ressources',
structural: 'la structure, les règles, du long terme',
}
export const REVERSIBILITY_LABELS: Record<Reversibility, string> = {
easy: 'facilement réversible',
costly: 'coûteux à défaire',
irreversible: 'irréversible',
}
// ── Nuanced scale — never « CONTRE » ──
export const NUANCED_LABELS: Record<NuancedValue, string> = {
0: 'Pas du tout',
1: 'Plutôt non',
2: 'Réservé',
3: 'Plutôt oui',
4: 'Oui',
5: 'Tout à fait',
}
// ── Vote methods ──
export const METHOD_LABELS: Record<VoteMethod, string> = {
consent: 'Consentement',
binary: 'Pour / contre',
nuanced: 'Vote nuancé',
parametric: 'Réglage collectif',
election: 'Élection',
}
export const BINARY_COST = 'deux camps, un perdant'
export const BINARY_DESCRIPTION = "l'outil de dernier recours des très grands corps"
// ── The review — l'épreuve du réel ──
export const REVIEW_TITLE = "L'épreuve du réel"
export const REVIEW_QUESTION = 'Le réel a-t-il suivi ?'
export const REVIEW_VERDICTS = {
confirmed: 'Ça tient',
revise: 'À revoir',
revoke: 'À révoquer',
} as const
// ── Windows, assent, perimeter ──
export const WINDOW_OK = 'Ça me va'
export const WINDOW_OBJECT = "J'objecte"
export const ASSENT_MISSING = 'Il manque un accord explicite'
export const BOUNDARY_SUSPENDED = 'compte à rebours suspendu — frontière contestée'
export const CONCERN_FIRST = 'Concerné·e en premier lieu'
export const CONCERN_SECOND = "Concerné·e en second lieu — s'est déclaré·e"
export const CONCERN_ME = 'Ça me concerne'
export const PERIMETER_OVERFLOW = "Le périmètre déborde — élargis d'un cran"
export const SCOPE_WIDEN = "Élargir d'un cran"
export const SCOPE_KEEP = 'Maintenir — en motivant publiquement'
// ── Parametric — le réglage collectif ──
export const PARAMETRIC_ALT = 'C\'est un réglage (montant, taux, répartition) — décidez au curseur'
export const PARAM_CONSTRAINT_LABEL = 'contrainte'
export const PARAM_DERIVED_LABEL = 'calculé'
export const IMPACT_CARD_TITLE = 'Pour moi'
export const IMPACT_DISCLAIMER = 'estimation sur données déclarées'
export const EXPLORE_LABEL = 'Explorer'
export const EXPLORE_DISCLAIMER = 'exploration — ne compte pas'
export const CRYSTALLIZE_ACTION = 'Cristalliser'
export const CRYSTALLIZE_CARD = 'Les votes sont figés — cristallise la médiane'
export const MEDIAN_EXPLANATION =
'Le collectif retient la médiane de chaque curseur — robuste aux extrêmes, '
+ "et c'est une position que chacun aurait pu proposer."
export const BIMODAL_BANNER = (paramLabel: string) =>
`Deux positions distinctes se dessinent sur ${paramLabel} — scinder la question ou changer de référentiel ?`
export const CRYSTALLIZE_CHOICES = ['Cristalliser quand même', 'Scinder', 'Reformuler'] as const
export const CRYSTALLIZE_MODAL_DESC = "Tu figes d'un geste la position que le collectif dessine."
// ── Record & observatory ──
export const RECORD_ALT = 'Déjà tranché — je le consigne'
export const RECORD_HOW = "Comment ça s'est décidé ?"
export const OBSERVATORY_TITLE = "L'Observatoire"
export const OBSERVATORY_SUBTITLE = 'comment nous décidons, dans les faits'
export const REVOKED_SECTION = "Révoquées — ce qu'on en a appris"
export const MATURATION_CARD = 'Cette pratique est mûre — protocolise-la en quelques phrases'
// ── Élection ──
export const BLANK_VOTE = 'Vote blanc'
export const ELECTION_RULE =
"La personne la plus désignée est nommée ; en cas d'égalité, vous départagez — jamais l'outil."
// ── Urgence ──
export const URGENT_TOGGLE = "C'est urgent"
export const URGENT_BADGE = 'Décidé en urgence — le collectif ratifie'
export const URGENT_REFUSED = "Irréversible : l'urgence ne peut pas contourner le collectif."
// ── Fil / Aujourd'hui ──
export const FEED_HEADER = 'À toi de décider'
export const CAPTURE_PLACEHOLDER = "Qu'est-ce que tu décides ?"
export const DOSSIER_COMPLETE_CARD = 'Le dossier est complet — clore et publier la cartographie de clôture'
// ── Fiche / tunnel ──
export const PATH_CARD_TITLE = 'Le chemin'
export const CHOOSE_OTHERWISE = 'Je choisis autrement'
export const WHY_DISCLOSURE = 'Pourquoi ?'
export const ENGAGES_LABEL = 'Ce que ça engage'
export const BASELINE_PREFIX = "Aujourd'hui :"
export const BASELINE_ARROW = "Aujourd'hui → Proposé"
export const INSTRUCT_BLOCK = "S'instruire"
export const REOPEN_HANDLE = 'Remettre en question'
export const ADOPTED_STAMP = 'Décidé'
export const ADOPTED_TOAST = "C'est décidé — et révisable, comme tout ici."
export const VOTE_PRIVACY = "Ton vote est remplaçable — ton historique n'est visible que de toi."
export const WHO_VOTES = (n: number, date: string) =>
`Qui vote : ${n} personne${n > 1 ? 's' : ''} — liste arrêtée le ${date}`
// ── Mandats ──
export const MANDATES_TITLE = 'Mandats — les pouvoirs confiés'
export const MANDATE_EXERCISE = 'Exercice du mandat'
export const MANDATE_SPOTLIGHT = 'Feux de la rampe'
export const MANDATE_SPOTLIGHT_SUB = 'toutes les décisions prises sous ce mandat — contestables'
export const MANDATE_CLAIM = 'Réclamer un mandat'
export const MANDATE_REVOKE = 'Demander la révocation'
export const NOMINATION_LABELS: Record<NominationMethod, string> = {
'ratified-self': 'Auto-désignation ratifiée',
'election-no-candidate': 'Élection sans candidat',
'nuanced-vote': 'Vote nuancé',
consent: 'Consentement',
draw: 'Tirage au sort',
rotation: 'Rotation',
}
// ── Textes / Pacte ──
export const PACT_BADGE = 'les règles du jeu'
export const PACT_SUBTITLE = 'Notre contrat social — sacralisé, jamais immuable'
export const INERTIA_LABELS: Record<InertiaPreset, string> = {
low: 'inertie faible',
standard: 'inertie standard',
high: 'inertie haute',
max: 'inertie maximale',
}
export const FIRST_DECISION = 'Adopter notre Pacte'
export const CREATE_SIGNATURE = 'Osons, nous verrons'
export const OPENING_DREAM = "Une démocratie qui n'a jamais existé — à votre échelle."
export const CLOSING_LINE = 'La ville change, le puits demeure.'
// ── Preuve / gravure / données ──
export const PROOF_LOCAL = 'empreinte locale — démo'
export const SECRET_DISPLAY = "secret d'affichage — démo"
export const LINEAGE_PREFIX = 'essaimé de'
export const WORKSHOP_MODE = 'mode atelier'
export const WORKSHOP_RECORDED = (by: string, forWhom: string, date: string) =>
`saisi par ${by} pour ${forWhom} — atelier du ${date}`
export const ATTRIBUTES_HINT =
'servent uniquement ta carte Pour moi, jamais montrés aux autres'
export const SYNC_NOTE =
'Un serveur pourra se brancher plus tard, tes données ne changent pas de forme.'
export const NO_PROTOCOL_BANNER = 'Aucun protocole — crée-le ou décide sur avis'
// ── Génération des phrases de seuil (sujet = le collectif, jamais la formule) ──
export const thresholdSentence = (W: number, T: number, threshold: number) =>
`${T.toLocaleString('fr-FR')} votant${T > 1 ? 's' : ''} sur ${W.toLocaleString('fr-FR')} : `
+ `à si faible participation, il faut presque l'unanimité — ${threshold.toLocaleString('fr-FR')} pour. `
+ 'Plus de monde vote, plus le seuil descend.'