diff --git a/crates/ld-server/src/docs.rs b/crates/ld-server/src/docs.rs index fc70a9e..88b9af7 100644 --- a/crates/ld-server/src/docs.rs +++ b/crates/ld-server/src/docs.rs @@ -815,6 +815,9 @@ fn proposal_json( "revision": p.revision, "createdAt": p.created_at, "updatedAt": p.updated_at, "submittedAt": p.submitted_at, "enteredAt": p.entered_at, "confirmedAt": p.confirmed_at, "seedMinimal": p.seed_minimal(), "seedProposal": p.seed_proposal(), + // Put forward by the seed (ADR 0011), in force since or not: the chapter « Info ouverture + // doc de réf. » (lot U3) says the level the Propositions d'amorce opened at. + "seedFeatured": p.seed_featured, "tally": st.shown_tally(&p.id), "tallyPending": st.pending_tally(&p.id), "tallySimulated": st.simulated_tally(&p.id), diff --git a/web/e2e/modules.spec.ts b/web/e2e/modules.spec.ts index 38696bd..ba05a40 100644 --- a/web/e2e/modules.spec.ts +++ b/web/e2e/modules.spec.ts @@ -200,7 +200,7 @@ test('C27 — la participation ramenée dans ses bornes : la mesure, puis ce que await expect(panel.getByText(/^3 votants sur 4 \(62,5\s%\) : la médiane des 12 propositions votées depuis un an\.$/)).toBeVisible(); // Then what the group asks, the group as subject. await expect(panel.getByText(/^Ramenée entre 5\s% et 50\s% de l'électorat : le groupe demande ses voix pour comme si 2 personnes votaient \(50\s%\)\.$/)).toBeVisible(); - await expect(panel.getByText("Part exigée : la part de « pour » demandée, à 2 votants comme d'habitude.")).toBeVisible(); + await expect(panel.getByText("Part exigée : celle que le cran demande au-delà du quorum de l'article, à la participation habituelle.")).toBeVisible(); await shot(page, 'c27-participation-bornee'); }); diff --git a/web/e2e/ordre-quorum.spec.ts b/web/e2e/ordre-quorum.spec.ts index 4a2047c..88b6caa 100644 --- a/web/e2e/ordre-quorum.spec.ts +++ b/web/e2e/ordre-quorum.spec.ts @@ -57,7 +57,7 @@ test('C35 — réordonner une section : les poignées sur ses articles, au gliss await shot(page, 'c35-2-poignees'); // By dragging its handle: Réciprocité onto the place of the article above it, Responsabilité - // civique (lot T4; a drag over two rows is not announced, see the report of lot T4 for U3). + // civique (lot T4). A drag over several rows is said and followed by the focus too (lot U3, C42). await list.locator('li', { hasText: E2 }).locator('.grip').dragTo(list.locator('li', { hasText: EC })); await expect(names).toHaveText([EF, E1, E2, EC]); await expect(page.getByText(`« ${E2} » en place 3 sur 4.`)).toBeAttached(); @@ -261,7 +261,7 @@ test('C38 — l’en-tête compte ce que montre la liste ; un changement d’ine await page.goto(DOC); await unfold(page); const facts = page.locator('.facts'); - await expect.soft(facts).toContainText('0 article en vigueur · 4 Propositions seed · '); + await expect.soft(facts).toContainText("0 article en vigueur · 4 Propositions d'amorce · "); await expect.soft(facts).toContainText(new RegExp(`· ${await listed()} propositions en cours`)); // A change of inertia on the order of a section and on the title (test group): each is found at diff --git a/web/e2e/reader.spec.ts b/web/e2e/reader.spec.ts index 8ba1678..e8b07cb 100644 --- a/web/e2e/reader.spec.ts +++ b/web/e2e/reader.spec.ts @@ -13,7 +13,10 @@ test('C1 — l’accueil sans session : la voûte abrite les trois points, le ti await expect(nav.locator('.tool', { hasText: item })).toBeVisible(); } await expect(nav.locator('.tool.off')).toHaveCount(3); - await expect(page.getByRole('link', { name: 'Se connecter avec sa clé Ğ1' })).toBeVisible(); + // The Ğ1 sign-in not active (as in production): Yvv's word for whoever is not invited yet, + // in place of the button (lot U3, C41). + await expect(page.getByText("Pour rentrer dans un groupe de décisions, il faut pour l'instant demander un lien d'invitation à Chiara ou à Yvv.", { exact: false })).toBeVisible(); + await expect(page.getByRole('link', { name: 'Se connecter avec sa clé Ğ1' })).toHaveCount(0); // The four pillars live on the page of a group, no longer here. await expect(page.locator('.pillars')).toHaveCount(0); await expect(page.getByText('Monnaie libre', { exact: true })).toHaveCount(0); @@ -37,17 +40,18 @@ test('C2 — l’accueil avec session : le choix du groupe, ceux de la même cl await expect(page).toHaveURL((u) => u.pathname === '/'); }); -test('C3 — au départ, aucun texte en vigueur : la raison d’être et les trois engagements, Propositions seed, ouvrent le texte', async ({ page }) => { +test('C3 — au départ, aucun texte en vigueur : la raison d’être et les trois engagements, Propositions d’amorce, ouvrent le texte', async ({ page }) => { await signIn(page, 'Chiara'); await page.goto(DOC); await expect(page.getByRole('heading', { level: 1 })).toHaveText("Acte d'engagement Certification"); // Nothing in force (Yvv, 2026-11-02): the page says where to begin, once — the seed proposals, // read whole at the head of the text (lot U), each marked: never the bare page of a text in force. - await expect(page.getByText("Aucun article n'est encore en vigueur. Commence par les Propositions seed : elles y entrent par le vote, comme toute proposition.")).toBeVisible(); + // Lot U3 (Yvv, 2026-11-03): said by « Info ouverture doc de réf. », in her words (C39). + await expect(page.getByRole('region', { name: 'Info ouverture doc de réf.' }).getByText("Aucun article n'est encore en vigueur. Les Propositions d'amorce y entrent par le vote")).toBeVisible(); const seeds = page.locator('section.text article.unit'); // Lot T4 (Yvv, 2026-11-03): « Responsabilité civique » between the two engagements. await expect(seeds.locator('h2')).toHaveText(["Raison d'être", 'Unicité du compte', 'Responsabilité civique', 'Réciprocité']); - await expect(seeds.locator('.tag', { hasText: 'Proposition seed' })).toHaveCount(4); + await expect(seeds.locator('.tag', { hasText: "Proposition d'amorce" })).toHaveCount(4); await expect(seeds.getByTitle("Texte de départ mis en avant pour lancer le vote. Il n'est pas en vigueur : il y entre par le vote, comme toute proposition.")).toHaveCount(4); // At the low level they start at, how far each is from passing. for (const seed of await seeds.all()) await expect(seed).toContainText('0 pour · 0 contre · 9 oui sur 12'); @@ -61,7 +65,7 @@ test('C3 — au départ, aucun texte en vigueur : la raison d’être et les tro await unfold(page); // One line per article: the 27 less the 4 at the head, E3 and E7 holding their two variants inside. await expect(page.locator('ul.deep button.row')).toHaveCount(23); - await expect(page.locator('ul.deep .tag', { hasText: 'Proposition seed' })).toHaveCount(0); + await expect(page.locator('ul.deep .tag', { hasText: "Proposition d'amorce" })).toHaveCount(0); }); test('C4 — les actions sont contextuelles, le vote se déplie sous l’article', async ({ page }) => { @@ -331,8 +335,9 @@ test('C20 — réglages de la formule et cinq crans', async ({ page }) => { // At 100 voters, above every quorum: each level's share of the 100. const required = panel.locator('table tbody td.required'); await expect(required).toHaveText(['60', '67', '75', '83', '90']); - await expect(panel.getByText("Voix pour : à 100 votants, comme d'habitude.")).toBeVisible(); - await expect(panel.getByText("Part exigée : la part de « pour » demandée, à 100 votants comme d'habitude.")).toBeVisible(); + // Lot U3: the defined terms of lot T3c, never « comme d'habitude ». + await expect(panel.getByText("Voix pour : à 100 votants, la participation habituelle arrondie, ou au quorum s'il est plus haut.")).toBeVisible(); + await expect(panel.getByText("Part exigée : celle que le cran demande au-delà du quorum de l'article, à la participation habituelle.")).toBeVisible(); await expect(panel.getByText(/^Quorum : en dessous, les votants qui manquent comptent comme « non »\./)).toBeVisible(); await page.keyboard.press('Escape'); @@ -349,7 +354,7 @@ test('C20 — réglages de la formule et cinq crans', async ({ page }) => { // No vote: each level on its quorum, the voters missing counted as « non ». await expect(quorums).toHaveText(['12', '12', '50', '50', '50']); await expect(required).toHaveText(['8', '9', '39', '43', '46']); - await expect(panel.getByText('Voix pour : à 0 votant, la participation de cette version.')).toBeVisible(); + await expect(panel.getByText("Voix pour : à 0 votant, la participation de cette version, ou au quorum s'il est plus haut.")).toBeVisible(); await page.keyboard.press('Escape'); // The five levels to choose from, in the test group (Réciprocité voted into force there, diff --git a/web/src/lib/components/Formula.svelte b/web/src/lib/components/Formula.svelte index 46b58aa..dc36fef 100644 --- a/web/src/lib/components/Formula.svelte +++ b/web/src/lib/components/Formula.svelte @@ -28,6 +28,9 @@ } = $props(); const p = $derived(levels.find((x) => x.level === inertia)); + /** Q — the quorum of the article it is judged on; T′ = max(T, Q) (ADR 0013). */ + const quorum = $derived(evaluation?.quorum ?? p?.quorum ?? floor); + const cast = $derived(tally.for + tally.against);
@@ -49,15 +52,19 @@ +

{t('formula.round')}

+ + {#if source === 'wot'}

{t('formula.B.wot')}

{/if}

{t('formula.reading')}

{#if onsettings} diff --git a/web/src/lib/components/FormulaSettings.svelte b/web/src/lib/components/FormulaSettings.svelte index fcbf32e..7481d3e 100644 --- a/web/src/lib/components/FormulaSettings.svelte +++ b/web/src/lib/components/FormulaSettings.svelte @@ -64,7 +64,7 @@ {/if}

{#if p.bounded}

{i18n.plural('settings.usual.bounded', p.voters, { lo: pct(p.bounds[0]), hi: pct(p.bounds[1]), pct: pct(p.ratio) })}

{/if} -

{p.measuredAt ? t('settings.usual.when', { date: i18n.date(p.measuredAt) }) : t('settings.usual.never')}

+

{p.measuredAt ? t('settings.usual.when', { date: i18n.date(p.measuredAt) }) : t('settings.usual.never')} {t('settings.usual.quorum')}

{t('settings.levels')}

@@ -100,10 +100,11 @@ -

{i18n.plural('settings.share.note', p.voters)}

+

{t('settings.share.note')}

{t('settings.quorum.note')}

{i18n.plural(version ? 'settings.required.version' : 'settings.required.usual', cast)}

{t('settings.reading')}

+

{t('settings.gradient')}

{/if} diff --git a/web/src/lib/components/OpeningInfo.svelte b/web/src/lib/components/OpeningInfo.svelte new file mode 100644 index 0000000..771e811 --- /dev/null +++ b/web/src/lib/components/OpeningInfo.svelte @@ -0,0 +1,129 @@ + + +
+

{t('opening.title')}

+ {#if amorce} + +

{#if inForce}{t('opening.amorce.after')}{:else}{t('opening.none')} {t('opening.amorce')}{/if}

+ {/if} +
+ {#if rows.length} +
+

{t('opening.inertia')}

+
    + {#each rows as [key, ks] (key)} +
  • {t(key)} {#each ks as k, i (k)}{#if i}{/if}{/each}
  • + {/each} +
+
+ {/if} + {#if quorums} +
+

{t('opening.quorum')}

+

+ {quorums.same !== null + ? i18n.plural('opening.quorum.same', quorums.same) + : t('opening.quorum.levels', { list: quorums.list.map((q) => i18n.number(q)).join(' · ') })} +

+

{t('opening.quorum.rule')} {t('opening.quorum.author')}

+
+ {/if} +
+
+ + diff --git a/web/src/lib/components/Reorder.svelte b/web/src/lib/components/Reorder.svelte index 784bb53..0a69359 100644 --- a/web/src/lib/components/Reorder.svelte +++ b/web/src/lib/components/Reorder.svelte @@ -111,11 +111,13 @@ } } - // Dragging by the handle: the article takes the place of the row under the pointer. + // Dragging by the handle: the article takes the place of the row under the pointer. The pointer + // is captured by the list, which stays put, rather than by the handle of a row the reorder moves + // (a guard: what lot U3 found is the measure of the rows, in `drag`). function grab(e: PointerEvent, id: string) { if (busy || e.button !== 0) return; e.preventDefault(); - (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId); + root?.setPointerCapture(e.pointerId); dragging = id; chosen = null; error = ''; @@ -124,9 +126,12 @@ function drag(e: PointerEvent) { const id = dragging; if (!id || !root) return; + // The rows where the layout puts them, not where their animation shows them (`animate:flip` + // moves them by a transform): a row sliding under the pointer would send the article back. + const y = e.clientY - root.getBoundingClientRect().top; for (const row of root.querySelectorAll('li[data-id]')) { - const box = row.getBoundingClientRect(); - if (e.clientY < box.top || e.clientY >= box.bottom) continue; + const top = row.offsetTop - root.offsetTop; + if (y < top || y >= top + row.offsetHeight) continue; const target = row.dataset.id; if (target && target !== id) list = place(list, id, target); return; @@ -164,11 +169,20 @@

{t('order.edit.help')}

-
    +
      {#each list as id, i (id)} {@const name = nameOf(id)}
    1. -

    {said}

    -

    {t('order.edit.note', { level: inertiaLevel(unit.inertia) })} {t('module.order.rule')}

    +

    {t('order.edit.note', { level: inertiaLevel(unit.inertia) })} {t('order.edit.effect')} {t('module.order.rule')}

    {#if existing && !unchanged}

    {t('order.edit.exists')}

    {/if} {#if error}{/if}
    diff --git a/web/src/lib/components/VoteFold.svelte b/web/src/lib/components/VoteFold.svelte index d8fab1a..d4733f0 100644 --- a/web/src/lib/components/VoteFold.svelte +++ b/web/src/lib/components/VoteFold.svelte @@ -93,6 +93,14 @@ (x): x is Proposal => !!x && x.id !== p.id && !besides.includes(x.id), ), ); + /** + * A version beside a text in force (lot U3, from F1.2): it must also beat the margin of that text, + * which comes back among the proposals if replaced (ADR 0009 for a module). Not a change of + * inertia, voted apart; not a removal, which has its own note. + */ + const rival = $derived( + !!unit.inForce && unit.inForce.id !== p.id && p.status === 'open' && (p.kind === 'text' || p.kind === 'title' || p.kind === 'order'), + ); /** The inertia is said by « Inertie », below; a change of inertia has none: its level is said here. */ const ownInertia = $derived(p.kind === 'inertia'); @@ -177,6 +185,7 @@ {#if ownInertia}

    {/if}
+ {#if rival}

{t('vote.rival')}

{/if} {#if p.blocked}

{t('vote.held')}

@@ -254,6 +263,8 @@ {:else if access.simulated}

{t('vote.simulated.note')}

{/if} + + {#if !p.blocked}

{t('vote.permanent')}

{/if} {/if}

{t('vote.private')}

{#if error}{/if} @@ -284,6 +295,7 @@ {/each} +

{t('vote.others.rule')}

{/if} {#if !ownInertia}{/if} diff --git a/web/src/lib/i18n/en.ts b/web/src/lib/i18n/en.ts index 080fe1e..67830eb 100644 --- a/web/src/lib/i18n/en.ts +++ b/web/src/lib/i18n/en.ts @@ -42,8 +42,9 @@ export const en: Messages = { '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.g1.pending': "Ğ1 sign-in is on its way: libreDecision's registration with connect.monnaie-libre.fr is in progress.", 'signin.invite': 'Got an invitation link? Just open it: it signs you in.', + 'signin.uninvited': 'To join a decision group, for now ask Chiara or Yvv for an invitation link. This prototype is not yet wired to the chain.', 'signin.dev': 'Development sign-in', 'signin.dev.name': 'Name', 'signin.dev.admin': 'access administrator', @@ -103,8 +104,8 @@ export const en: Messages = { 'docs.inForce.other': '{n} articles in force', 'docs.open.one': '{n} open proposal', 'docs.open.other': '{n} open proposals', - 'docs.seeds.one': '{n} seed proposal', - 'docs.seeds.other': '{n} seed proposals', + 'docs.seeds.one': '{n} starter proposal', + 'docs.seeds.other': '{n} starter proposals', 'doc.view.label': 'Read', 'doc.view.inforce': 'In force', @@ -125,7 +126,7 @@ export const en: Messages = { 'doc.seed.long': 'Came into force without a vote, so as not to start from a blank page. It remains open for voting, like any text.', 'doc.seed.votes.one': 'seed a minima · {n} vote', 'doc.seed.votes.other': 'seed a minima · {n} votes', - 'doc.seedProposal': 'Seed proposal', + 'doc.seedProposal': 'Starter proposal', 'doc.seedProposal.long': 'A starting text put forward to start the vote. It is not in force: it comes into force through the vote, like any proposal.', 'doc.confirmed': 'confirmed by vote', 'doc.adopted': 'adopted on {date}', @@ -134,7 +135,21 @@ export const en: Messages = { 'doc.basket': 'Vote in bulk', 'doc.basket.count': 'Vote in bulk · {n}', 'doc.empty': 'No article is in force. The proposals below move up as soon as they reach their threshold.', - 'doc.empty.seeds': 'No article is in force yet. Start with the seed proposals: they come into force by the vote, like any proposal.', + 'opening.title': 'Opening info, reference doc', + 'opening.none': 'No article is in force yet.', + 'opening.amorce': 'Starter proposals come into force through the vote, like the proposals that follow, with less inertia so that the document comes to life more easily. It is recommended to raise the inertia afterwards, through the vote, to make changes less frequent.', + 'opening.amorce.after': 'Starter proposals come into force through the vote, like the proposals that follow, with less inertia so that the document comes to life more easily. It is recommended to raise the inertia afterwards, through the vote, to make changes less frequent.', + 'opening.inertia': 'Inertia at the opening', + 'opening.inertia.amorce': 'Starter proposals', + 'opening.inertia.title': 'Title of the document', + 'opening.inertia.orders': 'Order of each section', + 'opening.inertia.others': 'Other articles', + 'opening.quorum': 'Quorum', + 'opening.quorum.levels': '{list} voters, from level 1 to level 5.', + 'opening.quorum.same.one': '{n} voter at each level.', + 'opening.quorum.same.other': '{n} voters at each level.', + 'opening.quorum.rule': "Below an article's quorum, the missing voters count as against.", + 'opening.quorum.author': 'The author of the first version of an article, an administrator for the starting texts, can set another one as long as the article keeps no trace of any vote.', 'doc.others.one': '{n} other version open', 'doc.others.other': '{n} other versions open', 'doc.published': 'Published to the text repository', @@ -202,6 +217,7 @@ export const en: Messages = { 'order.proposed.one': '{n} order proposed', 'order.proposed.other': '{n} orders proposed', 'order.edit.note': 'An order is voted at inertia {level}, like an article: unfolded, in the basket, on the Ğ1 chain in Monnaie libre.', + 'order.edit.effect': 'If it comes into force, only the articles of this section change place; no text changes.', 'order.submitted': 'Order submitted to vote.', 'title.propose': 'Propose another title', 'title.propose.label': 'New title', @@ -246,6 +262,7 @@ export const en: Messages = { 'vote.simulated.mark': 'simulated vote', 'vote.others': 'Other versions', 'vote.others.count': 'Other versions ({n})', + 'vote.others.rule': 'If several versions reach their threshold, the one with the widest margin comes into force.', 'vote.need': '{yes} for to pass · {no} against to block', 'vote.need.quorum': '{yes} for to pass · the quorum blocks it', 'vote.need.first.one': '{yes} for out of {quorum} voter to pass', @@ -259,6 +276,8 @@ export const en: Messages = { '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.permanent': 'The vote stays open, with no deadline: you can change or withdraw your vote at any time; the last one is the one that counts.', + 'vote.rival': 'A text is already in force: to replace it, this version must also reach a wider margin (votes in favour minus votes required) than that text has. The replaced text becomes a proposal again, with its votes: it can come back.', 'vote.held': 'An objection is holding this version back: until it is lifted, nobody can vote for it any more. You can vote against it or take your vote back; once lifted, it enters into force only with the votes still there.', 'vote.held.redit': 'It repeats “{other}”.', 'vote.held.contredit': 'It contradicts “{other}”.', @@ -299,7 +318,7 @@ export const en: Messages = { 'quorum.heading.other': 'Quorum · {n} voters', 'quorum.default.one': 'By default, that of its level: {n} voter.', 'quorum.default.other': 'By default, that of its level: {n} voters.', - 'quorum.explain': 'You set the quorum of this article as long as nobody has voted. Below it, the missing voters count as against.', + 'quorum.explain': 'You set the quorum of this article as long as it keeps no trace of any vote. Below it, the missing voters count as against.', 'quorum.field': 'Quorum of the article', 'quorum.set': 'Set this quorum', 'quorum.reset': 'Back to the default ({n})', @@ -404,7 +423,7 @@ export const en: Messages = { 'ballots.verify.unreadable': 'This is not a readable vote receipt.', 'inertia.title': 'Inertia', - 'inertia.explain': 'The higher the inertia, the more votes it takes to change this text. Below the quorum, the missing voters count as against; beyond it, the share of votes in favour asked for falls as turnout rises.', + 'inertia.explain': "The higher the inertia, the more votes it takes to change this text. Below the article's quorum, the missing voters count as against; beyond it, the share asked falls as turnout rises.", 'inertia.change': 'Propose an inertia change', 'inertia.change.to': 'New inertia', 'inertia.change.note': "A change of inertia is itself voted at the article's inertia: {level}.", @@ -420,38 +439,42 @@ export const en: Messages = { 'formula.W.wot': 'members of the Ğ1 web of trust', 'formula.W.group': 'group members', 'formula.T': 'T — votes cast: {n}', + 'formula.Tprime': 'T′ — voters counted: {n}, T or Q if some are missing', 'formula.M': 'M — majority at full turnout: {n}%', - 'formula.G': 'G — gradient: {n}', + 'formula.G': "G — gradient: {n}, derived from the group's usual turnout", 'formula.B': 'B — base: {n}', + 'formula.B.wot': 'Raised to the power W, B is so small that it does not count in the web of trust.', 'formula.Q': 'Q — quorum: {n}', 'formula.R': 'R — current threshold: {n} votes in favour', - 'formula.reading': 'Below the quorum Q, the missing voters count as against. Beyond it, the share of votes in favour asked for, highest at the quorum, falls as turnout rises; when everyone votes, majority M is enough.', + 'formula.round': 'A version reaches its threshold when its votes in favour reach R, rounded up to a whole number.', + 'formula.reading': "Below the article's quorum Q, the missing voters count as against: it takes as many votes in favour as if Q people had voted. Beyond it, the share of votes in favour asked for, highest at the quorum, falls as turnout rises; when everyone votes, majority M is enough.", 'common.pct': '{n}%', 'settings.title': 'Formula settings', 'settings.intro': 'To change a text, the group asks for a number of votes in favour that depends on its electorate, its usual turnout and the inertia of the article.', 'settings.usual': 'Usual turnout', 'settings.usual.median.one': '{n} voter out of {electorate} ({pct}): the median of the {sample} proposals voted on over the past year.', 'settings.usual.median.other': '{n} voters out of {electorate} ({pct}): the median of the {sample} proposals voted on over the past year.', - 'settings.usual.reference.one': '{n} voter out of {electorate} ({pct}), twice {q}, the reference quorum: the reference turnout, until the group has voted on {min} proposals ({sample} so far).', - 'settings.usual.reference.other': '{n} voters out of {electorate} ({pct}), twice {q}, the reference quorum: the reference turnout, until the group has voted on {min} proposals ({sample} so far).', + 'settings.usual.reference.one': '{n} voter out of {electorate} ({pct}), twice {q}, the reference quorum: the reference turnout, while it rests on fewer than {min} proposals voted on over the past year ({sample} so far).', + 'settings.usual.reference.other': '{n} voters out of {electorate} ({pct}), twice {q}, the reference quorum: the reference turnout, while it rests on fewer than {min} proposals voted on over the past year ({sample} so far).', 'settings.usual.bounded.one': 'Kept between {lo} and {hi} of the electorate: the group asks for its votes in favour as if {n} person voted ({pct}).', 'settings.usual.bounded.other': 'Kept between {lo} and {hi} of the electorate: the group asks for its votes in favour as if {n} people voted ({pct}).', 'settings.usual.when': 'Measured once a day, last on {date}.', 'settings.usual.never': 'Measured once a day.', + 'settings.usual.quorum': 'It does not depend on the quorum of an article.', 'settings.levels': 'The five levels', 'settings.col.level': 'Level', 'settings.col.share': 'Share asked', 'settings.col.quorum': 'Quorum', 'settings.col.required': 'Votes for', 'settings.current': 'this article', - 'settings.share.note.one': 'Share asked: the share of votes in favour asked, at {n} voter as usual.', - 'settings.share.note.other': 'Share asked: the share of votes in favour asked, at {n} voters as usual.', + 'settings.share.note': "Share asked: the one the level asks for beyond the article's quorum, at the usual turnout.", 'settings.quorum.note': "Quorum: below it, the missing voters count as against. It is the level's; the author of an article may set another one before the first vote.", - 'settings.required.version.one': "Votes for: at {n} voter, this version's turnout.", - 'settings.required.version.other': "Votes for: at {n} voters, this version's turnout.", - 'settings.required.usual.one': 'Votes for: at {n} voter, as usual.', - 'settings.required.usual.other': 'Votes for: at {n} voters, as usual.', - 'settings.reading': 'At each level, the group asks for its share of votes in favour when it votes as usual; beyond the quorum, the more voters, the closer it comes to M. Below the quorum of the level, the missing voters count as against.', + 'settings.required.version.one': "Votes for: at {n} voter, this version's turnout, or at the quorum if it is higher.", + 'settings.required.version.other': "Votes for: at {n} voters, this version's turnout, or at the quorum if it is higher.", + 'settings.required.usual.one': 'Votes for: at {n} voter, the usual turnout rounded, or at the quorum if it is higher.', + 'settings.required.usual.other': 'Votes for: at {n} voters, the usual turnout rounded, or at the quorum if it is higher.', + 'settings.reading': "Beyond the article's quorum, the more voters, the lower the share asked gets, down to the majority M when everyone votes. Below the article's quorum, the missing voters count as against, and it takes as many votes in favour as if that quorum had voted.", + 'settings.gradient': "G is computed so that the share asked, beyond the article's quorum, is exactly that of the level at the usual turnout.", 'history.title': 'Corrections', 'history.initial': 'initial text', diff --git a/web/src/lib/i18n/es.ts b/web/src/lib/i18n/es.ts index 8f45aa4..5eb4340 100644 --- a/web/src/lib/i18n/es.ts +++ b/web/src/lib/i18n/es.ts @@ -42,8 +42,9 @@ export const es: Messages = { '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.g1.pending': 'La conexión Ğ1 llega pronto: el registro de libreDecision en connect.monnaie-libre.fr está en curso.', 'signin.invite': '¿Has recibido un enlace de invitación? Ábrelo sin más: te conecta.', + 'signin.uninvited': 'Para entrar en un grupo de decisiones, por ahora hay que pedir un enlace de invitación a Chiara o a Yvv. Este prototipo aún no está conectado a la cadena.', 'signin.dev': 'Conexión de desarrollo', 'signin.dev.name': 'Nombre', 'signin.dev.admin': 'administrador de accesos', @@ -103,8 +104,8 @@ export const es: Messages = { 'docs.inForce.other': '{n} artículos en vigor', 'docs.open.one': '{n} propuesta en curso', 'docs.open.other': '{n} propuestas en curso', - 'docs.seeds.one': '{n} propuesta semilla', - 'docs.seeds.other': '{n} propuestas semilla', + 'docs.seeds.one': '{n} propuesta de arranque', + 'docs.seeds.other': '{n} propuestas de arranque', 'doc.view.label': 'Leer', 'doc.view.inforce': 'En vigor', @@ -125,7 +126,7 @@ export const es: Messages = { 'doc.seed.long': 'Entró en vigor sin votación, para no partir de una página en blanco. Sigue en votación, como todo texto.', 'doc.seed.votes.one': 'seed a minima · {n} voto', 'doc.seed.votes.other': 'seed a minima · {n} votos', - 'doc.seedProposal': 'Propuesta seed', + 'doc.seedProposal': 'Propuesta de arranque', 'doc.seedProposal.long': 'Texto de partida destacado para lanzar la votación. No está en vigor: entra en vigor por la votación, como toda propuesta.', 'doc.confirmed': 'confirmado por la votación', 'doc.adopted': 'adoptado el {date}', @@ -134,7 +135,21 @@ export const es: Messages = { 'doc.basket': 'Votar en bloque', 'doc.basket.count': 'Votar en bloque · {n}', 'doc.empty': 'Ningún artículo está en vigor. Las propuestas de abajo suben en cuanto alcanzan su umbral.', - 'doc.empty.seeds': 'Ningún artículo está aún en vigor. Empieza por las propuestas semilla: entran en vigor por el voto, como cualquier propuesta.', + 'opening.title': 'Info de apertura, doc. de referencia', + 'opening.none': 'Ningún artículo está aún en vigor.', + 'opening.amorce': 'Las propuestas de arranque entran en vigor por el voto, como las propuestas que siguen, con menos inercia para que el documento cobre vida más fácilmente. Se recomienda aumentar después la inercia, por el voto, para reducir la frecuencia de los cambios.', + 'opening.amorce.after': 'Las propuestas de arranque entran en vigor por el voto, como las propuestas que siguen, con menos inercia para que el documento cobre vida más fácilmente. Se recomienda aumentar después la inercia, por el voto, para reducir la frecuencia de los cambios.', + 'opening.inertia': 'Inercia en la apertura', + 'opening.inertia.amorce': 'Propuestas de arranque', + 'opening.inertia.title': 'Título del documento', + 'opening.inertia.orders': 'Orden de cada sección', + 'opening.inertia.others': 'Demás artículos', + 'opening.quorum': 'Quórum', + 'opening.quorum.levels': '{list} votantes, del nivel 1 al nivel 5.', + 'opening.quorum.same.one': '{n} votante en cada nivel.', + 'opening.quorum.same.other': '{n} votantes en cada nivel.', + 'opening.quorum.rule': 'Por debajo del quórum de un artículo, los votantes que faltan cuentan como «no».', + 'opening.quorum.author': 'El autor de la primera versión de un artículo, un administrador para los textos de partida, puede fijar otro mientras el artículo no guarde rastro de ningún voto.', 'doc.others.one': '{n} otra versión en curso', 'doc.others.other': '{n} otras versiones en curso', 'doc.published': 'Publicado en el repositorio de textos', @@ -202,6 +217,7 @@ export const es: Messages = { 'order.proposed.one': '{n} orden propuesto', 'order.proposed.other': '{n} órdenes propuestos', 'order.edit.note': 'Un orden se vota con la inercia {level}, como un artículo: desplegado, en la cesta, por la cadena Ğ1 en Monnaie libre.', + 'order.edit.effect': 'Si entra en vigor, solo los artículos de esta sección cambian de lugar; ningún texto cambia.', 'order.submitted': 'Orden sometido a votación.', 'title.propose': 'Proponer otro título', 'title.propose.label': 'Nuevo título', @@ -246,6 +262,7 @@ export const es: Messages = { 'vote.simulated.mark': 'voto simulado', 'vote.others': 'Otras versiones', 'vote.others.count': 'Otras versiones ({n})', + 'vote.others.rule': 'Si varias versiones alcanzan su umbral, entra en vigor la que tiene el margen más amplio.', 'vote.need': '{yes} sí para aprobar · {no} no para bloquear', 'vote.need.quorum': '{yes} sí para aprobar · el quórum la bloquea', 'vote.need.first.one': '{yes} sí de {quorum} votante para aprobar', @@ -259,6 +276,8 @@ export const es: Messages = { '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.permanent': 'La votación sigue abierta, sin plazo: puedes cambiar o retirar tu voto en cualquier momento; el último es el que cuenta.', + 'vote.rival': 'Ya hay un texto en vigor: para sustituirlo, esta versión necesita además un margen (votos a favor menos votos exigidos) más amplio que el de ese texto. El texto sustituido vuelve a ser una propuesta, con sus votos: puede volver.', 'vote.held': 'Una objeción retiene esta versión: hasta que se levante, nadie puede votar a favor. Puedes votar en contra o retirar tu voto; al levantarse, solo entra en vigor con los votos que sigan ahí.', 'vote.held.redit': 'Repite «{other}».', 'vote.held.contredit': 'Contradice «{other}».', @@ -299,7 +318,7 @@ export const es: Messages = { 'quorum.heading.other': 'Quórum · {n} votantes', 'quorum.default.one': 'Por defecto, el de su nivel: {n} votante.', 'quorum.default.other': 'Por defecto, el de su nivel: {n} votantes.', - 'quorum.explain': 'Fijas el quórum de este artículo mientras nadie haya votado. Por debajo, los votantes que faltan cuentan como «no».', + 'quorum.explain': 'Fijas el quórum de este artículo mientras no guarde rastro de ningún voto. Por debajo, los votantes que faltan cuentan como «no».', 'quorum.field': 'Quórum del artículo', 'quorum.set': 'Fijar este quórum', 'quorum.reset': 'Volver al valor por defecto ({n})', @@ -404,7 +423,7 @@ export const es: Messages = { 'ballots.verify.unreadable': 'No es un recibo de voto legible.', 'inertia.title': 'Inercia', - 'inertia.explain': 'Cuanto más alta es la inercia, más votos hacen falta para cambiar este texto. Por debajo del quórum, los votantes que faltan cuentan como «no»; más allá, la proporción de «sí» pedida baja cuando sube la participación.', + 'inertia.explain': 'Cuanto más alta es la inercia, más votos hacen falta para cambiar este texto. Por debajo del quórum del artículo, los votantes que faltan cuentan como «no»; más allá, la proporción exigida baja cuando sube la participación.', '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 del artículo: {level}.', @@ -420,38 +439,42 @@ export const es: Messages = { 'formula.W.wot': 'miembros de la red de confianza Ğ1', 'formula.W.group': 'miembros del grupo', 'formula.T': 'T — votos emitidos: {n}', + 'formula.Tprime': 'T′ — votantes contados: {n}, T o Q si faltan', 'formula.M': 'M — mayoría con participación plena: {n} %', - 'formula.G': 'G — gradiente: {n}', + 'formula.G': 'G — gradiente: {n}, deducido de la participación habitual del grupo', 'formula.B': 'B — base: {n}', + 'formula.B.wot': 'Elevada a la potencia W, B es tan pequeña que no cuenta en la red de confianza.', 'formula.Q': 'Q — quórum: {n}', 'formula.R': 'R — umbral actual: {n} votos a favor', - 'formula.reading': 'Por debajo del quórum Q, los votantes que faltan cuentan como «no». Más allá, la proporción de votos a favor pedida, la más alta en el quórum, baja cuando sube la participación; cuando vota todo el mundo, basta la mayoría M.', + 'formula.round': 'Una versión alcanza su umbral cuando sus votos a favor alcanzan R, redondeado al entero superior.', + 'formula.reading': 'Por debajo del quórum Q del artículo, los votantes que faltan cuentan como «no»: hacen falta tantos votos a favor como si hubieran votado Q personas. Más allá, la proporción de votos a favor pedida, la más alta en el quórum, baja cuando sube la participación; cuando vota todo el mundo, basta la mayoría M.', 'common.pct': '{n} %', 'settings.title': 'Ajustes de la fórmula', 'settings.intro': 'Para cambiar un texto, el grupo pide un número de votos a favor que depende de su electorado, de su participación habitual y de la inercia del artículo.', 'settings.usual': 'Participación habitual', 'settings.usual.median.one': '{n} votante de {electorate} ({pct}): la mediana de las {sample} propuestas votadas en el último año.', 'settings.usual.median.other': '{n} votantes de {electorate} ({pct}): la mediana de las {sample} propuestas votadas en el último año.', - 'settings.usual.reference.one': '{n} votante de {electorate} ({pct}), dos veces {q}, el quórum de referencia: la participación de referencia, hasta que el grupo haya votado {min} propuestas ({sample} por ahora).', - 'settings.usual.reference.other': '{n} votantes de {electorate} ({pct}), dos veces {q}, el quórum de referencia: la participación de referencia, hasta que el grupo haya votado {min} propuestas ({sample} por ahora).', + 'settings.usual.reference.one': '{n} votante de {electorate} ({pct}), dos veces {q}, el quórum de referencia: la participación de referencia, mientras se base en menos de {min} propuestas votadas en el último año ({sample} por ahora).', + 'settings.usual.reference.other': '{n} votantes de {electorate} ({pct}), dos veces {q}, el quórum de referencia: la participación de referencia, mientras se base en menos de {min} propuestas votadas en el último año ({sample} por ahora).', 'settings.usual.bounded.one': 'Mantenida entre el {lo} y el {hi} del electorado: el grupo pide sus votos a favor como si votara {n} persona ({pct}).', 'settings.usual.bounded.other': 'Mantenida entre el {lo} y el {hi} del electorado: el grupo pide sus votos a favor como si votaran {n} personas ({pct}).', 'settings.usual.when': 'Medida una vez al día, la última el {date}.', 'settings.usual.never': 'Medida una vez al día.', + 'settings.usual.quorum': 'No depende del quórum de un artículo.', 'settings.levels': 'Los cinco niveles', 'settings.col.level': 'Nivel', 'settings.col.share': 'Proporción exigida', 'settings.col.quorum': 'Quórum', 'settings.col.required': 'Votos a favor', 'settings.current': 'este artículo', - 'settings.share.note.one': 'Proporción exigida: la parte de votos a favor pedida, con {n} votante como de costumbre.', - 'settings.share.note.other': 'Proporción exigida: la parte de votos a favor pedida, con {n} votantes como de costumbre.', + 'settings.share.note': 'Proporción exigida: la que pide el nivel más allá del quórum del artículo, con la participación habitual.', 'settings.quorum.note': 'Quórum: por debajo, los votantes que faltan cuentan como «no». Es el del nivel; el autor de un artículo puede fijar otro antes del primer voto.', - 'settings.required.version.one': 'Votos a favor: con {n} votante, la participación de esta versión.', - 'settings.required.version.other': 'Votos a favor: con {n} votantes, la participación de esta versión.', - 'settings.required.usual.one': 'Votos a favor: con {n} votante, como de costumbre.', - 'settings.required.usual.other': 'Votos a favor: con {n} votantes, como de costumbre.', - 'settings.reading': 'En cada nivel, el grupo pide su parte de votos a favor cuando vota como de costumbre; más allá del quórum, cuantos más votantes, más se acerca a M. Por debajo del quórum del nivel, los votantes que faltan cuentan como «no».', + 'settings.required.version.one': 'Votos a favor: con {n} votante, la participación de esta versión, o con el quórum si es más alto.', + 'settings.required.version.other': 'Votos a favor: con {n} votantes, la participación de esta versión, o con el quórum si es más alto.', + 'settings.required.usual.one': 'Votos a favor: con {n} votante, la participación habitual redondeada, o con el quórum si es más alto.', + 'settings.required.usual.other': 'Votos a favor: con {n} votantes, la participación habitual redondeada, o con el quórum si es más alto.', + 'settings.reading': 'Más allá del quórum del artículo, cuantos más votantes, más baja la proporción exigida, hasta la mayoría M cuando vota todo el mundo. Por debajo del quórum del artículo, los votantes que faltan cuentan como «no», y hacen falta tantos votos a favor como si hubiera votado ese quórum.', + 'settings.gradient': 'G se calcula para que la proporción exigida, más allá del quórum del artículo, sea exactamente la del nivel con la participación habitual.', 'history.title': 'Correcciones', 'history.initial': 'texto inicial', diff --git a/web/src/lib/i18n/fr.ts b/web/src/lib/i18n/fr.ts index 028b964..c6faedd 100644 --- a/web/src/lib/i18n/fr.ts +++ b/web/src/lib/i18n/fr.ts @@ -46,8 +46,9 @@ export const fr = { '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.g1.pending': "La connexion Ğ1 arrive : l'inscription de libreDecision auprès de connect.monnaie-libre.fr est en cours.", 'signin.invite': "Tu as reçu un lien d'invitation ? Ouvre-le simplement : il te connecte.", + 'signin.uninvited': "Pour rentrer dans un groupe de décisions, il faut pour l'instant demander un lien d'invitation à Chiara ou à Yvv. Ce prototype n'est pas encore câblé sur la chaîne.", 'signin.dev': 'Connexion de développement', 'signin.dev.name': 'Nom', 'signin.dev.admin': 'administrateur des accès', @@ -107,8 +108,8 @@ export const fr = { 'docs.inForce.other': '{n} articles en vigueur', 'docs.open.one': '{n} proposition en cours', 'docs.open.other': '{n} propositions en cours', - 'docs.seeds.one': '{n} Proposition seed', - 'docs.seeds.other': '{n} Propositions seed', + 'docs.seeds.one': "{n} Proposition d'amorce", + 'docs.seeds.other': "{n} Propositions d'amorce", 'doc.view.label': 'Lire', 'doc.view.inforce': 'En vigueur', @@ -129,7 +130,7 @@ export const fr = { 'doc.seed.long': "Entré en vigueur sans vote, pour ne pas partir d'une page blanche. Il reste sous vote, comme tout texte.", 'doc.seed.votes.one': 'seed a minima · {n} vote', 'doc.seed.votes.other': 'seed a minima · {n} votes', - 'doc.seedProposal': 'Proposition seed', + 'doc.seedProposal': "Proposition d'amorce", 'doc.seedProposal.long': "Texte de départ mis en avant pour lancer le vote. Il n'est pas en vigueur : il y entre par le vote, comme toute proposition.", 'doc.confirmed': 'confirmé par le vote', 'doc.adopted': 'adopté le {date}', @@ -138,7 +139,21 @@ export const fr = { 'doc.basket': 'Voter en lot', 'doc.basket.count': 'Voter en lot · {n}', 'doc.empty': "Aucun article n'est en vigueur. Les propositions ci-dessous remontent dès qu'elles atteignent leur seuil.", - 'doc.empty.seeds': "Aucun article n'est encore en vigueur. Commence par les Propositions seed : elles y entrent par le vote, comme toute proposition.", + 'opening.title': 'Info ouverture doc de réf.', + 'opening.none': "Aucun article n'est encore en vigueur.", + 'opening.amorce': "Les Propositions d'amorce y entrent par le vote, comme les propositions qui suivent, avec moins d'inertie pour que le document prenne vie plus facilement. Il est recommandé d'augmenter ensuite l'inertie, par le vote, pour diminuer la fréquence des changements.", + 'opening.amorce.after': "Les Propositions d'amorce entrent en vigueur par le vote, comme les propositions qui suivent, avec moins d'inertie pour que le document prenne vie plus facilement. Il est recommandé d'augmenter ensuite l'inertie, par le vote, pour diminuer la fréquence des changements.", + 'opening.inertia': "Inertie à l'ouverture", + 'opening.inertia.amorce': "Propositions d'amorce", + 'opening.inertia.title': 'Titre du document', + 'opening.inertia.orders': 'Ordre de chaque section', + 'opening.inertia.others': 'Autres articles', + 'opening.quorum': 'Quorum', + 'opening.quorum.levels': '{list} votants, du cran 1 au cran 5.', + 'opening.quorum.same.one': '{n} votant à chaque cran.', + 'opening.quorum.same.other': '{n} votants à chaque cran.', + 'opening.quorum.rule': "Sous le quorum d'un article, les votants qui manquent comptent comme « non ».", + 'opening.quorum.author': "L'auteur de la première version d'un article, un administrateur pour les textes de départ, peut en fixer un autre tant que l'article ne garde la trace d'aucun vote.", 'doc.others.one': '{n} autre version en cours', 'doc.others.other': '{n} autres versions en cours', 'doc.published': 'Publié sur le dépôt des textes', @@ -206,6 +221,7 @@ export const fr = { 'order.proposed.one': '{n} ordre proposé', 'order.proposed.other': '{n} ordres proposés', 'order.edit.note': "Un ordre se vote à l'inertie {level}, comme un article : déplié, dans le panier, par la chaîne Ğ1 dans Monnaie libre.", + 'order.edit.effect': "S'il entre en vigueur, seuls les articles de cette section changent de place ; aucun texte ne change.", 'order.submitted': 'Ordre proposé au vote.', 'title.propose': 'Proposer un autre titre', 'title.propose.label': 'Nouveau titre', @@ -250,6 +266,7 @@ export const fr = { 'vote.simulated.mark': 'vote simulé', 'vote.others': 'Autres versions', 'vote.others.count': 'Autres versions ({n})', + 'vote.others.rule': 'Si plusieurs versions atteignent leur seuil, celle qui a la marge la plus large entre en vigueur.', 'vote.need': '{yes} oui pour passer · {no} non pour bloquer', 'vote.need.quorum': '{yes} oui pour passer · le quorum la bloque', 'vote.need.first.one': '{yes} oui sur {quorum} votant pour passer', @@ -263,6 +280,8 @@ export const fr = { '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.permanent': 'Le vote reste ouvert, sans échéance : tu peux changer ou retirer ton vote à tout moment, le dernier fait foi.', + 'vote.rival': 'Un texte est déjà en vigueur : pour le remplacer, cette version doit en plus atteindre une marge (voix pour moins voix exigées) plus large que celle de ce texte. Le texte remplacé redevient une proposition, avec ses votes : il peut revenir.', 'vote.held': "Une objection retient cette version : jusqu'à sa levée, personne ne peut plus voter pour. Tu peux voter contre ou retirer ton vote ; à la levée, elle n'entre en vigueur qu'avec les voix encore là.", 'vote.held.redit': 'Elle redit « {other} ».', 'vote.held.contredit': 'Elle contredit « {other} ».', @@ -303,7 +322,7 @@ export const fr = { 'quorum.heading.other': 'Quorum · {n} votants', 'quorum.default.one': 'Par défaut, celui de son cran : {n} votant.', 'quorum.default.other': 'Par défaut, celui de son cran : {n} votants.', - 'quorum.explain': "Tu fixes le quorum de cet article tant que personne n'a voté. En dessous, les votants qui manquent comptent comme « non ».", + 'quorum.explain': "Tu fixes le quorum de cet article tant qu'il ne garde la trace d'aucun vote. En dessous, les votants qui manquent comptent comme « non ».", 'quorum.field': "Quorum de l'article", 'quorum.set': 'Fixer ce quorum', 'quorum.reset': 'Revenir au défaut ({n})', @@ -408,7 +427,7 @@ export const fr = { 'ballots.verify.unreadable': "Ce n'est pas un reçu de vote lisible.", 'inertia.title': 'Inertie', - 'inertia.explain': "Plus l'inertie est haute, plus il faut de voix pour changer ce texte. Sous le quorum, les votants qui manquent comptent comme « non » ; au-delà, la part de « pour » demandée baisse quand la participation monte.", + 'inertia.explain': "Plus l'inertie est haute, plus il faut de voix pour changer ce texte. Sous le quorum de l'article, les votants qui manquent comptent comme « non » ; au-delà, la part exigée baisse quand la participation monte.", 'inertia.change': "Proposer un changement d'inertie", 'inertia.change.to': 'Nouvelle inertie', 'inertia.change.note': "Un changement d'inertie se vote lui-même à l'inertie de l'article : {level}.", @@ -424,38 +443,42 @@ export const fr = { 'formula.W.wot': 'membres de la toile de confiance Ğ1', 'formula.W.group': 'membres du groupe', 'formula.T': 'T — voix exprimées : {n}', + 'formula.Tprime': "T′ — votants retenus : {n}, T ou Q s'il en manque", 'formula.M': 'M — majorité à pleine participation : {n} %', - 'formula.G': 'G — gradient : {n}', + 'formula.G': 'G — gradient : {n}, déduit de la participation habituelle du groupe', 'formula.B': 'B — base : {n}', + 'formula.B.wot': "À la puissance W, B est si petit qu'il ne compte pas dans la toile de confiance.", 'formula.Q': 'Q — quorum : {n}', 'formula.R': 'R — seuil actuel : {n} voix pour', - 'formula.reading': "Sous le quorum Q, les votants qui manquent comptent comme « non ». Au-delà, la part de « pour » demandée, la plus haute au quorum, baisse quand la participation monte ; quand tout le monde vote, la majorité M suffit.", + 'formula.round': "Une version atteint son seuil quand ses voix pour atteignent R, arrondi à l'entier au-dessus.", + 'formula.reading': "Sous le quorum Q de l'article, les votants qui manquent comptent comme « non » : il faut autant de « pour » que si Q personnes avaient voté. Au-delà, la part de « pour » demandée, la plus haute au quorum, baisse quand la participation monte ; quand tout le monde vote, la majorité M suffit.", 'common.pct': '{n} %', 'settings.title': 'Réglages de la formule', 'settings.intro': "Pour changer un texte, le groupe demande un nombre de voix pour qui dépend de son électorat, de sa participation habituelle et de l'inertie de l'article.", 'settings.usual': 'Participation habituelle', 'settings.usual.median.one': '{n} votant sur {electorate} ({pct}) : la médiane des {sample} propositions votées depuis un an.', 'settings.usual.median.other': '{n} votants sur {electorate} ({pct}) : la médiane des {sample} propositions votées depuis un an.', - 'settings.usual.reference.one': "{n} votant sur {electorate} ({pct}), deux fois {q}, le quorum de référence : la participation de référence, tant que le groupe n'a pas voté sur {min} propositions ({sample} pour l'instant).", - 'settings.usual.reference.other': "{n} votants sur {electorate} ({pct}), deux fois {q}, le quorum de référence : la participation de référence, tant que le groupe n'a pas voté sur {min} propositions ({sample} pour l'instant).", + 'settings.usual.reference.one': "{n} votant sur {electorate} ({pct}), deux fois {q}, le quorum de référence : la participation de référence, tant qu'elle repose sur moins de {min} propositions votées depuis un an ({sample} pour l'instant).", + 'settings.usual.reference.other': "{n} votants sur {electorate} ({pct}), deux fois {q}, le quorum de référence : la participation de référence, tant qu'elle repose sur moins de {min} propositions votées depuis un an ({sample} pour l'instant).", 'settings.usual.bounded.one': "Ramenée entre {lo} et {hi} de l'électorat : le groupe demande ses voix pour comme si {n} personne votait ({pct}).", 'settings.usual.bounded.other': "Ramenée entre {lo} et {hi} de l'électorat : le groupe demande ses voix pour comme si {n} personnes votaient ({pct}).", 'settings.usual.when': 'Mesurée une fois par jour, la dernière fois le {date}.', 'settings.usual.never': 'Mesurée une fois par jour.', + 'settings.usual.quorum': "Elle ne dépend pas du quorum d'un article.", 'settings.levels': 'Les cinq crans', 'settings.col.level': 'Cran', 'settings.col.share': 'Part exigée', 'settings.col.quorum': 'Quorum', 'settings.col.required': 'Voix pour', 'settings.current': 'cet article', - 'settings.share.note.one': "Part exigée : la part de « pour » demandée, à {n} votant comme d'habitude.", - 'settings.share.note.other': "Part exigée : la part de « pour » demandée, à {n} votants comme d'habitude.", + 'settings.share.note': "Part exigée : celle que le cran demande au-delà du quorum de l'article, à la participation habituelle.", 'settings.quorum.note': "Quorum : en dessous, les votants qui manquent comptent comme « non ». C'est celui du cran ; l'auteur d'un article peut en fixer un autre avant le premier vote.", - 'settings.required.version.one': 'Voix pour : à {n} votant, la participation de cette version.', - 'settings.required.version.other': 'Voix pour : à {n} votants, la participation de cette version.', - 'settings.required.usual.one': "Voix pour : à {n} votant, comme d'habitude.", - 'settings.required.usual.other': "Voix pour : à {n} votants, comme d'habitude.", - 'settings.reading': "À chaque cran, le groupe demande sa part de « pour » quand il vote comme d'habitude ; au-delà du quorum, plus il y a de votants, plus elle se rapproche de M. Sous le quorum du cran, les votants qui manquent comptent comme « non ».", + 'settings.required.version.one': "Voix pour : à {n} votant, la participation de cette version, ou au quorum s'il est plus haut.", + 'settings.required.version.other': "Voix pour : à {n} votants, la participation de cette version, ou au quorum s'il est plus haut.", + 'settings.required.usual.one': "Voix pour : à {n} votant, la participation habituelle arrondie, ou au quorum s'il est plus haut.", + 'settings.required.usual.other': "Voix pour : à {n} votants, la participation habituelle arrondie, ou au quorum s'il est plus haut.", + 'settings.reading': "Au-delà du quorum de l'article, plus il y a de votants, plus la part exigée baisse, jusqu'à la majorité M quand tout le monde vote. Sous le quorum de l'article, les votants qui manquent comptent comme « non », et il faut autant de « pour » que si ce quorum avait voté.", + 'settings.gradient': "G se calcule pour que la part exigée, au-delà du quorum de l'article, soit exactement celle du cran à la participation habituelle.", 'history.title': 'Corrections', 'history.initial': 'texte initial', diff --git a/web/src/lib/labels.ts b/web/src/lib/labels.ts index b01a133..b091fe6 100644 --- a/web/src/lib/labels.ts +++ b/web/src/lib/labels.ts @@ -18,6 +18,19 @@ export function sectionLabel(s: string | null | undefined): string | null { return s && SECTIONS.includes(s) ? t(`section.${s}` as Key) : null; } +/** + * The sections whose articles carry the title already (Yvv, 2026-11-03: « Enlève les titres + * Introduction et Engagements fondamentaux, les titres sont déjà dans les contenus à voter. Ça + * embrouille. »): not named again where the text is read. They keep their name elsewhere — the + * order of the section, the basket, the open proposals. + */ +const TITLED_BY_THEIR_ARTICLES = ['introduction', 'fondamental']; + +/** The name of a section where the text is read, or null. */ +export function readerSectionLabel(s: string | null | undefined): string | null { + return s && TITLED_BY_THEIR_ARTICLES.includes(s) ? null : sectionLabel(s); +} + /** The name of a module (ADR 0009): « Titre du document », « Ordre des articles · Conclusion ». */ export function moduleName(unit: Unit): string { if (unit.kind === 'title') return t('module.title.name'); diff --git a/web/src/lib/opening.ts b/web/src/lib/opening.ts new file mode 100644 index 0000000..b42112a --- /dev/null +++ b/web/src/lib/opening.ts @@ -0,0 +1,52 @@ +// « Info ouverture doc de réf. » (lot U3; Yvv, 2026-11-03: « un petit chapitre qui explicite les +// réglages initiaux de l'auteur, notamment Quorum et Inertie »): what the chapter says is read in +// the data of the document, never written in. Pure: the component only says it. + +import type { DocView, Inertia, Unit } from './types'; + +/** + * The level a module opened at: the one before the first change voted. A change of the seed's own + * settings (an amendment, « réglage de départ du texte ») is the author's, part of the opening. + */ +export function openedAt(u: Unit): Inertia { + const voted = u.inertiaChanges.find((c) => c.cause === 'vote'); + return voted?.from ?? u.inertia; +} + +/** A Proposition d'amorce (ADR 0011): put forward by the seed, open or in force since. */ +export function isAmorce(u: Unit): boolean { + return [u.inForce, ...u.proposals].some((p) => p?.seedFeatured === true); +} + +/** An article the seed wrote (the author's): not one a member proposed since. */ +function fromSeed(u: Unit): boolean { + return [u.inForce, ...u.proposals].some((p) => p?.origin === 'seed'); +} + +const levels = (units: (Unit | null | undefined)[]): Inertia[] => + [...new Set(units.filter((u): u is Unit => !!u).map(openedAt))].sort((a, b) => a - b); + +export interface OpeningLevels { + amorce: Inertia[]; + title: Inertia[]; + orders: Inertia[]; + others: Inertia[]; +} + +/** The levels at the opening, by kind of module, each said once, lowest first. */ +export function openingLevels(view: Pick): OpeningLevels { + const seeded = view.units.filter(fromSeed); + return { + amorce: levels(seeded.filter(isAmorce)), + title: levels([view.modules.title]), + orders: levels(view.modules.orders), + others: levels(seeded.filter((u) => !isAmorce(u))), + }; +} + +/** The quorum of each level, 1 to 5 (ADR 0013): said once when they are all the same. */ +export function quorumsOf(quorums: number[] | undefined): { same: number | null; list: number[] } | null { + if (!quorums?.length) return null; + const first = quorums[0] ?? 0; + return { same: quorums.every((q) => q === first) ? first : null, list: quorums }; +} diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 6f5def8..9025a1f 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -117,6 +117,8 @@ export interface Proposal { seedMinimal: boolean; /** A seed proposal (ADR 0011): open, put forward by the seed to start the vote, not in force. */ seedProposal?: boolean; + /** Put forward by the seed (ADR 0011), in force since or not: a Proposition d'amorce stays one (lot U3). */ + seedFeatured?: boolean; /** The totals shown: recorded, pending and simulated votes, each intention in place of * the vote its session sees as its own (ADR 0012). */ tally: Tally; diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 26d6b08..cbaca28 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -27,11 +27,18 @@ {#if session.me} - {:else} + {:else if session.ready && session.g1}
{t('home.signin')}

{t('home.signin.note')}

+ {:else if session.ready} + +
+

{t('signin.uninvited')}

+

{t('signin.invite')}

+
{/if}
@@ -138,6 +145,13 @@ font-size: 0.9rem; max-width: 34ch; } + .enter .uninvited { + max-width: 40ch; + font-family: var(--font-text); + font-size: 1.02rem; + line-height: 1.6; + color: var(--ink); + } /* The keystone sets the arch, then the three points come under it. */ @media (prefers-reduced-motion: no-preference) { diff --git a/web/src/routes/connexion/+page.svelte b/web/src/routes/connexion/+page.svelte index d6ea732..1938a1e 100644 --- a/web/src/routes/connexion/+page.svelte +++ b/web/src/routes/connexion/+page.svelte @@ -42,6 +42,8 @@ {:else}

{t('signin.g1.pending')}

+ +

{t('signin.uninvited')}

{/if} diff --git a/web/src/routes/g/[group]/docs/[doc]/+page.svelte b/web/src/routes/g/[group]/docs/[doc]/+page.svelte index 32020b5..ada6c24 100644 --- a/web/src/routes/g/[group]/docs/[doc]/+page.svelte +++ b/web/src/routes/g/[group]/docs/[doc]/+page.svelte @@ -12,6 +12,7 @@ import DocPanel from '$lib/components/DocPanel.svelte'; import Guard from '$lib/components/Guard.svelte'; import Icon from '$lib/components/Icon.svelte'; + import OpeningInfo from '$lib/components/OpeningInfo.svelte'; import OrderLine from '$lib/components/OrderLine.svelte'; import Reorder from '$lib/components/Reorder.svelte'; import TitleBar from '$lib/components/TitleBar.svelte'; @@ -21,7 +22,7 @@ import { sections, votableIds } from '$lib/basket'; import { DocState, panelSearch, shown, TABS, type Entry, type PanelState, type View } from '$lib/doc.svelte'; import { i18n, LANGS, t } from '$lib/i18n/index.svelte'; - import { sectionLabel } from '$lib/labels'; + import { readerSectionLabel } from '$lib/labels'; import { answerPanel } from '$lib/objections'; import { BasketState } from '$lib/picks.svelte'; import { session } from '$lib/session.svelte'; @@ -259,12 +260,18 @@ {#if doc.mode !== 'inforce'}

{t(`doc.view.${doc.mode}.note`)}

{/if} + + +
+
{#if reading} - - {#if !doc.inForce.length}

{t(textSections.length ? 'doc.empty.seeds' : 'doc.empty')}

{/if} + + {#if !doc.inForce.length && !textSections.length}

{t('doc.empty')}

{/if} {#each textSections as s, i (`${i}:${s.section}`)} - {@const label = sectionLabel(s.section)} + {@const label = readerSectionLabel(s.section)}
{#if label}

{label}

{/if} {#if !s.order || reordering !== s.order.id} @@ -469,7 +476,22 @@ text-underline-offset: 3px; } .text { - margin-top: 2.2rem; + margin-top: 0; + } + /* The separation before the text (Yvv, 2026-11-03): a soft line in the keystone's colour of the + mood, fading at its ends — drawn by its background, never a border. */ + .opening-rule { + height: 2px; + margin: 2.2rem 0 1.6rem; + border: 0; + border-radius: 2px; + background: linear-gradient( + to right, + transparent, + color-mix(in srgb, var(--cle) 45%, transparent) 18%, + color-mix(in srgb, var(--cle) 45%, transparent) 82%, + transparent + ); } .lead { margin-bottom: 1.2rem; diff --git a/web/tests/unit/inertia.test.ts b/web/tests/unit/inertia.test.ts index ccd4efd..476be44 100644 --- a/web/tests/unit/inertia.test.ts +++ b/web/tests/unit/inertia.test.ts @@ -16,7 +16,7 @@ describe('five levels of inertia', () => { }); it('the threshold is told with the quorum, no longer with C (ADR 0013)', () => { const told = [ - 'deep.note', 'formula.reading', 'settings.reading', 'settings.share.note.one', 'settings.share.note.other', + 'deep.note', 'formula.reading', 'settings.reading', 'settings.share.note', 'settings.usual.reference.one', 'settings.usual.reference.other', ] as const; for (const cat of [fr, en, es]) { @@ -36,10 +36,10 @@ describe('five levels of inertia', () => { expect(cat['quorum.error.voted']).not.toMatch(/ne change plus|no longer changes|ya no cambia/); } expect(fr['inertia.explain']).toBe( - "Plus l'inertie est haute, plus il faut de voix pour changer ce texte. Sous le quorum, les votants qui manquent comptent comme « non » ; au-delà, la part de « pour » demandée baisse quand la participation monte.", + "Plus l'inertie est haute, plus il faut de voix pour changer ce texte. Sous le quorum de l'article, les votants qui manquent comptent comme « non » ; au-delà, la part exigée baisse quand la participation monte.", ); expect(fr['formula.reading']).toBe( - "Sous le quorum Q, les votants qui manquent comptent comme « non ». Au-delà, la part de « pour » demandée, la plus haute au quorum, baisse quand la participation monte ; quand tout le monde vote, la majorité M suffit.", + "Sous le quorum Q de l'article, les votants qui manquent comptent comme « non » : il faut autant de « pour » que si Q personnes avaient voté. Au-delà, la part de « pour » demandée, la plus haute au quorum, baisse quand la participation monte ; quand tout le monde vote, la majorité M suffit.", ); expect(fr['quorum.error.voted']).toBe("Quelqu'un a déjà voté sur cet article : son quorum ne change pas."); }); diff --git a/web/tests/unit/lexicon.test.ts b/web/tests/unit/lexicon.test.ts index c577ff3..8f8d20e 100644 --- a/web/tests/unit/lexicon.test.ts +++ b/web/tests/unit/lexicon.test.ts @@ -60,9 +60,10 @@ describe('anti-lexicon', () => { describe('the reading of the formula', () => { it('the settings say, like N1.1 and the formula, that the share falls beyond the quorum', () => { expect(fr['formula.reading']).toContain('Au-delà'); - expect(fr['settings.reading']).toContain('au-delà du quorum'); - expect(en['settings.reading']).toContain('beyond the quorum'); - expect(es['settings.reading']).toContain('más allá del quórum'); + // Lot U3: the quorum of the article, the defined term of lot T3c. + expect(fr['settings.reading']).toContain("Au-delà du quorum de l'article"); + expect(en['settings.reading']).toContain("Beyond the article's quorum"); + expect(es['settings.reading']).toContain('Más allá del quórum del artículo'); }); });