8ebc1da9e4
- Astro statique, EN par defaut + /fr/ + /nl/, detection langue navigateur - i18n par fichiers JSON, ajouter une langue = ajouter des traductions - Contenu original porte a l'identique (diff d'inventaire par langue) - Chronologie legislative corrigee sur sources primaires (PE, Conseil, votes) - Timeline 23 precedents + observatoire 35 items + annuaire 66 outils FOSS, chaque affirmation verifiee et sourcee - Zero requete tierce (teste), lisible JS coupe, axe WCAG AA x2 themes - Version offline monofichier par langue a chaque build - Docker multi-stage vers nginx + CSP stricte auto-generee - CI GitHub Actions (SHA-pinnees) + verification hebdo des liens - CONTRIBUTING : divulgation d'affiliation obligatoire, allowlist de domaines testee en CI
164 lines
5.8 KiB
JavaScript
164 lines
5.8 KiB
JavaScript
// QA gate for the content migration: proves that, inside the narrative window
|
||
// (everything between the first section head and the manifesto), the built
|
||
// pages carry EXACTLY the legacy prose, line-for-line and in order, modulo
|
||
// the documented categories below. Anything else fails loudly.
|
||
//
|
||
// Categories (each is reported with counts and samples):
|
||
// labels built-only lines that are ToolCard field labels — in the
|
||
// legacy page these strings were CSS-generated (::before
|
||
// content), so they are absent from the legacy DOM text.
|
||
// excluded legacy-only lines that belong to the deliberately deferred
|
||
// legislative-status claims (see extract-log.json).
|
||
// quotes " and ' are typeset as “ ” ‘ ’ by Astro's default
|
||
// markdown `smartypants`; normalized before comparing.
|
||
// spacing identical text but different whitespace at tag boundaries
|
||
// (component templates put line breaks between inline elements
|
||
// where the legacy HTML had none).
|
||
//
|
||
// Usage: node migration/compare.mjs (after extract + build + extract --built)
|
||
|
||
import { readFileSync, writeFileSync } from 'node:fs'
|
||
import { dirname, join } from 'node:path'
|
||
import { fileURLToPath } from 'node:url'
|
||
|
||
const INV = join(dirname(fileURLToPath(import.meta.url)), 'inventory')
|
||
const LANGS = ['en', 'fr', 'nl']
|
||
|
||
const LABELS = {
|
||
en: new Set(["What it's for", 'Why it matters', 'For whom & when', 'Install & use']),
|
||
fr: new Set(['À quoi ça sert', 'Pourquoi', 'Pour qui & quand', 'Installer & utiliser']),
|
||
nl: new Set([
|
||
'Waarvoor dient het',
|
||
'Waarom het ertoe doet',
|
||
'Voor wie & wanneer',
|
||
'Installeren & gebruiken',
|
||
]),
|
||
}
|
||
|
||
// The six cell lines per language of the two deferred #menace table rows.
|
||
const EXCLUDED = {
|
||
en: [
|
||
'Duration',
|
||
'Temporary, extended to 3 April 2028',
|
||
'Permanent',
|
||
'Status (Jul 2026)',
|
||
'Adopted 9 July 2026',
|
||
'Under negotiation, 5th trilogue failed 29 June',
|
||
],
|
||
fr: [
|
||
'Durée',
|
||
"Temporaire, prolongé jusqu'au 3 avril 2028",
|
||
'Permanent',
|
||
'Statut (juil. 2026)',
|
||
'Adopté le 9 juillet 2026',
|
||
'En négociation, 5e trilogue échoué le 29 juin',
|
||
],
|
||
nl: [
|
||
'Looptijd',
|
||
'Tijdelijk, verlengd tot 3 april 2028',
|
||
'Permanent',
|
||
'Status (juli 2026)',
|
||
'Aangenomen op 9 juli 2026',
|
||
'In onderhandeling, 5e trialoog mislukt op 29 juni',
|
||
],
|
||
}
|
||
|
||
const unquote = (s) => s.replace(/[“”]/g, '"').replace(/[‘’]/g, "'")
|
||
const START = /^(PART|PARTIE|DEEL|MEMO) 00 · |^(PART|PARTIE|DEEL) 00 · /
|
||
const END = /(become ungovernable|devenir ingouvernable|onbestuurbaar worden)/i
|
||
|
||
function narrativeWindow(lines, file) {
|
||
const start = lines.findIndex((l) => START.test(l))
|
||
// the end sentinel is searched after the start (the new hero h1 also
|
||
// contains "Become Ungovernable")
|
||
const end = lines.findIndex((l, idx) => idx > start && END.test(l))
|
||
if (start === -1 || end === -1 || end < start) {
|
||
throw new Error(`cannot find narrative window in ${file} (start=${start}, end=${end})`)
|
||
}
|
||
return {
|
||
head: lines.slice(0, start),
|
||
body: lines.slice(start, end + 1),
|
||
tail: lines.slice(end + 1),
|
||
}
|
||
}
|
||
|
||
const report = {}
|
||
let failed = false
|
||
|
||
for (const lang of LANGS) {
|
||
const legacyAll = readFileSync(join(INV, `legacy-${lang}.txt`), 'utf8')
|
||
.trimEnd()
|
||
.split('\n')
|
||
const builtAll = readFileSync(join(INV, `built-${lang}.txt`), 'utf8')
|
||
.trimEnd()
|
||
.split('\n')
|
||
const legacy = narrativeWindow(legacyAll, `legacy-${lang}.txt`)
|
||
const built = narrativeWindow(builtAll, `built-${lang}.txt`)
|
||
|
||
const excludedPending = [...EXCLUDED[lang]]
|
||
const stats = { labels: 0, excluded: [], quotes: 0, spacing: [], unexplained: [] }
|
||
|
||
let i = 0
|
||
let j = 0
|
||
const L = legacy.body
|
||
const B = built.body.map(unquote)
|
||
const Braw = built.body
|
||
while (i < L.length || j < B.length) {
|
||
const a = L[i]
|
||
const b = B[j]
|
||
if (a !== undefined && b !== undefined && a === b) {
|
||
if (Braw[j] !== b) stats.quotes++
|
||
i += 1
|
||
j += 1
|
||
continue
|
||
}
|
||
if (a !== undefined && b !== undefined && a.replace(/\s+/g, '') === b.replace(/\s+/g, '')) {
|
||
if (Braw[j] !== b) stats.quotes++
|
||
stats.spacing.push(a.length > 90 ? a.slice(0, 90) + '…' : a)
|
||
i += 1
|
||
j += 1
|
||
continue
|
||
}
|
||
if (b !== undefined && LABELS[lang].has(b)) {
|
||
stats.labels++
|
||
j++
|
||
continue
|
||
}
|
||
if (a !== undefined && excludedPending.includes(a)) {
|
||
excludedPending.splice(excludedPending.indexOf(a), 1)
|
||
stats.excluded.push(a)
|
||
i++
|
||
continue
|
||
}
|
||
stats.unexplained.push({ line: i + 1, legacy: a ?? '(end)', built: b ?? '(end)' })
|
||
failed = true
|
||
if (stats.unexplained.length > 10) break
|
||
i += 1
|
||
j += 1
|
||
}
|
||
|
||
report[lang] = {
|
||
legacy: { chromeHead: legacy.head.length, narrative: L.length, chromeTail: legacy.tail.length },
|
||
built: { chromeHead: built.head.length, narrative: B.length, chromeTail: built.tail.length },
|
||
matchedAfterCategories: !stats.unexplained.length,
|
||
toolCardLabelLines: stats.labels,
|
||
deferredLegislativeLines: stats.excluded,
|
||
linesWithSmartQuotes: stats.quotes,
|
||
tagBoundarySpacingLines: stats.spacing.length,
|
||
tagBoundarySpacingSamples: stats.spacing.slice(0, 8),
|
||
unexplained: stats.unexplained,
|
||
}
|
||
}
|
||
|
||
writeFileSync(join(INV, 'compare-report.json'), JSON.stringify(report, null, 2) + '\n')
|
||
for (const [lang, r] of Object.entries(report)) {
|
||
console.log(
|
||
`${lang}: narrative legacy=${r.legacy.narrative} built=${r.built.narrative} | ` +
|
||
`labels=+${r.toolCardLabelLines} excluded=-${r.deferredLegislativeLines.length} ` +
|
||
`smartquotes=${r.linesWithSmartQuotes} spacing=${r.tagBoundarySpacingLines} ` +
|
||
`unexplained=${r.unexplained.length}`,
|
||
)
|
||
for (const u of r.unexplained) console.log(' MISMATCH', u)
|
||
}
|
||
process.exit(failed ? 1 : 0)
|