Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 4 additions & 49 deletions news/2026-05-15-propositions-ar.html

Large diffs are not rendered by default.

53 changes: 4 additions & 49 deletions news/2026-05-15-propositions-da.html

Large diffs are not rendered by default.

53 changes: 4 additions & 49 deletions news/2026-05-15-propositions-de.html

Large diffs are not rendered by default.

53 changes: 4 additions & 49 deletions news/2026-05-15-propositions-en.html

Large diffs are not rendered by default.

53 changes: 4 additions & 49 deletions news/2026-05-15-propositions-es.html

Large diffs are not rendered by default.

53 changes: 4 additions & 49 deletions news/2026-05-15-propositions-fi.html

Large diffs are not rendered by default.

53 changes: 4 additions & 49 deletions news/2026-05-15-propositions-fr.html

Large diffs are not rendered by default.

53 changes: 4 additions & 49 deletions news/2026-05-15-propositions-he.html

Large diffs are not rendered by default.

53 changes: 4 additions & 49 deletions news/2026-05-15-propositions-ja.html

Large diffs are not rendered by default.

53 changes: 4 additions & 49 deletions news/2026-05-15-propositions-ko.html

Large diffs are not rendered by default.

53 changes: 4 additions & 49 deletions news/2026-05-15-propositions-nl.html

Large diffs are not rendered by default.

53 changes: 4 additions & 49 deletions news/2026-05-15-propositions-no.html

Large diffs are not rendered by default.

53 changes: 4 additions & 49 deletions news/2026-05-15-propositions-sv.html

Large diffs are not rendered by default.

53 changes: 4 additions & 49 deletions news/2026-05-15-propositions-zh.html

Large diffs are not rendered by default.

22 changes: 16 additions & 6 deletions scripts/render-lib/article-brief-lead.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,22 @@ import {
} from './aggregator/cleaning/structural.js';

/**
* Title-cased single-segment language codes for the 13 non-English
* locales, matching `prettifyFallbackTitle('executive-brief_<lang>.md')`
* in `aggregator/order.ts` (e.g. `sv` → `Sv`, `no` → `No`, `zh` → `Zh`).
* English is excluded — its brief renders as `## What Happened`, never as
* a `## Executive Brief <Lang>` carrier.
* Title-cased single-segment language codes for **all 14** locales,
* matching `prettifyFallbackTitle('executive-brief_<lang>.md')` in
* `aggregator/order.ts` (e.g. `sv` → `Sv`, `no` → `No`, `zh` → `Zh`,
* `en` → `En`).
*
* English (`En`) is intentionally included: the canonical English brief
* renders as the `## What Happened` lead (or the legacy `## Executive
* Brief` heading with **no** language suffix), so a suffixed `## Executive
* Brief En` heading is always a stray `executive-brief_en.md` carrier — it
* must be stripped just like the 13 localized carriers. Without it, an
* `executive-brief_en.md` artifact (e.g. when aggregating a single language
* in isolation) leaks an `Executive Brief En` heading into the rendered
* TOC. The `\b` boundary after the suffix means the legacy unsuffixed
* `## Executive Brief` lead is never matched.
*/
const LOCALIZED_BRIEF_TITLE_SUFFIXES: readonly string[] = LANGUAGES
.filter((l) => l !== 'en')
.map((l) => l.charAt(0).toUpperCase() + l.slice(1));

/**
Expand All @@ -70,6 +78,8 @@ const LOCALIZED_BRIEF_TITLE_SUFFIXES: readonly string[] = LANGUAGES
* next `<h2>`. Mirrors the line-anchored sweep used by
* `stripBodyDuplicateSections` so `###`/`# `/code-fence lines inside the
* section are consumed while the next `## ` boundary stops the match.
* The required `<Lang>` suffix means the canonical unsuffixed
* `## Executive Brief` lead heading is preserved.
*/
const EMBEDDED_BRIEF_SECTION_RE = new RegExp(
String.raw`^##\s+Executive Brief (?:${LOCALIZED_BRIEF_TITLE_SUFFIXES.join('|')})\b[^\n]*\n(?:(?!^##\s)[^\n]*\n?)*`,
Expand Down
407 changes: 407 additions & 0 deletions scripts/render-lib/section-title-i18n.ts

Large diffs are not rendered by default.

65 changes: 65 additions & 0 deletions tests/article-brief-lead.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,41 @@ describe('stripEmbeddedLocalizedBriefSections', () => {
expect(stripEmbeddedLocalizedBriefSections(body)).toContain('Plain English lead.');
expect(stripEmbeddedLocalizedBriefSections(body)).not.toMatch(/## Executive Brief/);
});

it('strips a stray `## Executive Brief En` carrier (executive-brief_en.md) so it never leaks into the TOC', () => {
const body = [
'## What Happened',
'',
'Canonical English lead.',
'',
'## Executive Brief En',
'<!-- source: executive-brief_en.md -->',
'',
'### Summary',
'',
'Stray English carrier body.',
'',
'## Risk Assessment',
'',
'Body.',
'',
].join('\n');
const out = stripEmbeddedLocalizedBriefSections(body);
expect(out).not.toMatch(/## Executive Brief En/);
expect(out).not.toContain('Stray English carrier body.');
// Canonical lead and analytical section survive.
expect(out).toContain('## What Happened');
expect(out).toContain('Canonical English lead.');
expect(out).toContain('## Risk Assessment');
});

it('preserves the legacy unsuffixed `## Executive Brief` lead heading (no language suffix)', () => {
const body = '## Executive Brief\n\nEnglish legacy lead.\n\n## Risk Assessment\n\nBody.\n';
const out = stripEmbeddedLocalizedBriefSections(body);
expect(out).toContain('## Executive Brief');
expect(out).toContain('English legacy lead.');
expect(out).toContain('## Risk Assessment');
});
});

describe('localizeExecutiveBriefLead', () => {
Expand All @@ -104,6 +139,36 @@ describe('localizeExecutiveBriefLead', () => {
expect(out).not.toContain('Busch-regeringen lämnade sju propositioner.');
});

it('English: also strips a stray `## Executive Brief En` carrier while keeping the canonical lead', () => {
const body = [
'## What Happened',
'',
"Sweden's Busch government submitted seven propositions.",
'',
'## Executive Brief En',
'',
'### Summary',
'',
'Stray English carrier body.',
'',
'## Risk Assessment',
'',
'Three measures carry constitutional review risk.',
'',
].join('\n');
const out = localizeExecutiveBriefLead({
content: body,
lang: 'en',
localizedBriefMarkdown: undefined,
subfolderRepoRelPath: 'analysis/daily/2026-05-20/propositions',
});
expect(out).not.toMatch(/## Executive Brief/);
expect(out).not.toContain('Stray English carrier body.');
expect(out).toContain('## What Happened');
expect(out).toContain("Sweden's Busch government submitted seven propositions.");
expect(out).toContain('## Risk Assessment');
});

it('non-English with a brief: swaps the lead body to localized prose under the stable H2', () => {
const out = localizeExecutiveBriefLead({
content: ARTICLE_BODY,
Expand Down
100 changes: 100 additions & 0 deletions tests/section-title-i18n.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,89 @@ describe('localizedSectionTitle', () => {
'deep-dive-methodology--limitations',
'deep-dive-data-download-manifest',
'analysis-artifact-coverage-report',
// Secondary analysis artifacts + recurring journalist sections added for
// full 14-language TOC coverage.
'deep-dive-political-classification',
'pestle-analysis',
'quantitative-swot',
'wildcards--black-swans',
'political-stride-assessment',
'cycle-trajectory',
'election-cycle-analysis',
'parliamentary-season-outlook',
'horizon-pir-roll-forward',
'actor-analysis',
'actor-assessment',
'actor-network',
'civil-society-analysis',
'coalition-stability',
'coalition-dynamics',
'coalition-implications',
'defence-policy-analysis',
'defence-security',
'economic-policy-analysis',
'economic-context',
'economic-impact',
'election-proximity-analysis',
'electoral-implications',
'electoral-analysis',
'electoral-forecast',
'infrastructure-analysis',
'international-context',
'geopolitical-context',
'eu-context',
'comparative-context',
'comparative-analysis',
'media-narrative-analysis',
'media-narrative',
'media-framing',
'opposition-mapping',
'opposition-analysis',
'opposition-response',
'policy-implications',
'policy-impact',
'policy-domain-analysis',
'social-welfare-analysis',
'strategic-intelligence-brief',
'strategic-implications',
'timeline-analysis',
'key-developments',
'key-actors',
'party-positions',
'political-landscape',
'public-opinion',
'historical-baseline',
'historical-context',
'horizon-assessment',
'intelligence-gaps',
'information-gaps',
'institutional-constraints',
'confidence-calibration',
'confidence-assessment',
'risk-register',
'risk-indicators',
'scenario-tree',
'forward-look',
'network-analysis',
'trend-analysis',
'voting-analysis',
'committee-analysis',
'legislative-calendar',
'stakeholder-mapping',
'stakeholder-map',
'methodology-notes',
'source-registry',
'source-inventory',
'source-quality',
'document-registry',
'analysis-index',
'reference-analysis-quality',
'workflow-audit',
'mcp-reliability-audit',
'cross-session-intelligence',
'cross-run-diff',
'session-baseline',
'diw-scores',
];

it('returns a non-empty localised title for every mapped slug × language', () => {
Expand Down Expand Up @@ -76,6 +159,23 @@ describe('localizedSectionTitle', () => {
);
});

it('localises recurring journalist topical sections (Korean regression)', () => {
// These previously fell back to English in non-English article TOCs
// (see the cited 2026-05-22 interpellations Korean article).
expect(localizedSectionTitle('actor-analysis', 'ko')).toBe('행위자 분석');
expect(localizedSectionTitle('defence-policy-analysis', 'ko')).toBe('국방 정책 분석');
expect(localizedSectionTitle('strategic-intelligence-brief', 'ko')).toBe('전략 정보 브리핑');
expect(localizedSectionTitle('timeline-analysis', 'ko')).toBe('타임라인 분석');
expect(localizedSectionTitle('international-context', 'ja')).toBe('国際的背景');
expect(localizedSectionTitle('social-welfare-analysis', 'de')).toBe('Sozialstaatsanalyse');
});

it('reuses the vetted artifact translation for the stakeholder-map slug', () => {
// Delegated through SLUG_TO_ARTIFACT_FILE → ARTIFACT_TITLE_I18N.
expect(localizedSectionTitle('stakeholder-map', 'en')).toBe('Stakeholder Map');
expect(localizedSectionTitle('stakeholder-map', 'sv')).toBe('Intressentkarta');
});

it('returns undefined for slugs without a curated localisation', () => {
expect(localizedSectionTitle('quux-unknown', 'sv')).toBeUndefined();
expect(localizedSectionTitle('', 'en')).toBeUndefined();
Expand Down
Loading