Files
decision/web/tests/unit/lexicon.test.ts
T
YvvandClaude Opus 5.5 b260596cfd v3 : le vote déplié sous l'article, le panier, la signature par le portefeuille Ğ1
- Le vote se déplie sous l'article, façon accordéon, l'URL pour état (?vote=) :
  niveau d'eau, totaux, seuil et voix manquantes, ton vote et où il en est
  (enregistré, inscrit sur la chaîne, en attente, à reconfirmer, non compté),
  inertie et formule. Le reste d'une version (coquilles, traductions,
  signalements, historique) passe dans l'onglet « Détails » du panneau.
- Le panier : article par article, « tout pour / tout contre » par section ou
  pour tout le document, jamais pour un article à plusieurs versions ; un seul
  geste (une écriture, ou une transaction).
- La signature : le portefeuille de la page (injectedWeb3), polkadot-api chargé
  au moment de signer ; sans portefeuille, Ğecko par lien et QR ; la page
  attend la confirmation. Un bulletin que le portefeuille n'a pas signé est
  lâché aussitôt (DELETE /api/ballots/{id}).
- Pastilles d'ambiance : flèches dans le groupe, anneau de focus pointillé à
  la couleur d'accent de la page (3:1 au moins), distinct du choix.
- e2e : chaîne simulée (indexeur et nœud), faux portefeuille ; C21–C24, et
  C4, C5, C6, C11, C17, C19, C20, M1 mis à jour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 22:47:26 +01:00

58 lines
2.4 KiB
TypeScript

// The words of the screen: every catalogue has the same keys and the same
// placeholders, and no visible text uses a forbidden term.
import { readdirSync, readFileSync, statSync } from 'node:fs';
import { join } from 'node:path';
import { describe, expect, it } from 'vitest';
import { en } from '../../src/lib/i18n/en';
import { es } from '../../src/lib/i18n/es';
import { fr } from '../../src/lib/i18n/fr';
/** Never shown to a person (case- and accent-insensitive). They may live on as code identifiers. */
export const FORBIDDEN_UI_TERMS = [
'déléguer', 'délégation', 'délégué', 'dette', 'retard', 'en souffrance', 'corpus', 'snapshot',
'verdict', 'triage', 'oligarchie', 'peuple adulte',
// The Ğ1 chain and its wallets, in plain words (lot B): never the jargon.
'blockchain', 'extrinsic', 'extrinsèque',
];
const fold = (s: string) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase();
const marks = (s: string) => (s.match(/\{\w+\}/g) ?? []).sort().join(',');
function svelteFiles(dir: string): string[] {
return readdirSync(dir).flatMap((name) => {
const p = join(dir, name);
return statSync(p).isDirectory() ? svelteFiles(p) : p.endsWith('.svelte') ? [p] : [];
});
}
describe('catalogues', () => {
it('en and es have exactly the keys of fr, in order', () => {
expect(Object.keys(en)).toEqual(Object.keys(fr));
expect(Object.keys(es)).toEqual(Object.keys(fr));
});
it('placeholders are kept', () => {
for (const k of Object.keys(fr) as (keyof typeof fr)[]) {
expect(marks(en[k]), `en ${k}`).toBe(marks(fr[k]));
expect(marks(es[k]), `es ${k}`).toBe(marks(fr[k]));
}
});
});
describe('anti-lexicon', () => {
it('no forbidden term in the French catalogue', () => {
for (const [k, v] of Object.entries(fr)) {
for (const term of FORBIDDEN_UI_TERMS) expect(fold(v).includes(fold(term)), `${k}: ${term}`).toBe(false);
}
});
it('no forbidden term in template text of components', () => {
for (const file of svelteFiles(join(__dirname, '../../src'))) {
const markup = readFileSync(file, 'utf8')
.replace(/<script[\s\S]*?<\/script>/g, '')
.replace(/<style[\s\S]*?<\/style>/g, '')
.replace(/\{[^}]*\}/g, ' ');
const text = fold(markup.replace(/<[^>]+>/g, ' '));
for (const term of FORBIDDEN_UI_TERMS) expect(text.includes(fold(term)), `${file}: ${term}`).toBe(false);
}
});
});