v3 : interface Svelte — accueil, groupes, lecteur du texte en vigueur, panneau contextuel, éditeur à assistant, trustinWallet

- Accueil sobre, menu complet à gauche (Décisions, Docs de référence, Mandats,
  Protocoles), intro demandée, quatre sections, connexion Ğ1.
- Lecteur : texte en vigueur en Literata, actions contextuelles par article
  (le vote, propositions, proposer), propositions en cours sous la « surface »
  avec leur niveau d'eau, aperçus « si les bien cotées passent » / « si tout
  passe », langue du texte fr/en/es (traductions éditoriales).
- Panneau : vote pour/contre, seuil et formule (MathML), inertie et son
  changement (voté en inertie +), coquille sans vote, traduction, signalement
  de contradiction, historique des corrections.
- trustinWallet (/acces) : groupes et membres, invitations à usage unique,
  administrateurs, retours d'usage.
- Contrôles : vitest (markdown, catalogues, anti-lexique), Playwright C1–C10 + M1.
- Sections du seed portées jusqu'à l'écran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-10-21 02:58:34 +02:00
co-authored by Claude Opus 5.5
parent 67ddee28b2
commit 7a022dd35e
66 changed files with 7771 additions and 15 deletions
+315
View File
@@ -0,0 +1,315 @@
import type { Messages } from './fr';
export const en: Messages = {
'app.name': 'libreDecision',
'app.skip': 'Skip to content',
'nav.label': 'Toolbox',
'nav.home': 'Home',
'nav.decisions': 'Decisions',
'nav.docs': 'Reference documents',
'nav.mandates': 'Mandates',
'nav.protocols': 'Protocols',
'nav.soon': 'coming soon',
'nav.groups': 'My groups',
'nav.access': 'Access',
'nav.access.hint': 'trustinWallet',
'nav.feedback': 'Give feedback',
'nav.signin': 'Sign in',
'nav.signout': 'Sign out',
'nav.mood.toNight': 'Night mood',
'nav.mood.toStone': 'Stone mood',
'nav.lang': 'Language',
'nav.menu': 'Menu',
'nav.close': 'Close',
'home.title': 'Making collective decisions possible',
'home.subtitle': 'depending on the context, processes and methods, a toolbox',
'home.decisions': 'A guided path to choose the right decision methods, and to keep the decision log.',
'home.docs': 'Commitment act, license, charter, constitution: living texts, always open for voting.',
'home.mandates': 'Create and revoke any kind of mandate.',
'home.protocols': 'Workflows to organize without a statutory body: rotating roles and technical automations.',
'home.open': 'open',
'home.soon': 'coming soon',
'home.signin': 'Sign in with your Ğ1 key',
'home.signin.note': 'The same access as the Monnaie libre forum: you sign with Ğecko, no password needed.',
'home.continue': 'Open my groups',
'signin.title': 'Sign in',
'signin.g1': 'Sign in with Ğ1',
'signin.g1.how': "You'll be sent to connect.monnaie-libre.fr, the sign-in service of the Monnaie libre forum. There you approve the sign-in in Ğecko (link or QR code), then come back here. No password and no key is ever passed to us.",
'signin.g1.members': 'For members of the Ğ1 web of trust only.',
'signin.g1.pending': "Ğ1 sign-in is on its way: libreDecision's registration with connect.monnaie-libre.fr is in progress. Meanwhile, you get in with an invitation link.",
'signin.invite': 'Got an invitation link? Just open it: it signs you in.',
'signin.dev': 'Development sign-in',
'signin.dev.name': 'Name',
'signin.dev.admin': 'access administrator',
'signin.dev.go': 'Enter',
'signin.error.refus': 'The sign-in was declined on connect.monnaie-libre.fr.',
'signin.error.etat': 'The sign-in expired or came from another browser. Start again from this browser.',
'signin.error.incomplet': 'The response from connect.monnaie-libre.fr was incomplete. Start again.',
'signin.error.echange': "connect.monnaie-libre.fr isn't responding. Try again in a moment.",
'signin.error.interne': 'Sign-in failed on our side. Try again in a moment.',
'invite.title': 'Invitation',
'invite.working': 'Signing in…',
'invite.invalid': 'This invitation link is no longer valid: it has already been used or it has expired. Ask the person who sent it to you for a new link.',
'invite.missing': 'This link is incomplete. Open the whole invitation link, exactly as you received it.',
'groups.title': 'My groups',
'groups.same.key': 'Groups that use the same key',
'groups.locked': 'Members only',
'groups.locked.long': 'This group opens to its members. Ask a trustinWallet administrator for access.',
'groups.test': 'For testing',
'groups.open': 'Open',
'groups.empty': 'No group is visible yet.',
'group.tools': 'Tools',
'group.tool.soon': 'This tool is coming later.',
'group.docs.empty': 'No reference documents in this group yet.',
'docs.title': 'Reference documents',
'docs.inForce.one': '{n} article in force',
'docs.inForce.other': '{n} articles in force',
'docs.open.one': '{n} open proposal',
'docs.open.other': '{n} open proposals',
'doc.view.label': 'Read',
'doc.view.inforce': 'In force',
'doc.view.wellrated': 'If well-rated ones pass',
'doc.view.all': 'If everything passes',
'doc.view.wellrated.note': 'Preview: the text as it would be if the well-rated proposals came into force. Nothing is adopted.',
'doc.view.all.note': 'Preview: the text as it would be if all proposals came into force, even those with little support. Nothing is adopted.',
'doc.view.empty': 'No proposal changes the text in this preview.',
'doc.mark.added': 'added',
'doc.mark.replaced': 'changed',
'doc.mark.removed': 'removed',
'doc.lang.label': 'Text language',
'doc.lang.translation': 'Editorial translation, not voted on. The authoritative text is in French.',
'doc.lang.original': 'Read the original',
'doc.lang.stale': 'translation needs review',
'doc.lang.missing': 'not translated yet',
'doc.seed': 'seed a minima',
'doc.seed.long': 'Came into force without a vote, so as not to start from a blank page. It remains open for voting: 0 votes so far.',
'doc.confirmed': 'confirmed by vote',
'doc.adopted': 'adopted on {date}',
'doc.action.vote': 'Voting',
'doc.action.proposals': 'Proposals',
'doc.action.propose': 'Propose a change',
'doc.empty': 'No article is in force. The proposals below move up as soon as they reach their threshold.',
'doc.published': 'Published to the text repository',
'doc.publish.pending': 'Publication pending',
'deep.title': 'Open proposals',
'deep.note': 'Not in force yet. Each one moves up as soon as it reaches its threshold: at least {floor} votes in favor, then the majority its inertia requires.',
'deep.new': 'Propose a new article',
'deep.drafts': 'Your drafts',
'deep.drafts.note': 'Not submitted to vote. Only you can see them.',
'deep.rival': 'would replace “{title}”',
'deep.addition': 'new article',
'deep.removal': 'removal of “{title}”',
'deep.inertia': 'inertia: {level}',
'deep.blocked': 'blocked',
'deep.mine': 'your proposal',
'deep.stale': 'needs reconfirming',
'deep.wellrated': 'well-rated',
'deep.filter.all': 'All',
'deep.filter.wellrated': 'Well-rated',
'deep.filter.mine': 'Mine',
'deep.none': 'No proposals in this filter.',
'deep.variant': 'variant {label}',
'section.introduction': 'Introduction',
'section.fondamental': 'Fundamental commitments',
'section.technique': 'Technical commitments',
'section.conclusion': 'Conclusion',
'section.annexe': 'Annexes',
'section.formule': 'Voting formula',
'section.inertie': 'Inertia',
'section.ordonnancement': 'Ordering',
'tally.short': '{for} for · {against} against',
'tally.required': 'Threshold: {n} votes in favor',
'tally.missing.one': '{n} more vote in favor needed.',
'tally.missing.other': '{n} more votes in favor needed.',
'tally.reached': 'Threshold reached.',
'tally.level': 'Level: {pct}% of the threshold',
'inertia.low': 'low inertia',
'inertia.standard': 'standard inertia',
'inertia.high': 'high inertia',
'inertia.max': 'maximum inertia',
'inertia.level.low': 'low',
'inertia.level.standard': 'standard',
'inertia.level.high': 'high',
'inertia.level.max': 'maximum',
'panel.vote': 'Voting',
'panel.proposals': 'Proposals',
'panel.propose': 'Propose',
'panel.close': 'Close panel',
'panel.inforce': 'In force',
'panel.proposal': 'Proposal',
'panel.by': 'proposed by {name}',
'panel.seed.origin': 'text from the seed',
'panel.changes': 'View changes',
'panel.text': 'View text',
'vote.yours': 'Your vote',
'vote.for': 'For',
'vote.against': 'Against',
'vote.retract': 'Withdraw my vote',
'vote.saved': 'Vote saved.',
'vote.retracted': 'Vote withdrawn.',
'vote.stale': 'The text has changed in substance since you voted: your vote no longer counts until you reconfirm it.',
'vote.reconfirm': 'Reconfirm: {choice}',
'vote.private': 'Nobody sees your vote: only the totals are visible.',
'vote.blocked': "A contradiction flag is holding it back: it can't come into force while the flag stands.",
'vote.entered': '“{title}” has come into force.',
'vote.entered.remove': '“{title}” has been removed from the text in force.',
'vote.entered.inertia': 'The inertia of “{title}” has changed.',
'vote.closed': 'This version is no longer open for voting.',
'inertia.title': 'Inertia',
'inertia.explain': 'The higher the inertia, the more votes it takes to change this text when few people vote.',
'inertia.change': 'Propose an inertia change',
'inertia.change.to': 'New inertia',
'inertia.change.note': 'An inertia change is itself voted on with the next inertia up: {level}.',
'inertia.change.submit': 'Submit to vote',
'inertia.change.open': 'Inertia change in progress: to {level}',
'formula.title': 'The threshold formula',
'formula.intro': 'The number of votes in favor required, R, depends on turnout:',
'formula.W': 'W — electorate: {n} ({source})',
'formula.W.wot': 'members of the Ğ1 web of trust',
'formula.W.group': 'group members',
'formula.T': 'T — votes cast: {n}',
'formula.M': 'M — majority at full turnout: {n}%',
'formula.G': 'G — gradient: {n}',
'formula.B': 'B — base: {n}',
'formula.C': 'C — minimum votes in favor: {n}',
'formula.R': 'R — current threshold: {n} votes in favor',
'formula.reading': 'At low turnout, near-unanimity is needed; when everyone votes, majority M is enough. Nothing passes with fewer than C votes in favor.',
'history.title': 'Corrections',
'history.initial': 'initial text',
'history.typo': 'typo fixed',
'history.substantive': 'substantive change (votes to reconfirm)',
'history.by': 'by {name}',
'typo.title': 'Fix a typo',
'typo.note': 'Accents, punctuation, typos: the fix applies without a vote, with its author and its diff. A change in meaning goes through a proposal.',
'typo.save': 'Save the fix',
'typo.done': 'Typo fixed, votes untouched.',
'typo.substantive': 'This edit affects the meaning: propose it as a change.',
'typo.asProposal': 'Make it a proposal',
'signal.title': 'Contradictions',
'signal.none': 'No flags.',
'signal.add': 'Flag a contradiction',
'signal.target': 'Contradicts',
'signal.reason': 'How these two texts contradict each other',
'signal.send': 'Flag',
'signal.sent': 'Flag saved.',
'signal.note': "While a flag stands, these two texts can't be in force together. Its author lifts it; a substantive change makes it lapse.",
'signal.lift': 'Lift',
'signal.lifted': 'lifted',
'signal.lifted.revision': 'lapsed: the text changed',
'signal.by': 'by {name}',
'propose.mode.replace': 'Change this text',
'propose.mode.remove': 'Remove this article',
'propose.mode.new': 'New article',
'propose.after': 'Place after',
'propose.after.end': 'at the end',
'propose.title': 'Title',
'propose.body': 'Text',
'propose.remove.note': 'A removal proposal is voted on like any other; if it passes, the article leaves the text in force.',
'propose.draft': 'Save draft',
'propose.submit': 'Submit to vote',
'propose.drafted': 'Draft saved.',
'propose.submitted': 'Proposal submitted to vote.',
'propose.rule': "While it's a draft, you edit it freely. Once it's submitted, a typo can be fixed without touching the votes; a substantive change asks voters to reconfirm.",
'propose.edit': 'Edit',
'propose.withdraw': 'Withdraw my proposal',
'propose.withdrawn': 'Proposal withdrawn.',
'propose.saved.typo': 'Fix saved, the votes stand.',
'propose.saved.substantive': 'New version saved: voters will have to reconfirm.',
'assist.title': 'Assistant',
'assist.quiet': 'Nothing to report: one proposal, one decision.',
'assist.split': 'This text seems to carry {n} decisions. One proposal = one decision: split it?',
'assist.split.do': 'Split into {n} drafts',
'assist.split.done': 'Split into {n} drafts.',
'assist.similar': 'Close to “{title}”{where}.',
'assist.similar.inForce': ' (in force)',
'assist.similar.variant': 'Make it a variant of this article',
'assist.long': 'Long text ({n} characters): a short article is easier to vote on.',
'translate.title': 'Translate ({lang})',
'translate.save': 'Save translation',
'translate.saved': 'Translation saved.',
'translate.note': 'Translations are not voted on. They switch to “needs review” when the French text changes in substance.',
'feedback.title': 'Give feedback',
'feedback.prompt': 'What you did, what you expected, what happened.',
'feedback.send': 'Send feedback',
'feedback.sent': 'Feedback sent. Thank you.',
'feedback.context': 'The page address, language and screen size are attached.',
'access.title': 'trustinWallet — access',
'access.intro': 'Who opens which group. Monnaie libre opens by itself to members of the Ğ1 web of trust; other groups open one by one.',
'access.groups': 'Groups',
'access.members.none': 'No members granted access.',
'access.members.wot': '+ all members of the Ğ1 web of trust',
'access.grant': 'Grant',
'access.revoke': 'Remove',
'access.choose': 'Choose an identity',
'access.invitations': 'Invitations',
'access.invite.label': "Who it's for",
'access.invite.groups': 'Groups opened by the link',
'access.invite.admin': 'access administrator',
'access.invite.create': 'Create invitation link',
'access.invite.link': "Invitation link, single use, valid for 14 days. It won't be shown again: copy it now.",
'access.invite.copy': 'Copy link',
'access.invite.copied': 'Link copied.',
'access.invite.cancel': 'Cancel',
'access.invite.pending': 'Pending',
'access.identities': 'Identities',
'access.admin': 'administrator',
'access.admin.give': 'Make administrator',
'access.admin.take': 'Remove role',
'access.kind.g1': 'Ğ1',
'access.kind.invite': 'invitation',
'access.kind.dev': 'dev',
'access.member': 'web of trust member',
'access.feedback': 'Feedback',
'access.feedback.none': 'No feedback.',
'access.feedback.seen': 'Seen',
'access.feedback.done': 'Handled',
'access.denied': 'Access management is for trustinWallet administrators only.',
'soon.title': 'Coming soon',
'soon.body': 'This tool is coming later. Tools open one at a time: reference documents first.',
'error.unauthenticated': 'Sign in to continue.',
'error.forbidden': "You don't have access to this page.",
'error.not_found': "This page doesn't exist, or isn't visible to you.",
'error.network': "The server isn't responding. Check your connection and try again.",
'error.internal': 'Something failed on our side. Try again in a moment.',
'error.title': 'The title is empty or too long (120 characters max).',
'error.body': 'The text is empty or too long (4,000 characters max).',
'error.reason': 'Say in a few words how these texts contradict each other (10 to 600 characters).',
'error.substantive_not_author': "This edit affects the meaning: only the proposal's author can make it. Propose your own version.",
'error.substantive_in_force': 'This edit affects the meaning: propose it as a change.',
'error.same_unit': "These two texts target the same article: they can't be in force together anyway.",
'error.last_admin': 'At least one administrator must remain.',
'error.g1_not_configured': "Ğ1 sign-in isn't active yet.",
'error.invitation_invalid': 'This invitation link is no longer valid.',
'error.retry': 'Try again',
'common.cancel': 'Cancel',
'common.close': 'Close',
'common.save': 'Save',
'common.loading': 'Loading…',
'common.back': 'Back',
};
+315
View File
@@ -0,0 +1,315 @@
import type { Messages } from './fr';
export const es: Messages = {
'app.name': 'libreDecision',
'app.skip': 'Ir al contenido',
'nav.label': 'Caja de herramientas',
'nav.home': 'Inicio',
'nav.decisions': 'Decisiones',
'nav.docs': 'Documentos de referencia',
'nav.mandates': 'Mandatos',
'nav.protocols': 'Protocolos',
'nav.soon': 'próximamente',
'nav.groups': 'Mis grupos',
'nav.access': 'Accesos',
'nav.access.hint': 'trustinWallet',
'nav.feedback': 'Dejar un comentario',
'nav.signin': 'Iniciar sesión',
'nav.signout': 'Cerrar sesión',
'nav.mood.toNight': 'Ambiente noche',
'nav.mood.toStone': 'Ambiente piedra',
'nav.lang': 'Idioma',
'nav.menu': 'Menú',
'nav.close': 'Cerrar',
'home.title': 'Hacer posible la decisión colectiva',
'home.subtitle': 'según el contexto, procesos y modalidades, caja de herramientas',
'home.decisions': 'Un recorrido guiado para elegir las modalidades de decisión adecuadas y llevar el registro de las decisiones.',
'home.docs': 'Acta de compromiso, licencia, carta, constitución: textos vivos, en votación permanente.',
'home.mandates': 'Crear y revocar todo tipo de mandato.',
'home.protocols': 'Flujos de trabajo para organizarse sin órgano estatutario: roles rotativos y automatizaciones técnicas.',
'home.open': 'abierto',
'home.soon': 'próximamente',
'home.signin': 'Iniciar sesión con tu clave Ğ1',
'home.signin.note': 'El mismo acceso que el foro Monnaie libre: firmas en Ğecko, sin contraseña.',
'home.continue': 'Abrir mis grupos',
'signin.title': 'Iniciar sesión',
'signin.g1': 'Iniciar sesión con Ğ1',
'signin.g1.how': 'Se te redirige a connect.monnaie-libre.fr, el servicio de conexión del foro Monnaie libre. Allí apruebas la conexión en Ğecko (enlace o código QR) y luego vuelves aquí. No se nos transmite ninguna contraseña ni ninguna clave.',
'signin.g1.members': 'Reservado a los miembros de la red de confianza Ğ1.',
'signin.g1.pending': 'La conexión Ğ1 llega pronto: el registro de libreDecision en connect.monnaie-libre.fr está en curso. Mientras tanto, se entra con un enlace de invitación.',
'signin.invite': '¿Has recibido un enlace de invitación? Ábrelo sin más: te conecta.',
'signin.dev': 'Conexión de desarrollo',
'signin.dev.name': 'Nombre',
'signin.dev.admin': 'administrador de accesos',
'signin.dev.go': 'Entrar',
'signin.error.refus': 'La conexión fue rechazada en connect.monnaie-libre.fr.',
'signin.error.etat': 'La conexión ha caducado o viene de otro navegador. Vuelve a empezar desde este navegador.',
'signin.error.incomplet': 'La respuesta de connect.monnaie-libre.fr llegó incompleta. Vuelve a empezar.',
'signin.error.echange': 'connect.monnaie-libre.fr no responde. Inténtalo de nuevo en un momento.',
'signin.error.interne': 'La conexión ha fallado por nuestra parte. Inténtalo de nuevo en un momento.',
'invite.title': 'Invitación',
'invite.working': 'Conectando…',
'invite.invalid': 'Este enlace de invitación ya no es válido: ya se ha usado o ha caducado. Pide un enlace nuevo a la persona que te lo envió.',
'invite.missing': 'Este enlace está incompleto. Abre el enlace de invitación completo, tal como lo recibiste.',
'groups.title': 'Mis grupos',
'groups.same.key': 'Grupos que usan la misma clave',
'groups.locked': 'Reservado a sus miembros',
'groups.locked.long': 'Este grupo se abre a sus miembros. El acceso se pide a un administrador de trustinWallet.',
'groups.test': 'Para pruebas',
'groups.open': 'Abrir',
'groups.empty': 'Por ahora no hay ningún grupo visible.',
'group.tools': 'Herramientas',
'group.tool.soon': 'Esta herramienta llegará más adelante.',
'group.docs.empty': 'Por ahora no hay ningún documento de referencia en este grupo.',
'docs.title': 'Documentos de referencia',
'docs.inForce.one': '{n} artículo en vigor',
'docs.inForce.other': '{n} artículos en vigor',
'docs.open.one': '{n} propuesta en curso',
'docs.open.other': '{n} propuestas en curso',
'doc.view.label': 'Leer',
'doc.view.inforce': 'En vigor',
'doc.view.wellrated': 'Si pasan las bien valoradas',
'doc.view.all': 'Si todo pasa',
'doc.view.wellrated.note': 'Vista previa: el texto tal como quedaría si las propuestas bien valoradas entraran en vigor. No se adopta nada.',
'doc.view.all.note': 'Vista previa: el texto tal como quedaría si todas las propuestas entraran en vigor, incluso las poco apoyadas. No se adopta nada.',
'doc.view.empty': 'Ninguna propuesta cambia el texto en esta vista previa.',
'doc.mark.added': 'añadido',
'doc.mark.replaced': 'modificado',
'doc.mark.removed': 'retirado',
'doc.lang.label': 'Idioma del texto',
'doc.lang.translation': 'Traducción editorial, sin votación. El texto que hace fe está en francés.',
'doc.lang.original': 'Leer el original',
'doc.lang.stale': 'traducción por revisar',
'doc.lang.missing': 'aún sin traducir',
'doc.seed': 'seed a minima',
'doc.seed.long': 'Entró en vigor sin votación, para no partir de una página en blanco. Sigue en votación: 0 votos por ahora.',
'doc.confirmed': 'confirmado por la votación',
'doc.adopted': 'adoptado el {date}',
'doc.action.vote': 'Votación',
'doc.action.proposals': 'Propuestas',
'doc.action.propose': 'Proponer una modificación',
'doc.empty': 'Ningún artículo está en vigor. Las propuestas de abajo suben en cuanto alcanzan su umbral.',
'doc.published': 'Publicado en el repositorio de textos',
'doc.publish.pending': 'Publicación pendiente',
'deep.title': 'Propuestas en curso',
'deep.note': 'Aún no están en vigor. Cada una sube en cuanto alcanza su umbral: al menos {floor} votos a favor y, después, la mayoría que exige su inercia.',
'deep.new': 'Proponer un artículo nuevo',
'deep.drafts': 'Tus borradores',
'deep.drafts.note': 'No sometidos a votación. Solo tú los ves.',
'deep.rival': 'reemplazaría «{title}»',
'deep.addition': 'artículo nuevo',
'deep.removal': 'retirada de «{title}»',
'deep.inertia': 'inercia: {level}',
'deep.blocked': 'bloqueada',
'deep.mine': 'tu propuesta',
'deep.stale': 'por reconfirmar',
'deep.wellrated': 'bien valorada',
'deep.filter.all': 'Todas',
'deep.filter.wellrated': 'Bien valoradas',
'deep.filter.mine': 'Las mías',
'deep.none': 'Ninguna propuesta en este filtro.',
'deep.variant': 'variante {label}',
'section.introduction': 'Introducción',
'section.fondamental': 'Compromisos fundamentales',
'section.technique': 'Compromisos técnicos',
'section.conclusion': 'Conclusión',
'section.annexe': 'Anexos',
'section.formule': 'Fórmula de votación',
'section.inertie': 'Inercia',
'section.ordonnancement': 'Ordenación',
'tally.short': '{for} a favor · {against} en contra',
'tally.required': 'Umbral: {n} votos a favor',
'tally.missing.one': 'Falta {n} voto a favor.',
'tally.missing.other': 'Faltan {n} votos a favor.',
'tally.reached': 'Umbral alcanzado.',
'tally.level': 'Nivel: {pct} % del umbral',
'inertia.low': 'inercia baja',
'inertia.standard': 'inercia estándar',
'inertia.high': 'inercia alta',
'inertia.max': 'inercia máxima',
'inertia.level.low': 'baja',
'inertia.level.standard': 'estándar',
'inertia.level.high': 'alta',
'inertia.level.max': 'máxima',
'panel.vote': 'Votación',
'panel.proposals': 'Propuestas',
'panel.propose': 'Proponer',
'panel.close': 'Cerrar el panel',
'panel.inforce': 'En vigor',
'panel.proposal': 'Propuesta',
'panel.by': 'propuesto por {name}',
'panel.seed.origin': 'texto procedente del seed',
'panel.changes': 'Ver los cambios',
'panel.text': 'Ver el texto',
'vote.yours': 'Tu voto',
'vote.for': 'A favor',
'vote.against': 'En contra',
'vote.retract': 'Retirar mi voto',
'vote.saved': 'Voto registrado.',
'vote.retracted': 'Voto retirado.',
'vote.stale': 'El texto ha cambiado de fondo desde tu voto: tu voto deja de contar hasta que lo reconfirmes.',
'vote.reconfirm': 'Reconfirmar: {choice}',
'vote.private': 'Tu voto no se muestra a nadie: solo se ven los totales.',
'vote.blocked': 'Un aviso de contradicción la retiene: no puede entrar en vigor mientras se mantenga.',
'vote.entered': '«{title}» ha entrado en vigor.',
'vote.entered.remove': '«{title}» se ha retirado del texto en vigor.',
'vote.entered.inertia': 'La inercia de «{title}» ha cambiado.',
'vote.closed': 'Esta versión ya no está en votación.',
'inertia.title': 'Inercia',
'inertia.explain': 'Cuanto más alta es la inercia, más votos hacen falta para cambiar este texto cuando vota poca gente.',
'inertia.change': 'Proponer un cambio de inercia',
'inertia.change.to': 'Nueva inercia',
'inertia.change.note': 'Un cambio de inercia se vota a su vez con la inercia superior: {level}.',
'inertia.change.submit': 'Someter a votación',
'inertia.change.open': 'Cambio de inercia en curso: a {level}',
'formula.title': 'La fórmula del umbral',
'formula.intro': 'El número de votos a favor exigido, R, depende de la participación:',
'formula.W': 'W — electorado: {n} ({source})',
'formula.W.wot': 'miembros de la red de confianza Ğ1',
'formula.W.group': 'miembros del grupo',
'formula.T': 'T — votos emitidos: {n}',
'formula.M': 'M — mayoría con participación plena: {n} %',
'formula.G': 'G — gradiente: {n}',
'formula.B': 'B — base: {n}',
'formula.C': 'C — mínimo de votos a favor: {n}',
'formula.R': 'R — umbral actual: {n} votos a favor',
'formula.reading': 'Con poca participación, hace falta casi la unanimidad; cuando vota todo el mundo, basta la mayoría M. Nada pasa con menos de C votos a favor.',
'history.title': 'Correcciones',
'history.initial': 'texto inicial',
'history.typo': 'errata corregida',
'history.substantive': 'cambio de fondo (votos por reconfirmar)',
'history.by': 'por {name}',
'typo.title': 'Corregir una errata',
'typo.note': 'Acentos, puntuación, erratas: la corrección se aplica sin votación, con su autor y su diff. Un cambio de sentido pasa por una propuesta.',
'typo.save': 'Guardar la corrección',
'typo.done': 'Errata corregida, sin tocar los votos.',
'typo.substantive': 'Este cambio afecta al sentido: proponlo como modificación.',
'typo.asProposal': 'Convertirlo en propuesta',
'signal.title': 'Contradicciones',
'signal.none': 'Ningún aviso.',
'signal.add': 'Señalar una contradicción',
'signal.target': 'Contradice',
'signal.reason': 'En qué se contradicen estos dos textos',
'signal.send': 'Señalar',
'signal.sent': 'Aviso registrado.',
'signal.note': 'Mientras un aviso se mantenga, estos dos textos no pueden estar en vigor a la vez. Su autor lo levanta; un cambio de fondo lo deja sin efecto.',
'signal.lift': 'Levantar',
'signal.lifted': 'levantado',
'signal.lifted.revision': 'sin efecto: el texto ha cambiado',
'signal.by': 'por {name}',
'propose.mode.replace': 'Modificar este texto',
'propose.mode.remove': 'Retirar este artículo',
'propose.mode.new': 'Artículo nuevo',
'propose.after': 'Colocar después de',
'propose.after.end': 'al final',
'propose.title': 'Título',
'propose.body': 'Texto',
'propose.remove.note': 'La propuesta de retirada se vota como cualquier otra; si se aprueba, el artículo sale del texto en vigor.',
'propose.draft': 'Guardar el borrador',
'propose.submit': 'Someter a votación',
'propose.drafted': 'Borrador guardado.',
'propose.submitted': 'Propuesta sometida a votación.',
'propose.rule': 'Mientras sea un borrador, lo modificas libremente. Una vez sometido, una errata se corrige sin tocar los votos; un cambio de fondo pide a los votantes que reconfirmen.',
'propose.edit': 'Modificar',
'propose.withdraw': 'Retirar mi propuesta',
'propose.withdrawn': 'Propuesta retirada.',
'propose.saved.typo': 'Corrección guardada, los votos se mantienen.',
'propose.saved.substantive': 'Nueva versión guardada: los votantes tendrán que reconfirmar.',
'assist.title': 'Asistente',
'assist.quiet': 'Todo en orden: una propuesta, una decisión.',
'assist.split': 'Este texto parece contener {n} decisiones. Una propuesta = una decisión: ¿dividirlo?',
'assist.split.do': 'Dividir en {n} borradores',
'assist.split.done': 'Dividido en {n} borradores.',
'assist.similar': 'Parecido a «{title}»{where}.',
'assist.similar.inForce': ' (en vigor)',
'assist.similar.variant': 'Convertirlo en una variante de este artículo',
'assist.long': 'Texto largo ({n} caracteres): un artículo corto se vota mejor.',
'translate.title': 'Traducir ({lang})',
'translate.save': 'Guardar la traducción',
'translate.saved': 'Traducción guardada.',
'translate.note': 'Las traducciones no se votan. Pasan a «por revisar» cuando el texto francés cambia de fondo.',
'feedback.title': 'Dejar un comentario',
'feedback.prompt': 'Lo que hiciste, lo que esperabas, lo que pasó.',
'feedback.send': 'Enviar el comentario',
'feedback.sent': 'Comentario enviado. Gracias.',
'feedback.context': 'Se adjuntan la dirección de la página, el idioma y el tamaño de la pantalla.',
'access.title': 'trustinWallet — accesos',
'access.intro': 'Quién abre qué grupo. Monnaie libre se abre por sí solo a los miembros de la red de confianza Ğ1; los demás grupos se abren uno a uno.',
'access.groups': 'Grupos',
'access.members.none': 'Ningún miembro tiene acceso.',
'access.members.wot': '+ todos los miembros de la red de confianza Ğ1',
'access.grant': 'Conceder',
'access.revoke': 'Retirar',
'access.choose': 'Elegir una identidad',
'access.invitations': 'Invitaciones',
'access.invite.label': 'Para quién',
'access.invite.groups': 'Grupos que abre el enlace',
'access.invite.admin': 'administrador de accesos',
'access.invite.create': 'Crear el enlace de invitación',
'access.invite.link': 'Enlace de invitación, de un solo uso, válido 14 días. No volverá a mostrarse: cópialo ahora.',
'access.invite.copy': 'Copiar el enlace',
'access.invite.copied': 'Enlace copiado.',
'access.invite.cancel': 'Cancelar',
'access.invite.pending': 'Pendiente',
'access.identities': 'Identidades',
'access.admin': 'administrador',
'access.admin.give': 'Nombrar administrador',
'access.admin.take': 'Retirar el rol',
'access.kind.g1': 'Ğ1',
'access.kind.invite': 'invitación',
'access.kind.dev': 'dev',
'access.member': 'miembro de la red de confianza',
'access.feedback': 'Comentarios',
'access.feedback.none': 'Ningún comentario.',
'access.feedback.seen': 'Visto',
'access.feedback.done': 'Atendido',
'access.denied': 'La gestión de accesos está reservada a los administradores de trustinWallet.',
'soon.title': 'Próximamente',
'soon.body': 'Esta herramienta llegará más adelante. Las herramientas se abren una a una: primero, los documentos de referencia.',
'error.unauthenticated': 'Inicia sesión para continuar.',
'error.forbidden': 'No tienes acceso a esta página.',
'error.not_found': 'Esta página no existe, o no es visible para ti.',
'error.network': 'El servidor no responde. Comprueba tu conexión e inténtalo de nuevo.',
'error.internal': 'Algo ha fallado por nuestra parte. Inténtalo de nuevo en un momento.',
'error.title': 'El título está vacío o es demasiado largo (120 caracteres como máximo).',
'error.body': 'El texto está vacío o es demasiado largo (4000 caracteres como máximo).',
'error.reason': 'Di en pocas palabras en qué se contradicen estos textos (de 10 a 600 caracteres).',
'error.substantive_not_author': 'Este cambio afecta al sentido: solo el autor de la propuesta puede hacerlo. Propón tu propia versión.',
'error.substantive_in_force': 'Este cambio afecta al sentido: proponlo como modificación.',
'error.same_unit': 'Estos dos textos se refieren al mismo artículo: de todos modos no pueden estar en vigor a la vez.',
'error.last_admin': 'Debe quedar al menos un administrador.',
'error.g1_not_configured': 'La conexión Ğ1 aún no está activa.',
'error.invitation_invalid': 'Este enlace de invitación ya no es válido.',
'error.retry': 'Reintentar',
'common.cancel': 'Cancelar',
'common.close': 'Cerrar',
'common.save': 'Guardar',
'common.loading': 'Cargando…',
'common.back': 'Volver',
};
+321
View File
@@ -0,0 +1,321 @@
// Source unique des libellés de l'interface — en français d'abord.
// Ton : on tutoie la personne ; le sujet est la personne ou le collectif,
// jamais la formule ni le système. Les clés en/es doivent être identiques
// (vérifié à la compilation). Termes interdits : tests/unit/lexicon.test.ts.
export const fr = {
'app.name': 'libreDecision',
'app.skip': 'Aller au contenu',
'nav.label': 'Boîte à outils',
'nav.home': 'Accueil',
'nav.decisions': 'Décisions',
'nav.docs': 'Docs de référence',
'nav.mandates': 'Mandats',
'nav.protocols': 'Protocoles',
'nav.soon': 'à venir',
'nav.groups': 'Mes groupes',
'nav.access': 'Accès',
'nav.access.hint': 'trustinWallet',
'nav.feedback': 'Faire un retour',
'nav.signin': 'Se connecter',
'nav.signout': 'Se déconnecter',
'nav.mood.toNight': 'Ambiance nuit',
'nav.mood.toStone': 'Ambiance pierre',
'nav.lang': 'Langue',
'nav.menu': 'Menu',
'nav.close': 'Fermer',
'home.title': 'Rendre possible la décision collective',
'home.subtitle': 'en fonction du contexte, processus et modalités, boîte à outils',
'home.decisions': "Un parcours assisté pour choisir les bonnes modalités de décision, et tenir le journal des décisions.",
'home.docs': "Acte d'engagement, licence, charte, constitution : des textes vivants, sous vote permanent.",
'home.mandates': 'Créer et révoquer tout type de mandat.',
'home.protocols': "Des workflows pour s'organiser sans organe statutaire : rôles tournants et automatisations techniques.",
'home.open': 'ouvert',
'home.soon': 'à venir',
'home.signin': 'Se connecter avec sa clé Ğ1',
'home.signin.note': 'Le même accès que le forum Monnaie libre : tu signes dans Ğecko, sans mot de passe.',
'home.continue': 'Ouvrir mes groupes',
'signin.title': 'Se connecter',
'signin.g1': 'Se connecter avec Ğ1',
'signin.g1.how': "Tu es envoyé sur connect.monnaie-libre.fr, le service de connexion du forum Monnaie libre. Tu y approuves la connexion dans Ğecko (lien ou QR code), puis tu reviens ici. Aucun mot de passe, aucune clé ne nous est transmise.",
'signin.g1.members': 'Réservé aux membres de la toile de confiance Ğ1.',
'signin.g1.pending': "La connexion Ğ1 arrive : l'inscription de libreDecision auprès de connect.monnaie-libre.fr est en cours. En attendant, on entre avec un lien d'invitation.",
'signin.invite': "Tu as reçu un lien d'invitation ? Ouvre-le simplement : il te connecte.",
'signin.dev': 'Connexion de développement',
'signin.dev.name': 'Nom',
'signin.dev.admin': 'administrateur des accès',
'signin.dev.go': 'Entrer',
'signin.error.refus': 'La connexion a été refusée sur connect.monnaie-libre.fr.',
'signin.error.etat': 'La connexion a expiré ou vient d’un autre navigateur. Recommence depuis ce navigateur.',
'signin.error.incomplet': 'Le retour de connect.monnaie-libre.fr était incomplet. Recommence.',
'signin.error.echange': 'connect.monnaie-libre.fr ne répond pas. Réessaie dans un moment.',
'signin.error.interne': 'La connexion a échoué de notre côté. Réessaie dans un moment.',
'invite.title': 'Invitation',
'invite.working': 'Connexion en cours…',
'invite.invalid': "Ce lien d'invitation n'est plus valable : il a déjà servi ou il a expiré. Demande un nouveau lien à la personne qui te l'a envoyé.",
'invite.missing': "Ce lien est incomplet. Ouvre le lien d'invitation entier, tel que tu l'as reçu.",
'groups.title': 'Mes groupes',
'groups.same.key': 'Groupes qui utilisent la même clé',
'groups.locked': 'Réservé à ses membres',
'groups.locked.long': "Ce groupe s'ouvre à ses membres. L'accès se demande à un administrateur de trustinWallet.",
'groups.test': 'Pour les essais',
'groups.open': 'Ouvrir',
'groups.empty': "Aucun groupe n'est visible pour l'instant.",
'group.tools': 'Outils',
'group.tool.soon': 'Cet outil arrive plus tard.',
'group.docs.empty': "Aucun document de référence dans ce groupe pour l'instant.",
'docs.title': 'Docs de référence',
'docs.inForce.one': '{n} article en vigueur',
'docs.inForce.other': '{n} articles en vigueur',
'docs.open.one': '{n} proposition en cours',
'docs.open.other': '{n} propositions en cours',
'doc.view.label': 'Lire',
'doc.view.inforce': 'En vigueur',
'doc.view.wellrated': 'Si les bien cotées passent',
'doc.view.all': 'Si tout passe',
'doc.view.wellrated.note': "Aperçu : le texte tel qu'il serait si les propositions bien cotées entraient en vigueur. Rien n'est adopté.",
'doc.view.all.note': "Aperçu : le texte tel qu'il serait si toutes les propositions entraient en vigueur, même peu soutenues. Rien n'est adopté.",
'doc.view.empty': 'Aucune proposition ne change le texte dans cet aperçu.',
'doc.mark.added': 'ajouté',
'doc.mark.replaced': 'modifié',
'doc.mark.removed': 'retiré',
'doc.lang.label': 'Langue du texte',
'doc.lang.translation': 'Traduction éditoriale, sans vote. Le texte qui fait foi est en français.',
'doc.lang.original': "Lire l'original",
'doc.lang.stale': 'traduction à revoir',
'doc.lang.missing': 'pas encore traduit',
'doc.seed': 'seed a minima',
'doc.seed.long': "Entré en vigueur sans vote, pour ne pas partir d'une page blanche. Il reste sous vote : 0 vote pour l'instant.",
'doc.confirmed': 'confirmé par le vote',
'doc.adopted': 'adopté le {date}',
'doc.action.vote': 'Le vote',
'doc.action.proposals': 'Propositions',
'doc.action.propose': 'Proposer une modification',
'doc.empty': "Aucun article n'est en vigueur. Les propositions ci-dessous remontent dès qu'elles atteignent leur seuil.",
'doc.published': 'Publié sur le dépôt des textes',
'doc.publish.pending': 'Publication en attente',
'deep.title': 'Propositions en cours',
'deep.note': "Pas encore en vigueur. Chacune remonte dès qu'elle atteint son seuil : au moins {floor} voix pour, puis la majorité que demande son inertie.",
'deep.new': 'Proposer un nouvel article',
'deep.drafts': 'Tes brouillons',
'deep.drafts.note': 'Non soumis au vote. Toi seul les vois.',
'deep.rival': 'remplacerait « {title} »',
'deep.addition': 'nouvel article',
'deep.removal': 'retrait de « {title} »',
'deep.inertia': 'inertie : {level}',
'deep.blocked': 'bloquée',
'deep.mine': 'ta proposition',
'deep.stale': 'à reconfirmer',
'deep.wellrated': 'bien cotée',
'deep.filter.all': 'Toutes',
'deep.filter.wellrated': 'Bien cotées',
'deep.filter.mine': 'Les miennes',
'deep.none': 'Aucune proposition dans ce filtre.',
'deep.variant': 'variante {label}',
'section.introduction': 'Introduction',
'section.fondamental': 'Engagements fondamentaux',
'section.technique': 'Engagements techniques',
'section.conclusion': 'Conclusion',
'section.annexe': 'Annexes',
'section.formule': 'Formule de vote',
'section.inertie': 'Inertie',
'section.ordonnancement': 'Ordonnancement',
'tally.short': '{for} pour · {against} contre',
'tally.required': 'Seuil : {n} voix pour',
'tally.missing.one': 'Il manque {n} voix pour.',
'tally.missing.other': 'Il manque {n} voix pour.',
'tally.reached': 'Seuil atteint.',
'tally.level': 'Niveau : {pct} % du seuil',
'inertia.low': 'inertie faible',
'inertia.standard': 'inertie standard',
'inertia.high': 'inertie haute',
'inertia.max': 'inertie maximale',
'inertia.level.low': 'faible',
'inertia.level.standard': 'standard',
'inertia.level.high': 'haute',
'inertia.level.max': 'maximale',
'panel.vote': 'Le vote',
'panel.proposals': 'Propositions',
'panel.propose': 'Proposer',
'panel.close': 'Fermer le panneau',
'panel.inforce': 'En vigueur',
'panel.proposal': 'Proposition',
'panel.by': 'proposé par {name}',
'panel.seed.origin': 'texte issu du seed',
'panel.changes': 'Voir les changements',
'panel.text': 'Voir le texte',
'vote.yours': 'Ton vote',
'vote.for': 'Pour',
'vote.against': 'Contre',
'vote.retract': 'Retirer mon vote',
'vote.saved': 'Vote enregistré.',
'vote.retracted': 'Vote retiré.',
'vote.stale': "Le texte a changé de fond depuis ton vote : ton vote ne compte plus tant que tu ne l'as pas reconfirmé.",
'vote.reconfirm': 'Reconfirmer : {choice}',
'vote.private': 'Ton vote ne se montre à personne : seuls les totaux sont visibles.',
'vote.blocked': "Un signalement de contradiction la retient : elle ne peut pas entrer en vigueur tant qu'il tient.",
'vote.entered': '« {title} » est entré en vigueur.',
'vote.entered.remove': '« {title} » est retiré du texte en vigueur.',
'vote.entered.inertia': "L'inertie de « {title} » a changé.",
'vote.closed': 'Cette version ne se vote plus.',
'inertia.title': 'Inertie',
'inertia.explain': "Plus l'inertie est haute, plus il faut de voix pour changer ce texte quand peu de gens votent.",
'inertia.change': "Proposer un changement d'inertie",
'inertia.change.to': 'Nouvelle inertie',
'inertia.change.note': "Un changement d'inertie se vote lui-même avec l'inertie au-dessus : {level}.",
'inertia.change.submit': 'Soumettre au vote',
'inertia.change.open': "Changement d'inertie en cours : vers {level}",
'formula.title': 'La formule du seuil',
'formula.intro': 'Le nombre de voix pour exigé, R, dépend de la participation :',
'formula.W': "W — électorat : {n} ({source})",
'formula.W.wot': 'membres de la toile de confiance Ğ1',
'formula.W.group': 'membres du groupe',
'formula.T': 'T — voix exprimées : {n}',
'formula.M': 'M — majorité à pleine participation : {n} %',
'formula.G': 'G — gradient : {n}',
'formula.B': 'B — base : {n}',
'formula.C': 'C — minimum de voix pour : {n}',
'formula.R': 'R — seuil actuel : {n} voix pour',
'formula.reading': "À faible participation, il faut presque l'unanimité ; quand tout le monde vote, la majorité M suffit. Rien ne passe sous C voix pour.",
'history.title': 'Corrections',
'history.initial': 'texte initial',
'history.typo': 'coquille corrigée',
'history.substantive': 'changement de fond (votes à reconfirmer)',
'history.by': 'par {name}',
'typo.title': 'Corriger une coquille',
'typo.note': "Accents, ponctuation, faute de frappe : la correction s'applique sans vote, avec son auteur et son diff. Un changement de sens passe par une proposition.",
'typo.save': 'Enregistrer la correction',
'typo.done': 'Coquille corrigée, sans toucher aux votes.',
'typo.substantive': 'Ce changement touche au sens : propose-le comme modification.',
'typo.asProposal': 'En faire une proposition',
'signal.title': 'Contradictions',
'signal.none': 'Aucun signalement.',
'signal.add': 'Signaler une contradiction',
'signal.target': 'Contredit',
'signal.reason': 'En quoi ces deux textes se contredisent',
'signal.send': 'Signaler',
'signal.sent': 'Signalement enregistré.',
'signal.note': "Tant qu'un signalement tient, ces deux textes ne peuvent pas être en vigueur ensemble. Son auteur le lève ; un changement de fond le fait tomber.",
'signal.lift': 'Lever',
'signal.lifted': 'levé',
'signal.lifted.revision': 'tombé : le texte a changé',
'signal.by': 'par {name}',
'propose.mode.replace': 'Modifier ce texte',
'propose.mode.remove': 'Retirer cet article',
'propose.mode.new': 'Nouvel article',
'propose.after': 'Placer après',
'propose.after.end': 'à la fin',
'propose.title': 'Titre',
'propose.body': 'Texte',
'propose.remove.note': 'La proposition de retrait se vote comme une autre ; si elle passe, l’article sort du texte en vigueur.',
'propose.draft': 'Enregistrer le brouillon',
'propose.submit': 'Soumettre au vote',
'propose.drafted': 'Brouillon enregistré.',
'propose.submitted': 'Proposition soumise au vote.',
'propose.rule': "Tant que c'est un brouillon, tu le modifies librement. Une fois soumis, une coquille se corrige sans toucher aux votes ; un changement de fond demande aux votants de reconfirmer.",
'propose.edit': 'Modifier',
'propose.withdraw': 'Retirer ma proposition',
'propose.withdrawn': 'Proposition retirée.',
'propose.saved.typo': 'Correction enregistrée, les votes restent.',
'propose.saved.substantive': 'Nouvelle version enregistrée : les votants devront reconfirmer.',
'assist.title': 'Assistant',
'assist.quiet': 'Rien à signaler : une proposition, une décision.',
'assist.split': 'Ce texte semble porter {n} décisions. Une proposition = une décision : les découper ?',
'assist.split.do': 'Découper en {n} brouillons',
'assist.split.done': 'Découpé en {n} brouillons.',
'assist.similar': 'Proche de « {title} »{where}.',
'assist.similar.inForce': ' (en vigueur)',
'assist.similar.variant': 'En faire une variante de cet article',
'assist.long': 'Texte long ({n} signes) : un article court se vote mieux.',
'translate.title': 'Traduire ({lang})',
'translate.save': 'Enregistrer la traduction',
'translate.saved': 'Traduction enregistrée.',
'translate.note': 'Les traductions ne se votent pas. Elles passent « à revoir » quand le texte français change de fond.',
'feedback.title': 'Faire un retour',
'feedback.prompt': "Ce que tu as fait, ce que tu attendais, ce qui s'est passé.",
'feedback.send': 'Envoyer le retour',
'feedback.sent': 'Retour envoyé. Merci.',
'feedback.context': "L'adresse de la page, la langue et la taille de l'écran sont jointes.",
'access.title': 'trustinWallet — accès',
'access.intro': "Qui ouvre quel groupe. Monnaie libre s'ouvre de lui-même aux membres de la toile de confiance Ğ1 ; les autres groupes s'ouvrent un par un.",
'access.groups': 'Groupes',
'access.members.none': 'Aucun membre accordé.',
'access.members.wot': '+ tous les membres de la toile Ğ1',
'access.grant': 'Accorder',
'access.revoke': 'Retirer',
'access.choose': 'Choisir une identité',
'access.invitations': 'Invitations',
'access.invite.label': 'Pour qui',
'access.invite.groups': 'Groupes ouverts par le lien',
'access.invite.admin': 'administrateur des accès',
'access.invite.create': "Créer le lien d'invitation",
'access.invite.link': "Lien d'invitation, à usage unique, valable 14 jours. Il ne s'affichera plus : copie-le maintenant.",
'access.invite.copy': 'Copier le lien',
'access.invite.copied': 'Lien copié.',
'access.invite.cancel': 'Annuler',
'access.invite.pending': 'En attente',
'access.identities': 'Identités',
'access.admin': 'administrateur',
'access.admin.give': 'Rendre administrateur',
'access.admin.take': 'Retirer le rôle',
'access.kind.g1': 'Ğ1',
'access.kind.invite': 'invitation',
'access.kind.dev': 'dev',
'access.member': 'membre de la toile',
'access.feedback': 'Retours',
'access.feedback.none': 'Aucun retour.',
'access.feedback.seen': 'Vu',
'access.feedback.done': 'Traité',
'access.denied': 'La gestion des accès est réservée aux administrateurs de trustinWallet.',
'soon.title': 'À venir',
'soon.body': "Cet outil arrive plus tard. Les chantiers s'ouvrent un par un : d'abord les docs de référence.",
'error.unauthenticated': 'Connecte-toi pour continuer.',
'error.forbidden': "Tu n'as pas accès à cette page.",
'error.not_found': "Cette page n'existe pas, ou elle ne t'est pas visible.",
'error.network': 'Le serveur ne répond pas. Vérifie ta connexion et réessaie.',
'error.internal': 'Quelque chose a échoué de notre côté. Réessaie dans un moment.',
'error.title': 'Le titre est vide ou trop long (120 signes au plus).',
'error.body': 'Le texte est vide ou trop long (4 000 signes au plus).',
'error.reason': 'Dis en quelques mots en quoi ces textes se contredisent (10 à 600 signes).',
'error.substantive_not_author': "Ce changement touche au sens : seul l'auteur de la proposition peut le faire. Propose ta propre version.",
'error.substantive_in_force': 'Ce changement touche au sens : propose-le comme modification.',
'error.same_unit': "Ces deux textes visent le même article : ils ne peuvent de toute façon pas être en vigueur ensemble.",
'error.last_admin': 'Il faut garder au moins un administrateur.',
'error.g1_not_configured': "La connexion Ğ1 n'est pas encore active.",
'error.invitation_invalid': "Ce lien d'invitation n'est plus valable.",
'error.retry': 'Réessayer',
'common.cancel': 'Annuler',
'common.close': 'Fermer',
'common.save': 'Enregistrer',
'common.loading': 'Chargement…',
'common.back': 'Retour',
} as const;
export type Key = keyof typeof fr;
export type Messages = Record<Key, string>;
+60
View File
@@ -0,0 +1,60 @@
// Interface language: fr, en, es (Yvv, 2026-10-21). French is the source.
import { en } from './en';
import { es } from './es';
import { fr, type Key, type Messages } from './fr';
import type { Lang } from '../types';
export const LANGS: Lang[] = ['fr', 'en', 'es'];
const CATALOGS: Record<Lang, Messages> = { fr, en, es };
const LOCALES: Record<Lang, string> = { fr: 'fr-FR', en: 'en-GB', es: 'es-ES' };
function initial(): Lang {
try {
const saved = localStorage.getItem('ld:lang');
if (saved === 'fr' || saved === 'en' || saved === 'es') return saved;
} catch {
/* no storage */
}
const nav = (typeof navigator !== 'undefined' ? navigator.language : 'fr').slice(0, 2);
return nav === 'en' || nav === 'es' ? nav : 'fr';
}
class I18n {
lang = $state<Lang>('fr');
init() {
this.set(initial());
}
set(lang: Lang) {
this.lang = lang;
document.documentElement.lang = lang;
try {
localStorage.setItem('ld:lang', lang);
} catch {
/* no storage */
}
}
t = (key: Key, params: Record<string, string | number> = {}): string => {
const raw = CATALOGS[this.lang][key] ?? fr[key] ?? key;
return raw.replace(/\{(\w+)\}/g, (_, k: string) => (k in params ? String(params[k]) : `{${k}}`));
};
/** `<key>.one` / `<key>.other` — French and Spanish use « one » for 0 and 1. */
plural = (key: string, n: number, params: Record<string, string | number> = {}): string => {
const one = this.lang === 'en' ? n === 1 : n <= 1;
return this.t(`${key}.${one ? 'one' : 'other'}` as Key, { n, ...params });
};
date = (iso: string | null | undefined): string => {
if (!iso) return '';
return new Date(iso).toLocaleDateString(LOCALES[this.lang], { day: 'numeric', month: 'long', year: 'numeric' });
};
number = (n: number): string => n.toLocaleString(LOCALES[this.lang]);
}
export const i18n = new I18n();
export const t = i18n.t;
export type { Key };