Lot S, phase 2 : le quorum dans l'interface (fr, en, es), e2e 35 / 35

- types.ts : evaluation.quorum / yesNeeded / noNeeded, levels[].quorum, l'unité
  quorum / quorumDefault / canSetQuorum, doc.quorums (optionnels) ; publication
  « suspended » (ADR 0011) ;
- Réglages de la formule : la colonne « Quorum » des cinq crans et sa note, la
  participation de référence « deux fois 50, le quorum de référence » ; plus de
  ligne C ; sur téléphone, les six colonnes tiennent dans la feuille ;
- La formule du seuil : T' = max(T, Q), R = B^W + ratio(T')·T', la ligne Q (le
  quorum de la version) au lieu de C ;
- textes : deep.note, formula.reading, settings.* parlent du quorum, plus de C
  (test vu rouge puis vert, tests/unit/inertia.test.ts) ;
- e2e : Seuil 9 (cran 2) et 39 (cran 3) dans Monnaie libre, les quorums 12 · 12 ·
  50 · 50 · 50, voix pour à 100 votants 60 · 67 · 75 · 83 · 90, à 0 votant
  8 · 9 · 39 · 43 · 46.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Yvv
2026-11-03 05:15:18 +01:00
co-authored by Claude Opus 5.5
parent 01b7841472
commit 17addf8d9f
11 changed files with 112 additions and 47 deletions
+1 -1
View File
@@ -83,7 +83,7 @@ test('M1 — mobile : logo dans la barre, l’accueil sous la voûte, menu en ti
// in force at the start, its vote unfolds in the list of proposals).
await page.locator('ul.deep button.row').first().click();
const fold = page.locator('ul.deep section.fold');
await expect(fold.getByText('Seuil : 50 voix pour')).toBeVisible();
await expect(fold.getByText('Seuil : 9 voix pour')).toBeVisible();
// The settings of the formula read on a phone: the table of the five levels holds in the sheet.
await fold.getByRole('button', { name: 'Réglages de la formule' }).click();
const panel = page.locator('aside.panel');
+1 -1
View File
@@ -190,7 +190,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 : les « pour » demandés au-delà de C, à 2 votants comme d'habitude.")).toBeVisible();
await expect(panel.getByText("Part exigée : la part de « pour » demandée, à 2 votants comme d'habitude.")).toBeVisible();
await shot(page, 'c27-participation-bornee');
});
+14 -8
View File
@@ -283,7 +283,8 @@ test('C19 — un article, une ligne : ses versions se déplient', async ({ page
// A version unfolds its vote below it, as any proposal.
await versions.nth(1).click();
await expect(versions.nth(1)).toHaveAttribute('aria-expanded', 'true');
await expect(page.locator('section.fold').getByText('Seuil : 50 voix pour')).toBeVisible();
// Level 3, the quorum 50 (ADR 0013): 39 « pour » at no vote, the voters missing counted as « non ».
await expect(page.locator('section.fold').getByText('Seuil : 39 voix pour')).toBeVisible();
await expect(page).toHaveURL(/vote=/);
await e3.click();
await expect(e3).toHaveAttribute('aria-expanded', 'false');
@@ -299,19 +300,22 @@ test('C20 — réglages de la formule et cinq crans', async ({ page }) => {
const panel = page.locator('aside.panel');
await expect(panel.getByRole('heading', { name: 'Réglages de la formule' })).toBeVisible();
await expect(panel.getByText(/W — électorat : 5\s745/)).toBeVisible();
await expect(panel.getByText('C — minimum de voix pour : 50')).toBeVisible();
// No vote yet: the reference, twice C.
await expect(panel.getByText(/100 votants sur 5\s745 \(1,74 %\), deux fois C : la participation de référence/)).toBeVisible();
// No vote yet: the reference, twice the reference quorum (ADR 0013).
await expect(panel.getByText(/100 votants sur 5\s745 \(1,74 %\), deux fois 50, le quorum de référence : la participation de référence/)).toBeVisible();
const rows = panel.locator('table tbody tr');
await expect(rows).toHaveCount(5);
await expect(rows.locator('th')).toHaveText([/1\s*très basse/, /2\s*basse/, /3\s*moyenne/, /4\s*haute/, /5\s*très haute/]);
await expect(rows.locator('td:nth-child(2)')).toHaveText(['50 %', '50 %', '55 %', '60 %', '66 %']);
await expect(rows.locator('td:nth-child(4)')).toHaveText(['60 %', '67 %', '75 %', '83 %', '90 %']);
// At 100 voters: C plus each level's share of the 50 votes beyond C.
// The quorum of each level (Yvv: « 12 · 12 · 50 · 50 · 50 »).
const quorums = rows.locator('td:nth-child(5)');
await expect(quorums).toHaveText(['12', '12', '50', '50', '50']);
// 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(['80', '84', '88', '92', '95']);
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 : les « pour » demandés au-delà de C, à 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();
await expect(panel.getByText(/^Quorum : en dessous, les votants qui manquent comptent comme « non »\./)).toBeVisible();
await page.keyboard.press('Escape');
// From the vote of an article: Unicité du compte, a seed proposal, is at 2 / 5, its row is marked.
@@ -322,7 +326,9 @@ test('C20 — réglages de la formule et cinq crans', async ({ page }) => {
await expect(panel.locator('tr.current')).toHaveCount(1);
await expect(panel.locator('tr.current th')).toContainText('2');
await expect(panel.locator('tr.current th')).toContainText('cet article');
await expect(required).toHaveText(['50', '50', '50', '50', '50']);
// 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 page.keyboard.press('Escape');
+2 -1
View File
@@ -195,7 +195,8 @@ test('C24 — sans compte de vote, le vote par la chaîne est fermé et le dit ;
await expect(fold.getByText("Le vote par la chaîne Ğ1 n'est pas encore ouvert : le compte qui reçoit les votes reste à créer.", { exact: false })).toBeVisible();
await expect(fold.getByRole('button', { name: 'Pour', exact: true })).toHaveCount(0);
// The totals and the threshold stay in sight.
await expect(fold.getByText('Seuil : 50 voix pour')).toBeVisible();
// Raison d'être, level 2, the quorum 12 (ADR 0013).
await expect(fold.getByText('Seuil : 9 voix pour')).toBeVisible();
await fold.getByText('Ton vote ne se montre à personne', { exact: false }).scrollIntoViewIfNeeded();
await shot(page, '6-vote-ferme');
// The basket, opened by its address, says the vote is closed — never « Tes votes comptent dès l'envoi ».
+10 -4
View File
@@ -33,12 +33,18 @@
<details class="formula">
<summary>{t('formula.title')}</summary>
<p class="muted">{t('formula.intro')}</p>
<!-- ADR 0013: judged on T′ = max(T, Q) voters; below the quorum, the voters missing count as against. -->
<math display="block" class="math">
<mrow>
<mi>R</mi><mo>=</mo><mi>C</mi><mo>+</mo><msup><mi>B</mi><mi>W</mi></msup><mo>+</mo>
<msup><mi>T</mi><mo>′</mo></msup><mo>=</mo><mi>max</mi><mo>(</mo><mi>T</mi><mo>,</mo><mi>Q</mi><mo>)</mo>
</mrow>
</math>
<math display="block" class="math">
<mrow>
<mi>R</mi><mo>=</mo><msup><mi>B</mi><mi>W</mi></msup><mo>+</mo>
<mrow><mo>(</mo><mi>M</mi><mo>+</mo><mo>(</mo><mn>1</mn><mo>−</mo><mi>M</mi><mo>)</mo><mo>(</mo><mn>1</mn><mo>−</mo>
<msup><mrow><mo>(</mo><mfrac><mi>T</mi><mi>W</mi></mfrac><mo>)</mo></mrow><mi>G</mi></msup><mo>)</mo><mo>)</mo></mrow>
<mo>·</mo><mi>max</mi><mo>(</mo><mn>0</mn><mo>,</mo><mi>T</mi><mo>−</mo><mi>C</mi><mo>)</mo>
<msup><mrow><mo>(</mo><mfrac><msup><mi>T</mi><mo>′</mo></msup><mi>W</mi></mfrac><mo>)</mo></mrow><mi>G</mi></msup><mo>)</mo><mo>)</mo></mrow>
<mo>·</mo><msup><mi>T</mi><mo>′</mo></msup>
</mrow>
</math>
<ul class="values">
@@ -49,7 +55,7 @@
<li>{t('formula.G', { n: i18n.number(Math.round(p.gradient * 1000) / 1000) })}</li>
{/if}
<li>{t('formula.B', { n: i18n.number(base) })}</li>
<li>{t('formula.C', { n: i18n.number(floor) })}</li>
<li>{t('formula.Q', { n: i18n.number(evaluation?.quorum ?? p?.quorum ?? floor) })}</li>
{#if evaluation}<li class="r">{t('formula.R', { n: evaluation.required })}</li>{/if}
</ul>
<p class="faint">{t('formula.reading')}</p>
+19 -5
View File
@@ -1,8 +1,9 @@
<script lang="ts">
// The settings of the formula, read only: W and its source, C, B, the usual
// The settings of the formula, read only: W and its source, B, the usual
// turnout of the group (median or reference) and the five levels on its
// scale. From the head of the document, the *for* votes asked are those of
// the usual turnout; from a version, those of its own turnout.
// scale, each with its quorum (ADR 0013). From the head of the document, the
// *for* votes asked are those of the usual turnout; from a version, those of
// its own turnout, on its article's quorum.
import { api } from '$lib/api';
import type { DocState } from '$lib/doc.svelte';
import { i18n, t } from '$lib/i18n/index.svelte';
@@ -50,7 +51,6 @@
<ul class="values">
<li>{t('formula.W', { n: i18n.number(view.doc.electorate), source: t(view.doc.electorateSource === 'wot' ? 'formula.W.wot' : 'formula.W.group') })}</li>
<li>{t('formula.C', { n: i18n.number(view.doc.voteFloor) })}</li>
<li>{t('formula.B', { n: i18n.number(view.doc.formula.base) })}</li>
</ul>
@@ -60,7 +60,7 @@
{#if p.source === 'median'}
{i18n.plural('settings.usual.median', shown?.voters ?? p.voters, { electorate: i18n.number(view.doc.electorate), pct: pct(shown?.ratio ?? p.ratio), sample: p.sample })}
{:else}
{i18n.plural('settings.usual.reference', shown?.voters ?? p.voters, { electorate: i18n.number(view.doc.electorate), pct: pct(shown?.ratio ?? p.ratio), min: p.minSample, sample: p.sample })}
{i18n.plural('settings.usual.reference', shown?.voters ?? p.voters, { electorate: i18n.number(view.doc.electorate), pct: pct(shown?.ratio ?? p.ratio), q: i18n.number(view.doc.voteFloor), min: p.minSample, sample: p.sample })}
{/if}
</p>
{#if p.bounded}<p class="faint small">{i18n.plural('settings.usual.bounded', p.voters, { lo: pct(p.bounds[0]), hi: pct(p.bounds[1]), pct: pct(p.ratio) })}</p>{/if}
@@ -76,6 +76,7 @@
<th scope="col">M</th>
<th scope="col">G</th>
<th scope="col">{t('settings.col.share')}</th>
<th scope="col">{t('settings.col.quorum')}</th>
<th scope="col">{t('settings.col.required')}</th>
</tr>
</thead>
@@ -92,6 +93,7 @@
<td>{whole(l.majorityPct)}</td>
<td>{gradient(l.gradient)}</td>
<td>{whole(l.sharePct)}</td>
<td>{l.quorum === undefined ? '—' : i18n.number(l.quorum)}</td>
<td class="required">{l.required === null ? '—' : i18n.number(l.required)}</td>
</tr>
{/each}
@@ -99,6 +101,7 @@
</table>
</div>
<p class="faint small">{i18n.plural('settings.share.note', p.voters)}</p>
<p class="faint small">{t('settings.quorum.note')}</p>
<p class="faint small">{i18n.plural(version ? 'settings.required.version' : 'settings.required.usual', cast)}</p>
<p class="faint small">{t('settings.reading')}</p>
</section>
@@ -194,4 +197,15 @@
.tag {
font-size: 0.68rem;
}
/* On a phone, six columns hold in the sheet: the headers and the level's name wrap. */
@media (max-width: 480px) {
th,
td {
padding: 0.4rem 0.3rem;
}
thead th,
th[scope='row'] {
white-space: normal;
}
}
</style>
+10 -8
View File
@@ -137,7 +137,7 @@ export const en: Messages = {
'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.note': 'Not in force yet. Each one moves up as soon as it reaches its threshold: the share of votes in favour its inertia requires, counted on at least its quorum of voters.',
'deep.new': 'Propose a new article',
'deep.drafts': 'Your drafts',
'deep.drafts.note': 'Not submitted to vote. Only you can see them.',
@@ -375,17 +375,17 @@ export const en: Messages = {
'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.Q': 'Q — quorum: {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.',
'formula.reading': 'At low turnout, near-unanimity is needed; when everyone votes, majority M is enough. Below the quorum Q, the missing voters count as against.',
'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 C: 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 C: 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, 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.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}.',
@@ -393,15 +393,17 @@ export const en: Messages = {
'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 votes in favour asked beyond C, at {n} voter as usual.',
'settings.share.note.other': 'Share asked: the votes in favour asked beyond C, at {n} voters as usual.',
'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.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 beyond C when it votes as usual; the more voters, the closer it comes to M. Nothing passes below C votes in favour.',
'settings.reading': 'At each level, the group asks for its share of votes in favour when it votes as usual; the more voters, the closer it comes to M. Below the quorum of the level, the missing voters count as against.',
'history.title': 'Corrections',
'history.initial': 'initial text',
+10 -8
View File
@@ -137,7 +137,7 @@ export const es: Messages = {
'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.note': 'Aún no están en vigor. Cada una sube en cuanto alcanza su umbral: la proporción de votos a favor que exige su inercia, contada sobre al menos su quórum de votantes.',
'deep.new': 'Proponer un artículo nuevo',
'deep.drafts': 'Tus borradores',
'deep.drafts.note': 'No sometidos a votación. Solo tú los ves.',
@@ -375,17 +375,17 @@ export const es: Messages = {
'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.Q': 'Q — quórum: {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.',
'formula.reading': 'Con poca participación, hace falta casi la unanimidad; cuando vota todo el mundo, basta la mayoría M. Por debajo del quórum Q, los votantes que faltan cuentan como «no».',
'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 C: 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 C: 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, 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.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}.',
@@ -393,15 +393,17 @@ export const es: Messages = {
'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: los votos a favor pedidos más allá de C, con {n} votante como de costumbre.',
'settings.share.note.other': 'Proporción exigida: los votos a favor pedidos más allá de C, con {n} votantes como de costumbre.',
'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.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 más allá de C cuando vota como de costumbre; cuantos más votantes, más se acerca a M. Nada pasa con menos de C votos a favor.',
'settings.reading': 'En cada nivel, el grupo pide su parte de votos a favor cuando vota como de costumbre; cuantos más votantes, más se acerca a M. Por debajo del quórum del nivel, los votantes que faltan cuentan como «no».',
'history.title': 'Correcciones',
'history.initial': 'texto inicial',
+10 -8
View File
@@ -141,7 +141,7 @@ export const fr = {
'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.note': "Pas encore en vigueur. Chacune remonte dès qu'elle atteint son seuil : la part de voix pour que demande son inertie, comptée sur au moins son quorum de votants.",
'deep.new': 'Proposer un nouvel article',
'deep.drafts': 'Tes brouillons',
'deep.drafts.note': 'Non soumis au vote. Toi seul les vois.',
@@ -379,17 +379,17 @@ export const fr = {
'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.Q': 'Q — quorum : {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.",
'formula.reading': "À faible participation, il faut presque l'unanimité ; quand tout le monde vote, la majorité M suffit. Sous le quorum Q, les votants qui manquent comptent comme « non ».",
'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 C : 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 C : 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 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.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}.',
@@ -397,15 +397,17 @@ export const fr = {
'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 : les « pour » demandés au-delà de C, à {n} votant comme d'habitude.",
'settings.share.note.other': "Part exigée : les « pour » demandés au-delà de C, à {n} votants comme d'habitude.",
'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.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 » au-delà de C quand il vote comme d'habitude ; plus il y a de votants, plus elle se rapproche de M. Rien ne passe sous C voix pour.",
'settings.reading': "À chaque cran, le groupe demande sa part de « pour » quand il vote comme d'habitude ; plus il y a de votants, plus elle se rapproche de M. Sous le quorum du cran, les votants qui manquent comptent comme « non ».",
'history.title': 'Corrections',
'history.initial': 'texte initial',
+21 -3
View File
@@ -4,17 +4,19 @@ export type LangMap = Partial<Record<Lang, string>>;
export type Inertia = 1 | 2 | 3 | 4 | 5;
export const INERTIA_LEVELS: Inertia[] = [1, 2, 3, 4, 5];
/** One level on the group's scale: M and G of the formula, s_k, and the *for* votes asked at `cast` votes. */
/** One level on the group's scale: M and G of the formula, s_k, its quorum, and the *for* votes asked at `cast` votes. */
export interface LevelInfo {
level: Inertia;
majorityPct: number;
gradient: number;
sharePct: number;
/** Q — the quorum of the level (ADR 0013); from a version, its article's own if it has one. */
quorum?: number;
cast: number;
required: number | null;
}
/** p̃ — the usual turnout of the group, measured once a day (or the reference 2C/W). */
/** p̃ — the usual turnout of the group, measured once a day (or the reference 2Q₀/W, Q₀ = `voteFloor`). */
export interface ParticipationInfo {
ratio: number;
source: 'median' | 'reference';
@@ -60,11 +62,17 @@ export interface Tally {
export interface Evaluation {
cast: number;
/** Q — the quorum it is judged on (ADR 0013): below it, the voters missing count as against. */
quorum?: number;
required: number;
reached: boolean;
missing: number;
requiredRatio: number;
wellRated: boolean;
/** The total of *for* that passes with the *against* cast now. */
yesNeeded?: number;
/** The total of *against* that holds the *for* cast now in check (0: the quorum holds them). */
noNeeded?: number;
}
export interface Translation {
@@ -204,6 +212,12 @@ export interface Unit {
members?: string[];
/** An order of fewer than two articles: nothing to decide, not shown. */
moot?: boolean;
/** Its own quorum, set by the author of its first version before the first vote; `null`: its level's (ADR 0013). */
quorum?: number | null;
/** The quorum of its level. */
quorumDefault?: number;
/** This session may set its quorum now (`PUT /units/{id}/quorum`). */
canSetQuorum?: boolean;
}
/** What a document decides besides its articles: its title, the order of each section. */
@@ -213,7 +227,8 @@ export interface Modules {
}
export interface Publication {
state: 'none' | 'pending' | 'done';
/** `suspended`: no article in force yet, the queue waits for the first text (ADR 0011). */
state: 'none' | 'pending' | 'suspended' | 'done';
configured: boolean;
since?: string;
doneAt?: string | null;
@@ -233,7 +248,10 @@ export interface DocView {
title: LangMap;
summary: LangMap;
sourceLang: Lang;
/** Q₀ — the reference quorum (the C of before): the reference turnout is 2Q₀/W. */
voteFloor: number;
/** The quorum of each level, 1 to 5 (ADR 0013). */
quorums?: number[];
electorate: number;
electorateSource: 'wot' | 'group';
formula: { base: number };
+14
View File
@@ -14,6 +14,20 @@ describe('five levels of inertia', () => {
expect(INERTIA_LEVELS.map((k) => en[`inertia.name.${k}`])).toEqual(['very low', 'low', 'medium', 'high', 'very high']);
expect(INERTIA_LEVELS.map((k) => es[`inertia.name.${k}`])).toEqual(['muy baja', 'baja', 'media', 'alta', 'muy alta']);
});
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',
'settings.usual.reference.one', 'settings.usual.reference.other',
] as const;
for (const cat of [fr, en, es]) {
for (const k of told) expect(cat[k], k).not.toMatch(/\bC\b/);
expect(cat['deep.note']).not.toContain('{floor}');
expect(cat['formula.Q']).toContain('{n}');
expect(cat['settings.usual.reference.other']).toContain('{q}');
}
expect(fr['formula.reading']).toContain('les votants qui manquent comptent comme « non »');
expect(fr['settings.quorum.note']).toContain('avant le premier vote');
});
it('a change of inertia is said to be voted at the level of the article, no longer one above', () => {
for (const cat of [fr, en, es]) expect(cat['inertia.change.note']).toContain('{level}');
expect(fr['inertia.change.note']).toContain("à l'inertie de l'article");