feat(session-1): socle SvelteKit et encyclopédie /zerbaj/ prérendue

SvelteKit 2 + Svelte 5 runes + TS strict, adapter-static en prérendu
intégral, trailingSlash always, paths.relative false.

Mesures de la porte de sortie
- 56 pages HTML prérendues (13 fiches × 3 langues + pages de service)
- 59 Ko de JS gzip (budget : 170) · build total 1,4 Mo (budget : 2 Mo)
- 0 chargement tiers, vérifié par grep du build
- fiche lisible sans JS : 1694 caractères de texte rendu
- npm run check : 248 fichiers, 0 erreur, 0 warning
- 0 erreur console au chargement
Lighthouse non exécuté : porte encore ouverte, à vérifier avant la S6.

Charte OKI
Tokens partagés, thème sombre par défaut, Archivo et Inter self-hébergées
(124 Ko de woff2), flag-bar à segments francs, bouton à balayage or,
gate prefers-reduced-motion unique. CSP stricte en .htaccess et _headers,
404 statique autonome, PWA avec registerSW à chemin absolu (playbook 2.9).

L'interface kréyòl n'est pas écrite par l'agent
Le catalogue gcf ne contient que les termes venant des documents du
porteur — 10 chaînes sur 132. Le reste vaut null et se replie sur le
français ; check:i18n produit la liste des 122 phrases à traduire, par
écran. <html lang> annonce la langue réellement écrite : fr tant que la
couverture reste sous 60 %, avec les fragments kréyòl marqués un par un.
Des tournures kréyòl que j'avais inventées ont été retirées.

Corrections en cours de build
- vite-plugin-pwa/sveltekit n'existe pas sans @vite-pwa/sveltekit :
  VitePWA en direct, comme le prescrivait déjà le playbook.
- @sveltejs/vite-plugin-svelte v7 n'exporte plus sveltekit : import
  depuis @sveltejs/kit/vite, vérifié sur les exports réels.
- Le sélecteur de langue fabriquait /fr/offline/, page inexistante.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
OKI
2026-07-27 11:44:21 -04:00
co-authored by Claude Opus 5
parent 8cba5adfa5
commit ccad7177b3
58 changed files with 9946 additions and 3 deletions
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env node
/**
* check-i18n.mjs — inventaire des chaînes d'interface non traduites.
*
* Le kréyòl est la langue principale du projet, et l'interface est écrite en français.
* C'est une contradiction assumée : aucun agent de code n'écrit de kréyòl (ADR 0005),
* et un catalogue rempli par un automate serait pire que vide.
*
* Ce n'est donc pas une dette technique mais une tâche de participation : ce script
* produit la liste, phrase par phrase, avec la version française à traduire.
*
* Sortie : docs/i18n-a-traduire.md
*/
import { readFile, writeFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const I18N = join(ROOT, 'src', 'lib', 'i18n');
const LOCALES = ['gcf', 'en'];
/** Regroupe les clés par préfixe pour que la traduction se fasse par écran, pas en vrac. */
const SECTIONS = {
site: 'Identité du site',
nav: 'Navigation',
home: 'Accueil',
zerbaj: 'Encyclopédie',
fiche: 'Fiche despèce',
manques: 'Ce qui manque',
kreyol: 'Questions de langue',
kontribiye: 'Participation',
jeu: 'Le jeu',
patouche: 'Module « Pa touché »',
footer: 'Pied de page',
credits: 'Crédits',
erreur: 'Erreurs',
offline: 'Hors ligne',
i18n: 'Bandeau de traduction'
};
async function main() {
const fr = JSON.parse(await readFile(join(I18N, 'fr.json'), 'utf8'));
const cles = Object.keys(fr).filter((c) => !c.startsWith('$'));
const l = [
'# Interface à traduire',
'',
`> Généré par \`npm run check:i18n\` le ${new Date().toISOString().slice(0, 10)}.`,
'> Sortie de script : ne pas éditer à la main.',
'',
'Le kréyòl est la langue principale du projet, et pourtant ce site affiche du français.',
'Ce nest pas un oubli : **aucun agent de code n’écrit de kréyòl** (ADR 0005), et un',
'catalogue rempli par une machine serait pire que vide — il figerait des tournures que',
'personne nemploie.',
'',
'La traduction est donc une contribution, au même titre quune photographie ou un',
'enregistrement. Marche à suivre : [`kreyol-participatif.md`](kreyol-participatif.md).',
''
];
const resume = [];
for (const locale of LOCALES) {
const cat = JSON.parse(await readFile(join(I18N, `${locale}.json`), 'utf8'));
const manquantes = cles.filter((c) => !cat[c]);
const couverture = Math.round(((cles.length - manquantes.length) / cles.length) * 100);
resume.push({ locale, manquantes: manquantes.length, total: cles.length, couverture });
l.push(`## ${locale === 'gcf' ? 'Kréyòl gwadloupéyen' : 'English'}${couverture} % traduit`, '');
if (!manquantes.length) {
l.push('_Rien à traduire._', '');
continue;
}
l.push(`**${manquantes.length} phrases sur ${cles.length}.**`, '');
const parSection = new Map();
for (const c of manquantes) {
const prefixe = c.split('.')[0];
if (!parSection.has(prefixe)) parSection.set(prefixe, []);
parSection.get(prefixe).push(c);
}
for (const [prefixe, liste] of parSection) {
l.push(`### ${SECTIONS[prefixe] ?? prefixe} (${liste.length})`, '');
l.push('| clé | français | traduction |', '|---|---|---|');
for (const c of liste) {
const source = String(fr[c]).replace(/\|/g, '\\|');
l.push(`| \`${c}\` | ${source} | |`);
}
l.push('');
}
}
l.push(
'## Comment rendre une traduction',
'',
'Remplir la colonne « traduction », ou envoyer la liste par tout moyen — un message,',
'un fichier, une note vocale. La forme na aucune importance ; la relecture par un',
'locuteur en a.',
'',
'Les valeurs traduites se reportent dans `src/lib/i18n/gcf.json`. Une valeur `null`',
'se replie automatiquement sur le français, avec lattribut `lang` correct : rien ne',
'casse tant que la traduction manque.',
''
);
await writeFile(join(ROOT, 'docs', 'i18n-a-traduire.md'), l.join('\n'));
for (const r of resume) {
console.log(`${r.locale} : ${r.total - r.manquantes}/${r.total} traduites (${r.couverture} %)`);
}
console.log('\n docs/i18n-a-traduire.md');
}
main().catch((e) => {
console.error('✘', e.stack ?? e.message);
process.exit(1);
});