euparliamentmonitor 0.9.27 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/package.json +3 -3
  2. package/scripts/aggregator/html/localize-body.d.ts +28 -4
  3. package/scripts/aggregator/html/localize-body.js +79 -21
  4. package/scripts/aggregator/html/shell.js +8 -5
  5. package/scripts/aggregator/html/toc.js +4 -2
  6. package/scripts/aggregator/metadata/artifact-category-heading.js +8 -1
  7. package/scripts/aggregator/metadata/heading-rules.js +11 -0
  8. package/scripts/aggregator/metadata/seo-budgets.js +12 -9
  9. package/scripts/aggregator/progressive-disclosure.d.ts +2 -1
  10. package/scripts/aggregator/progressive-disclosure.js +8 -4
  11. package/scripts/aggregator/reader-friendly-transform.js +1 -1
  12. package/scripts/constants/languages.d.ts +2 -1
  13. package/scripts/constants/languages.js +1 -1
  14. package/scripts/constants/ui/index.d.ts +2 -0
  15. package/scripts/constants/ui/index.js +2 -0
  16. package/scripts/constants/ui/progressive-disclosure.d.ts +40 -0
  17. package/scripts/constants/ui/progressive-disclosure.js +150 -0
  18. package/scripts/discover-untranslated-briefs.js +296 -1
  19. package/scripts/generators/news-indexes/backfill-hreflang.d.ts +13 -0
  20. package/scripts/generators/news-indexes/backfill-hreflang.js +112 -0
  21. package/scripts/generators/news-indexes/backfill-reader-label.d.ts +47 -0
  22. package/scripts/generators/news-indexes/backfill-reader-label.js +86 -0
  23. package/scripts/generators/news-indexes/backfill.d.ts +19 -18
  24. package/scripts/generators/news-indexes/backfill.js +118 -111
  25. package/scripts/generators/news-indexes/per-language.js +2 -1
  26. package/scripts/generators/political-intelligence/html.js +2 -1
  27. package/scripts/generators/sitemap/html.js +2 -1
  28. package/scripts/generators/sitemap/index.d.ts +1 -1
  29. package/scripts/generators/sitemap/index.js +1 -1
  30. package/scripts/generators/sitemap/rss.d.ts +38 -2
  31. package/scripts/generators/sitemap/rss.js +54 -10
  32. package/scripts/generators/sitemap/xml.js +21 -6
  33. package/scripts/generators/sitemap.js +42 -9
  34. package/scripts/mcp/ep/error-classifier.d.ts +38 -0
  35. package/scripts/mcp/ep/error-classifier.js +49 -0
  36. package/scripts/mcp/ep/tools-feeds.js +27 -2
  37. package/scripts/templates/sections/footer.js +3 -1
  38. package/scripts/templates/sections/rss-discovery.d.ts +22 -0
  39. package/scripts/templates/sections/rss-discovery.js +48 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "euparliamentmonitor",
3
- "version": "0.9.27",
3
+ "version": "1.0.0",
4
4
  "type": "module",
5
5
  "description": "European Parliament Intelligence Platform - Monitor political activity with systematic transparency",
6
6
  "main": "scripts/index.js",
@@ -167,7 +167,7 @@
167
167
  "clean-css": "^5.3.3",
168
168
  "d3": "7.9.0",
169
169
  "esbuild": "0.28.0",
170
- "eslint": "10.4.0",
170
+ "eslint": "10.4.1",
171
171
  "eslint-config-prettier": "10.1.8",
172
172
  "eslint-plugin-jsdoc": "63.0.0",
173
173
  "eslint-plugin-security": "4.0.0",
@@ -179,7 +179,7 @@
179
179
  "husky": "9.1.7",
180
180
  "jscpd": "4.2.4",
181
181
  "knip": "^6.7.0",
182
- "lint-staged": "17.0.5",
182
+ "lint-staged": "17.0.6",
183
183
  "mermaid": "11.15.0",
184
184
  "papaparse": "5.5.3",
185
185
  "prettier": "3.8.3",
@@ -22,11 +22,32 @@ export declare function localizeArticleBody(bodyHtml: string, lang: LanguageCode
22
22
  * @returns Modified string, or `haystack` unchanged when `needle` is absent
23
23
  */
24
24
  export declare function replaceFirstStringIn(haystack: string, needle: string, replacement: string): string;
25
+ /**
26
+ * Locate the cut point that ends the Executive Brief body — the start of
27
+ * the next top-level boundary heading after `afterHeading`. A boundary is
28
+ * any `<h2>` whose `id` either starts with the canonical `section-` prefix
29
+ * or exactly matches one of {@link EXECUTIVE_BRIEF_BOUNDARY_ID_MARKERS}
30
+ * (Reader Guide / Tradecraft / Analysis Index / Supplementary appendices).
31
+ *
32
+ * Critically, this only matches **top-level** section anchors — never the
33
+ * brief's own internal `<h2>` sub-headings (`## BLUF`, `## 60-Second Read`,
34
+ * …), which carry slugified ids without the `section-` prefix. That is why
35
+ * we cannot simply look for the next `<h2`.
36
+ *
37
+ * Uses `indexOf`/`lastIndexOf` exclusively (no regex) to stay within
38
+ * CodeQL's safe-regex envelope.
39
+ *
40
+ * @param html - Full article body HTML
41
+ * @param afterHeading - Index immediately after the Executive Brief `</h2>`
42
+ * @returns Index of the next boundary `<h2`, or `-1` when the Executive
43
+ * Brief is the last block in the body.
44
+ */
45
+ export declare function findExecutiveBriefSectionCut(html: string, afterHeading: number): number;
25
46
  /**
26
47
  * Replace the **inner body** of the Executive Brief section (the
27
48
  * `<h2 id="section-executive-brief">…</h2>` heading and everything that
28
- * follows it up to — but not including — the next `<h2 id="section-…">`
29
- * sibling) with the supplied replacement HTML. The Executive Brief
49
+ * follows it up to — but not including — the next top-level boundary
50
+ * heading) with the supplied replacement HTML. The Executive Brief
30
51
  * heading itself is preserved by emitting it inline ahead of the
31
52
  * replacement, so the in-page anchor (`#section-executive-brief`) and
32
53
  * the table-of-contents link continue to work.
@@ -39,8 +60,11 @@ export declare function replaceFirstStringIn(haystack: string, needle: string, r
39
60
  * `render-one.writeLanguageVariant`.
40
61
  *
41
62
  * Implementation uses `indexOf`/slice exclusively to stay within
42
- * CodeQL's safe-regex envelope. Returns `html` unchanged when the
43
- * Executive Brief heading is absent or malformed.
63
+ * CodeQL's safe-regex envelope. The replacement spans from the heading to
64
+ * the next top-level boundary (see {@link findExecutiveBriefSectionCut});
65
+ * when the Executive Brief is the last block in the body the replacement
66
+ * extends to end-of-body. Returns `html` unchanged only when the Executive
67
+ * Brief heading is absent or malformed.
44
68
  *
45
69
  * @param html - Full article body HTML
46
70
  * @param localizedHeading - Localized text for the Executive Brief H2
@@ -12,6 +12,23 @@ import { TRADECRAFT_HEADING_LABELS, TRADECRAFT_INTRO_LABELS, TRADECRAFT_METHODOL
12
12
  import { escapeHTML } from '../../utils/file-utils.js';
13
13
  import { TRADECRAFT_SECTION_ID, MANIFEST_SECTION_ID, SUPPLEMENTARY_SECTION_ID, } from '../artifact-order.js';
14
14
  import { KEY_TAKEAWAYS_SECTION_ID } from '../key-takeaways.js';
15
+ import { READER_GUIDE_SECTION_ID } from '../reader-guide-constants.js';
16
+ /**
17
+ * Top-level section anchors that mark the **end** of the Executive Brief
18
+ * body. Canonical analysis sections are matched by the shared
19
+ * `id="section-…"` prefix (see {@link findExecutiveBriefSectionCut});
20
+ * the appendix and reader-guide sections below carry bespoke ids that do
21
+ * **not** share that prefix, so they are matched explicitly. Including
22
+ * them ensures the localized brief splice also fires on sparse runs where
23
+ * the Executive Brief is the last canonical section and only appendix
24
+ * blocks follow it.
25
+ */
26
+ const EXECUTIVE_BRIEF_BOUNDARY_ID_MARKERS = [
27
+ `id="${READER_GUIDE_SECTION_ID}"`,
28
+ `id="${TRADECRAFT_SECTION_ID}"`,
29
+ `id="${MANIFEST_SECTION_ID}"`,
30
+ `id="${SUPPLEMENTARY_SECTION_ID}"`,
31
+ ];
15
32
  /**
16
33
  * Localize the Tradecraft References and Analysis Index sections in the
17
34
  * rendered article body HTML. Replaces English headings, introductions,
@@ -102,11 +119,48 @@ export function replaceFirstStringIn(haystack, needle, replacement) {
102
119
  return haystack;
103
120
  return haystack.slice(0, idx) + replacement + haystack.slice(idx + needle.length);
104
121
  }
122
+ /**
123
+ * Locate the cut point that ends the Executive Brief body — the start of
124
+ * the next top-level boundary heading after `afterHeading`. A boundary is
125
+ * any `<h2>` whose `id` either starts with the canonical `section-` prefix
126
+ * or exactly matches one of {@link EXECUTIVE_BRIEF_BOUNDARY_ID_MARKERS}
127
+ * (Reader Guide / Tradecraft / Analysis Index / Supplementary appendices).
128
+ *
129
+ * Critically, this only matches **top-level** section anchors — never the
130
+ * brief's own internal `<h2>` sub-headings (`## BLUF`, `## 60-Second Read`,
131
+ * …), which carry slugified ids without the `section-` prefix. That is why
132
+ * we cannot simply look for the next `<h2`.
133
+ *
134
+ * Uses `indexOf`/`lastIndexOf` exclusively (no regex) to stay within
135
+ * CodeQL's safe-regex envelope.
136
+ *
137
+ * @param html - Full article body HTML
138
+ * @param afterHeading - Index immediately after the Executive Brief `</h2>`
139
+ * @returns Index of the next boundary `<h2`, or `-1` when the Executive
140
+ * Brief is the last block in the body.
141
+ */
142
+ export function findExecutiveBriefSectionCut(html, afterHeading) {
143
+ let best = -1;
144
+ const consider = (markerIdx) => {
145
+ if (markerIdx === -1)
146
+ return;
147
+ const h2 = html.lastIndexOf('<h2', markerIdx);
148
+ if (h2 === -1 || h2 < afterHeading)
149
+ return;
150
+ if (best === -1 || h2 < best)
151
+ best = h2;
152
+ };
153
+ consider(html.indexOf('id="section-', afterHeading));
154
+ for (const marker of EXECUTIVE_BRIEF_BOUNDARY_ID_MARKERS) {
155
+ consider(html.indexOf(marker, afterHeading));
156
+ }
157
+ return best;
158
+ }
105
159
  /**
106
160
  * Replace the **inner body** of the Executive Brief section (the
107
161
  * `<h2 id="section-executive-brief">…</h2>` heading and everything that
108
- * follows it up to — but not including — the next `<h2 id="section-…">`
109
- * sibling) with the supplied replacement HTML. The Executive Brief
162
+ * follows it up to — but not including — the next top-level boundary
163
+ * heading) with the supplied replacement HTML. The Executive Brief
110
164
  * heading itself is preserved by emitting it inline ahead of the
111
165
  * replacement, so the in-page anchor (`#section-executive-brief`) and
112
166
  * the table-of-contents link continue to work.
@@ -119,8 +173,11 @@ export function replaceFirstStringIn(haystack, needle, replacement) {
119
173
  * `render-one.writeLanguageVariant`.
120
174
  *
121
175
  * Implementation uses `indexOf`/slice exclusively to stay within
122
- * CodeQL's safe-regex envelope. Returns `html` unchanged when the
123
- * Executive Brief heading is absent or malformed.
176
+ * CodeQL's safe-regex envelope. The replacement spans from the heading to
177
+ * the next top-level boundary (see {@link findExecutiveBriefSectionCut});
178
+ * when the Executive Brief is the last block in the body the replacement
179
+ * extends to end-of-body. Returns `html` unchanged only when the Executive
180
+ * Brief heading is absent or malformed.
124
181
  *
125
182
  * @param html - Full article body HTML
126
183
  * @param localizedHeading - Localized text for the Executive Brief H2
@@ -147,23 +204,24 @@ export function replaceExecutiveBriefSection(html, localizedHeading, replacement
147
204
  if (h2CloseTagIdx === -1)
148
205
  return html;
149
206
  const afterHeading = h2CloseTagIdx + '</h2>'.length;
150
- // Find the next `<h2 id="section-...">` boundary — the start of the
151
- // following article section. If there is no further section heading
152
- // we conservatively bail out (replacing through end-of-body would
153
- // also drop appendix content like Reader Guide / Key Takeaways).
154
- const nextSectionId = html.indexOf('id="section-', afterHeading);
155
- if (nextSectionId === -1)
156
- return html;
157
- const nextH2 = html.lastIndexOf('<h2', nextSectionId);
158
- if (nextH2 === -1 || nextH2 <= afterHeading)
159
- return html;
160
- // Find the start of the line containing the next `<h2` so we don't
161
- // strip leading whitespace from the next section. We look at most
162
- // one newline back.
163
- let cutEnd = nextH2;
164
- const prevNewline = html.lastIndexOf('\n', nextH2 - 1);
165
- if (prevNewline !== -1 && prevNewline >= afterHeading) {
166
- cutEnd = prevNewline + 1;
207
+ // Find the next top-level boundary heading — the start of the following
208
+ // article section or appendix. When none exists the Executive Brief is
209
+ // the last block, so we replace through end-of-body. This guarantees the
210
+ // localized brief is spliced even on sparse runs (previously the splice
211
+ // bailed and non-English readers were stranded on the English brief).
212
+ const nextH2 = findExecutiveBriefSectionCut(html, afterHeading);
213
+ let cutEnd;
214
+ if (nextH2 === -1) {
215
+ cutEnd = html.length;
216
+ }
217
+ else {
218
+ // Start of the line containing the next `<h2` so we don't strip
219
+ // leading whitespace from the next section.
220
+ cutEnd = nextH2;
221
+ const prevNewline = html.lastIndexOf('\n', nextH2 - 1);
222
+ if (prevNewline !== -1 && prevNewline >= afterHeading) {
223
+ cutEnd = prevNewline + 1;
224
+ }
167
225
  }
168
226
  const newHeading = `<h2 id="section-executive-brief">${escapeHTML(localizedHeading)}</h2>\n`;
169
227
  const trimmedReplacement = replacementBodyHtml.endsWith('\n')
@@ -12,13 +12,14 @@
12
12
  */
13
13
  import { BASE_URL, BUILD_SHORT, MERMAID_VERSION } from '../../constants/config.js';
14
14
  import { buildHeadFreshnessTags } from '../../constants/build-info-meta.js';
15
- import { ALL_LANGUAGES, PAGE_TITLES, SKIP_LINK_TEXTS, ARTICLE_NAV_LABELS, BACK_TO_NEWS_LABELS, VIEW_SOURCE_MARKDOWN_LABELS, FOOTER_SITEMAP_LABELS, FOOTER_POLITICAL_INTELLIGENCE_LABELS, getLocalizedString, getTextDirection, } from '../../constants/languages.js';
15
+ import { ALL_LANGUAGES, PAGE_TITLES, SKIP_LINK_TEXTS, ARTICLE_NAV_LABELS, BACK_TO_NEWS_LABELS, VIEW_SOURCE_MARKDOWN_LABELS, FOOTER_SITEMAP_LABELS, FOOTER_POLITICAL_INTELLIGENCE_LABELS, PROGRESSIVE_DISCLOSURE_LABELS, getLocalizedString, getTextDirection, } from '../../constants/languages.js';
16
16
  import { buildOgLocaleTags } from '../../constants/og-locales.js';
17
17
  import { ORG_SAME_AS, buildTwitterAttributionTags } from '../../constants/social-handles.js';
18
18
  import { escapeHTML } from '../../utils/file-utils.js';
19
19
  import { buildResponsiveIconLinks, buildResponsiveSocialImageMeta, buildSiteFooter, buildSiteHeader, buildPageBanner, } from '../../templates/section-builders.js';
20
20
  import { getPoliticalIntelligenceFilename } from '../../generators/political-intelligence.js';
21
21
  import { getSitemapFilename } from '../../generators/sitemap/index.js';
22
+ import { buildRssAlternateLink } from '../../templates/sections/rss-discovery.js';
22
23
  import { truncateHeadline, getTitleSeparator, buildPageTitle, getLocalizedArticleType, getLocalizedArticleTypePlain, } from './headline.js';
23
24
  import { clampForBudget } from '../metadata/seo-budgets.js';
24
25
  import { getArticleFilename, buildArticleHreflangLinks, buildLanguageSwitcher, } from './hreflang.js';
@@ -186,7 +187,7 @@ export function wrapArticleHtml(options) {
186
187
  const tocHtml = buildArticleToc(options.toc ?? [], safeLang);
187
188
  const articleMainClass = tocHtml.length > 0 ? 'article-main--with-toc' : 'article-main--no-toc';
188
189
  const articleSectionLabel = getLocalizedArticleTypePlain(options.articleType, safeLang);
189
- const disclosureBody = buildProgressiveDisclosureBody(options.body);
190
+ const disclosureBody = buildProgressiveDisclosureBody(options.body, safeLang);
190
191
  const transformedBodyHtml = options.readerFriendly === false
191
192
  ? disclosureBody.bodyHtml
192
193
  : applyReaderFriendlyTransform(disclosureBody.bodyHtml);
@@ -199,7 +200,9 @@ export function wrapArticleHtml(options) {
199
200
  disclosureBody.wordCounts.analysis +
200
201
  disclosureBody.wordCounts.intelligence;
201
202
  const readingTimes = options.readingTimes ?? buildLayerReadingTimes(disclosureBody.wordCounts);
202
- const readingTimeLine = `⏱️ Quick read: ${readingTimes.quickRead} min · Full analysis: ${readingTimes.fullAnalysis} min · Complete intelligence: ${readingTimes.completeIntelligence} min`;
203
+ const disclosureLabels = getLocalizedString(PROGRESSIVE_DISCLOSURE_LABELS, safeLang);
204
+ const min = disclosureLabels.minutesAbbr;
205
+ const readingTimeLine = `⏱️ ${disclosureLabels.quickRead}: ${readingTimes.quickRead}${min} · ${disclosureLabels.fullAnalysis}: ${readingTimes.fullAnalysis}${min} · ${disclosureLabels.completeIntelligence}: ${readingTimes.completeIntelligence}${min}`;
203
206
  // Pre-compute the per-surface SEO-budget-clamped variants of title
204
207
  // and description. Each surface gets its own clamp tuned to the
205
208
  // documented platform envelope (Google/Bing SERP, Facebook/LinkedIn
@@ -350,7 +353,7 @@ ${keywordsMeta} <meta name="robots" content="index, follow, max-snippet:-1, max
350
353
  <meta property="article:publisher" content="https://hack23.com">
351
354
  <link rel="canonical" href="${canonicalUrl}">
352
355
  ${hreflangLinks}
353
- <link rel="alternate" type="application/rss+xml" title="EU Parliament Monitor RSS" href="${BASE_URL}/rss.xml">
356
+ ${buildRssAlternateLink(safeLang, `${BASE_URL}/`)}
354
357
  <link rel="preconnect" href="https://hack23.com" crossorigin>
355
358
  <meta property="og:type" content="article">
356
359
  <meta property="og:title" content="${escapeHTML(ogTitleClamped)}">
@@ -392,7 +395,7 @@ ${tocHtml} <article class="article-body" lang="${safeLang}">
392
395
  <p class="article-kicker">${escapeHTML(getLocalizedArticleType(options.articleType, safeLang))}</p>
393
396
  <h1>${escapeHTML(options.title)}</h1>
394
397
  <p class="article-dek">${escapeHTML(options.description)}</p>
395
- <p class="article-reading-times" aria-label="Estimated reading time">${escapeHTML(readingTimeLine)}</p>
398
+ <p class="article-reading-times" aria-label="${escapeHTML(disclosureLabels.readingTimeAria)}">${escapeHTML(readingTimeLine)}</p>
396
399
  <p class="article-meta"><time datetime="${options.date}">${options.date}</time> · EU Parliament Monitor</p>
397
400
  </header>
398
401
  ${sourceMdLink}
@@ -8,7 +8,7 @@
8
8
  * that mirrors the Reader Intelligence Guide so the two navigation
9
9
  * surfaces share a single visual vocabulary.
10
10
  */
11
- import { TOC_ARIA_LABELS, TRADECRAFT_HEADING_LABELS, ANALYSIS_INDEX_HEADING_LABELS, KEY_TAKEAWAYS_HEADING_LABELS, SUPPLEMENTARY_HEADING_LABELS, SECTION_TITLE_LABELS, getLocalizedString, } from '../../constants/languages.js';
11
+ import { TOC_ARIA_LABELS, TRADECRAFT_HEADING_LABELS, ANALYSIS_INDEX_HEADING_LABELS, KEY_TAKEAWAYS_HEADING_LABELS, SUPPLEMENTARY_HEADING_LABELS, SECTION_TITLE_LABELS, PROGRESSIVE_DISCLOSURE_LABELS, getLocalizedString, } from '../../constants/languages.js';
12
12
  import { escapeHTML } from '../../utils/file-utils.js';
13
13
  import { READER_GUIDE_SECTION_ID } from '../reader-guide-constants.js';
14
14
  import { READER_GUIDE_TITLE_LABELS, getReaderGuideSectionIcon, } from '../reader-intelligence-guide.js';
@@ -91,13 +91,15 @@ export function buildArticleToc(entries, lang) {
91
91
  if (entries.length === 0)
92
92
  return '';
93
93
  const label = escapeHTML(getLocalizedString(TOC_ARIA_LABELS, lang));
94
+ const layerBadgeWord = getLocalizedString(PROGRESSIVE_DISCLOSURE_LABELS, lang).layerBadge;
94
95
  const items = entries
95
96
  .map((e) => {
96
97
  const displayTitle = getLocalizedTocTitle(e.id, e.title, lang);
97
98
  const icon = getTocSectionIcon(e.id);
98
99
  const layer = resolveDisclosureLayer(e.id);
99
100
  const layerBadge = layer === 'quick' ? 'L1' : layer === 'analysis' ? 'L2' : 'L3';
100
- return ` <li data-layer="${layer}"><a href="#${escapeHTML(e.id)}"><span class="article-toc-icon" aria-hidden="true">${icon}</span> <span class="article-toc-text">${escapeHTML(displayTitle)}</span><span class="article-toc-layer article-toc-layer--${layer}" aria-label="Layer ${layerBadge}">${layerBadge}</span></a></li>`;
101
+ const layerAria = escapeHTML(`${layerBadgeWord} ${layerBadge}`);
102
+ return ` <li data-layer="${layer}"><a href="#${escapeHTML(e.id)}"><span class="article-toc-icon" aria-hidden="true">${icon}</span> <span class="article-toc-text">${escapeHTML(displayTitle)}</span><span class="article-toc-layer article-toc-layer--${layer}" aria-label="${layerAria}">${layerBadge}</span></a></li>`;
101
103
  })
102
104
  .join('\n');
103
105
  return [
@@ -153,6 +153,13 @@ export const ARTIFACT_CATEGORY_PREFIXES = [
153
153
  'voting patterns',
154
154
  'weekly outlook',
155
155
  'wildcards blackswans',
156
+ // CJK localized category prefixes (translations of "executive briefing")
157
+ 'エグゼクティブ・ブリーフィング',
158
+ 'エグゼクティブブリーフィング',
159
+ 'エグゼクティブ・ブリーフ',
160
+ '행정 브리핑',
161
+ '执行简报',
162
+ '執行簡報',
156
163
  ];
157
164
  /**
158
165
  * Match a single calendar month name (English) with optional `-uary` /
@@ -211,7 +218,7 @@ function normaliseCategoryHeading(raw) {
211
218
  return stripInlineMarkdown(raw)
212
219
  .trim()
213
220
  .toLowerCase()
214
- .replace(/^[^a-z0-9]+/, '')
221
+ .replace(/^[^a-z0-9\p{L}]+/u, '')
215
222
  .replace(/\s+/g, ' ');
216
223
  }
217
224
  /**
@@ -158,6 +158,17 @@ const BARE_INSTITUTIONAL_HEADINGS = [
158
158
  'briefing',
159
159
  'intelligence brief',
160
160
  'intelligence briefing',
161
+ // CJK / localized translations of generic headings
162
+ 'エグゼクティブ・ブリーフィング',
163
+ 'エグゼクティブブリーフィング',
164
+ 'エグゼクティブ・ブリーフ',
165
+ 'ブリーフィング',
166
+ '행정 브리핑',
167
+ '브리핑',
168
+ '执行简报',
169
+ '简报',
170
+ '執行簡報',
171
+ '簡報',
161
172
  ];
162
173
  /**
163
174
  * Return `true` when the heading is one of {@link BARE_INSTITUTIONAL_HEADINGS}
@@ -160,15 +160,18 @@ export function clampForBudget(text, lang, surface) {
160
160
  if (cleaned.length >= softMin)
161
161
  return cleaned;
162
162
  }
163
- // Whitespace-aware fallback. Chinese and Japanese text often has no
164
- // ASCII spaces, so skip this step for them and fall straight through
165
- // to the hard cut. Korean is the exception it uses inter-word spaces.
166
- if (family !== 'cjk' || lang === 'ko') {
167
- const lastSpace = window.lastIndexOf(' ');
168
- if (lastSpace >= softMin) {
169
- const safe = trimTrailingSeparators(window.slice(0, lastSpace));
170
- return `${safe}…`;
171
- }
163
+ // Whitespace-aware fallback. Runs for every script: an ASCII space
164
+ // past the soft minimum is a safe break that drops a partial trailing
165
+ // segment whole rather than slicing it mid-token. Chinese and Japanese
166
+ // prose has no inter-word spaces, so `lastIndexOf(' ')` returns -1 and
167
+ // this is a no-op for them — but composed SEO snippets join clauses
168
+ // (body, dateline, reader label) with ASCII spaces, so honouring that
169
+ // boundary prevents hard-cutting the reader label mid-word. Korean
170
+ // uses inter-word spaces natively and benefits the same way.
171
+ const lastSpace = window.lastIndexOf(' ');
172
+ if (lastSpace >= softMin) {
173
+ const safe = trimTrailingSeparators(window.slice(0, lastSpace));
174
+ return `${safe}…`;
172
175
  }
173
176
  const hardCut = trimTrailingSeparators(window);
174
177
  return `${hardCut}…`;
@@ -1,3 +1,4 @@
1
+ import type { LanguageCode } from '../types/index.js';
1
2
  export type DisclosureLayer = 'quick' | 'analysis' | 'intelligence';
2
3
  export interface LayerWordCounts {
3
4
  readonly quick: number;
@@ -20,7 +21,7 @@ export declare function splitBodyIntoDisclosureLayers(bodyHtml: string): {
20
21
  };
21
22
  export declare function estimateReadingMinutes(wordCount: number): number;
22
23
  export declare function buildLayerReadingTimes(words: LayerWordCounts): LayerReadingTimes;
23
- export declare function buildProgressiveDisclosureBody(bodyHtml: string): {
24
+ export declare function buildProgressiveDisclosureBody(bodyHtml: string, lang?: LanguageCode): {
24
25
  readonly bodyHtml: string;
25
26
  readonly wordCounts: LayerWordCounts;
26
27
  };
@@ -1,6 +1,9 @@
1
1
  // SPDX-FileCopyrightText: 2024-2026 Hack23 AB
2
2
  // SPDX-License-Identifier: Apache-2.0
3
3
  import { stripHtmlTags } from '../utils/html-sanitize.js';
4
+ import { escapeHTML } from '../utils/file-utils.js';
5
+ import { PROGRESSIVE_DISCLOSURE_LABELS } from '../constants/languages.js';
6
+ import { getLocalizedString } from '../constants/language-core.js';
4
7
  export const QUICK_LAYER_SECTION_IDS = new Set([
5
8
  'executive-brief',
6
9
  'key-takeaways',
@@ -87,18 +90,19 @@ export function buildLayerReadingTimes(words) {
87
90
  completeIntelligence: estimateReadingMinutes(complete),
88
91
  };
89
92
  }
90
- export function buildProgressiveDisclosureBody(bodyHtml) {
93
+ export function buildProgressiveDisclosureBody(bodyHtml, lang = 'en') {
94
+ const labels = getLocalizedString(PROGRESSIVE_DISCLOSURE_LABELS, lang);
91
95
  const layers = splitBodyIntoDisclosureLayers(bodyHtml);
92
96
  const output = [
93
- `<section class="article-layer article-layer--quick" data-disclosure-layer="quick" aria-label="Quick read">`,
97
+ `<section class="article-layer article-layer--quick" data-disclosure-layer="quick" aria-label="${escapeHTML(labels.quickRead)}">`,
94
98
  layers.quickHtml,
95
99
  `</section>`,
96
100
  ];
97
101
  if (layers.analysisHtml.trim().length > 0) {
98
- output.push(`<details class="article-layer article-layer--analysis article-layer-details" data-disclosure-layer="analysis" id="article-layer-analysis">`, `<summary class="article-layer-summary"><span class="article-layer-summary__title">Read full analysis ↓</span></summary>`, `<section class="article-layer-content" aria-label="Full analysis">`, layers.analysisHtml, `</section>`, `</details>`);
102
+ output.push(`<details class="article-layer article-layer--analysis article-layer-details" data-disclosure-layer="analysis" id="article-layer-analysis">`, `<summary class="article-layer-summary"><span class="article-layer-summary__title">${escapeHTML(labels.expandAnalysis)} ↓</span></summary>`, `<section class="article-layer-content" aria-label="${escapeHTML(labels.fullAnalysis)}">`, layers.analysisHtml, `</section>`, `</details>`);
99
103
  }
100
104
  if (layers.intelligenceHtml.trim().length > 0) {
101
- output.push(`<details class="article-layer article-layer--intelligence article-layer-details" data-disclosure-layer="intelligence" id="article-layer-intelligence">`, `<summary class="article-layer-summary"><span class="article-layer-summary__title">Open complete intelligence ↓</span></summary>`, `<section class="article-layer-content" aria-label="Complete intelligence">`, layers.intelligenceHtml, `</section>`, `</details>`);
105
+ output.push(`<details class="article-layer article-layer--intelligence article-layer-details" data-disclosure-layer="intelligence" id="article-layer-intelligence">`, `<summary class="article-layer-summary"><span class="article-layer-summary__title">${escapeHTML(labels.expandIntelligence)} ↓</span></summary>`, `<section class="article-layer-content" aria-label="${escapeHTML(labels.completeIntelligence)}">`, layers.intelligenceHtml, `</section>`, `</details>`);
102
106
  }
103
107
  return { bodyHtml: output.join('\n'), wordCounts: layers.wordCounts };
104
108
  }
@@ -46,7 +46,7 @@ const ADMIRALTY_LABELS = {
46
46
  export function applyReaderFriendlyTransform(html) {
47
47
  const state = createInitialState(html);
48
48
  const withGlossary = injectReaderGlossary(html);
49
- const parts = withGlossary.split(/(<[^>]+>)/g);
49
+ const parts = withGlossary.split(/(<[^<>]+>)/g);
50
50
  for (let i = 0; i < parts.length; i++) {
51
51
  const part = parts[i] ?? '';
52
52
  if (part.startsWith('<')) {
@@ -8,7 +8,8 @@
8
8
  * - **language-articles** — Article-type title generators and body-text strings
9
9
  */
10
10
  export { ALL_LANGUAGES, LANGUAGE_PRESETS, LANGUAGE_FLAGS, LANGUAGE_NAMES, getLocalizedString, isSupportedLanguage, getTextDirection, } from './language-core.js';
11
- export { PAGE_TITLES, PAGE_DESCRIPTIONS, SECTION_HEADINGS, NO_ARTICLES_MESSAGES, SKIP_LINK_TEXTS, ARTICLE_TYPE_LABELS, READ_TIME_LABELS, BACK_TO_NEWS_LABELS, ARTICLE_NAV_LABELS, RELATED_ARTICLES_NAV_LABELS, BREADCRUMB_HOME_LABELS, BREADCRUMB_NEWS_LABELS, TIMELINE_HEADINGS, COMPARISON_BEFORE_LABELS, COMPARISON_AFTER_LABELS, KEY_FIGURES_HEADINGS, AI_SECTION_CONTENT, FILTER_LABELS, SOURCES_HEADING_LABELS, HEADER_SUBTITLE_LABELS, THEME_TOGGLE_LABELS, FOOTER_ABOUT_HEADING_LABELS, FOOTER_ABOUT_TEXT_LABELS, FOOTER_QUICK_LINKS_LABELS, FOOTER_BUILT_BY_LABELS, FOOTER_LANGUAGES_LABELS, FOOTER_HOME_LABELS, FOOTER_SITEMAP_LABELS, FOOTER_RSS_LABELS, FOOTER_GITHUB_REPO_LABELS, FOOTER_LICENSE_LABELS, FOOTER_EUROPARL_LABELS, FOOTER_LINKEDIN_LABELS, FOOTER_SECURITY_POLICY_LABELS, FOOTER_CONTACT_LABELS, FOOTER_DISCLAIMER_LABELS, FOOTER_REPORT_ISSUES_LABELS, FOOTER_ARTICLES_AVAILABLE_LABELS, FOOTER_POLITICAL_INTELLIGENCE_LABELS, TOC_ARIA_LABELS, TRADECRAFT_HEADING_LABELS, TRADECRAFT_INTRO_LABELS, TRADECRAFT_METHODOLOGIES_LABELS, TRADECRAFT_TEMPLATES_LABELS, ANALYSIS_INDEX_HEADING_LABELS, ANALYSIS_INDEX_INTRO_LABELS, ANALYSIS_INDEX_COL_SECTION_LABELS, ANALYSIS_INDEX_COL_ARTIFACT_LABELS, ANALYSIS_INDEX_COL_PATH_LABELS, KEY_TAKEAWAYS_HEADING_LABELS, SUPPLEMENTARY_HEADING_LABELS, SECTION_TITLE_LABELS, RELATED_ANALYSIS_LABELS, ANALYSIS_TRANSPARENCY_LABELS, ANALYSIS_SUMMARY_LABELS, METHODOLOGY_LABELS, TRANSPARENCY_DISCLOSURE_LABELS, CLASSIFICATION_ANALYSIS_LABELS, THREAT_ASSESSMENT_LABELS, RISK_SCORING_LABELS, DEEP_ANALYSIS_LABELS, VIEW_SOURCE_LABELS, VIEW_SOURCE_MARKDOWN_LABELS, ARTICLE_TYPE_ICONS, OPEN_SOURCE_NOTE_LABELS, AI_ANALYSIS_GUIDE_LABELS, SWOT_FRAMEWORK_LABELS, RISK_METHODOLOGY_LABELS, THREAT_FRAMEWORK_LABELS, CLASSIFICATION_GUIDE_LABELS, STYLE_GUIDE_LABELS, SIGNIFICANCE_CLASSIFICATION_LABELS, ACTOR_MAPPING_LABELS, FORCES_ANALYSIS_LABELS, IMPACT_MATRIX_LABELS, POLITICAL_THREAT_LANDSCAPE_LABELS, ACTOR_THREAT_PROFILING_LABELS, CONSEQUENCE_TREES_LABELS, LEGISLATIVE_DISRUPTION_LABELS, RISK_MATRIX_LABELS, QUANTITATIVE_SWOT_LABELS, POLITICAL_CAPITAL_RISK_LABELS, LEGISLATIVE_VELOCITY_RISK_LABELS, AGENT_RISK_WORKFLOW_LABELS, STAKEHOLDER_IMPACT_LABELS, COALITION_DYNAMICS_LABELS, VOTING_PATTERNS_LABELS, CROSS_SESSION_INTELLIGENCE_LABELS, SYNTHESIS_SUMMARY_LABELS, DOCUMENT_ANALYSIS_LABELS, SIGNIFICANCE_SCORING_LABELS, INSTALL_APP_LABELS, UPDATE_AVAILABLE_LABELS, UPDATE_REFRESH_CTA_LABELS, UPDATE_DISMISS_LABELS, OFFLINE_TITLE_LABELS, OFFLINE_BODY_LABELS, OFFLINE_RETRY_LABELS, BUILD_INFO_COMMIT_LABELS, BUILD_INFO_DEPLOYED_LABELS, HEADER_CTA_SPONSOR_LABELS, HEADER_CTA_BECOME_SPONSOR_LABELS, HEADER_CTA_SECURITY_LABELS, FOOTER_NEWS_LABELS, FOOTER_DASHBOARD_LABELS, FOOTER_ANALYSIS_REPORTS_LABELS, FOOTER_API_DOCS_LABELS, FOOTER_COMPANY_TAGLINE_LABELS, LANGUAGE_SELECTION_ARIA_LABELS, FOOTER_TRUST_BADGES_ARIA_LABELS, } from './language-ui.js';
11
+ export { PAGE_TITLES, PAGE_DESCRIPTIONS, SECTION_HEADINGS, NO_ARTICLES_MESSAGES, SKIP_LINK_TEXTS, ARTICLE_TYPE_LABELS, READ_TIME_LABELS, PROGRESSIVE_DISCLOSURE_LABELS, BACK_TO_NEWS_LABELS, ARTICLE_NAV_LABELS, RELATED_ARTICLES_NAV_LABELS, BREADCRUMB_HOME_LABELS, BREADCRUMB_NEWS_LABELS, TIMELINE_HEADINGS, COMPARISON_BEFORE_LABELS, COMPARISON_AFTER_LABELS, KEY_FIGURES_HEADINGS, AI_SECTION_CONTENT, FILTER_LABELS, SOURCES_HEADING_LABELS, HEADER_SUBTITLE_LABELS, THEME_TOGGLE_LABELS, FOOTER_ABOUT_HEADING_LABELS, FOOTER_ABOUT_TEXT_LABELS, FOOTER_QUICK_LINKS_LABELS, FOOTER_BUILT_BY_LABELS, FOOTER_LANGUAGES_LABELS, FOOTER_HOME_LABELS, FOOTER_SITEMAP_LABELS, FOOTER_RSS_LABELS, FOOTER_GITHUB_REPO_LABELS, FOOTER_LICENSE_LABELS, FOOTER_EUROPARL_LABELS, FOOTER_LINKEDIN_LABELS, FOOTER_SECURITY_POLICY_LABELS, FOOTER_CONTACT_LABELS, FOOTER_DISCLAIMER_LABELS, FOOTER_REPORT_ISSUES_LABELS, FOOTER_ARTICLES_AVAILABLE_LABELS, FOOTER_POLITICAL_INTELLIGENCE_LABELS, TOC_ARIA_LABELS, TRADECRAFT_HEADING_LABELS, TRADECRAFT_INTRO_LABELS, TRADECRAFT_METHODOLOGIES_LABELS, TRADECRAFT_TEMPLATES_LABELS, ANALYSIS_INDEX_HEADING_LABELS, ANALYSIS_INDEX_INTRO_LABELS, ANALYSIS_INDEX_COL_SECTION_LABELS, ANALYSIS_INDEX_COL_ARTIFACT_LABELS, ANALYSIS_INDEX_COL_PATH_LABELS, KEY_TAKEAWAYS_HEADING_LABELS, SUPPLEMENTARY_HEADING_LABELS, SECTION_TITLE_LABELS, RELATED_ANALYSIS_LABELS, ANALYSIS_TRANSPARENCY_LABELS, ANALYSIS_SUMMARY_LABELS, METHODOLOGY_LABELS, TRANSPARENCY_DISCLOSURE_LABELS, CLASSIFICATION_ANALYSIS_LABELS, THREAT_ASSESSMENT_LABELS, RISK_SCORING_LABELS, DEEP_ANALYSIS_LABELS, VIEW_SOURCE_LABELS, VIEW_SOURCE_MARKDOWN_LABELS, ARTICLE_TYPE_ICONS, OPEN_SOURCE_NOTE_LABELS, AI_ANALYSIS_GUIDE_LABELS, SWOT_FRAMEWORK_LABELS, RISK_METHODOLOGY_LABELS, THREAT_FRAMEWORK_LABELS, CLASSIFICATION_GUIDE_LABELS, STYLE_GUIDE_LABELS, SIGNIFICANCE_CLASSIFICATION_LABELS, ACTOR_MAPPING_LABELS, FORCES_ANALYSIS_LABELS, IMPACT_MATRIX_LABELS, POLITICAL_THREAT_LANDSCAPE_LABELS, ACTOR_THREAT_PROFILING_LABELS, CONSEQUENCE_TREES_LABELS, LEGISLATIVE_DISRUPTION_LABELS, RISK_MATRIX_LABELS, QUANTITATIVE_SWOT_LABELS, POLITICAL_CAPITAL_RISK_LABELS, LEGISLATIVE_VELOCITY_RISK_LABELS, AGENT_RISK_WORKFLOW_LABELS, STAKEHOLDER_IMPACT_LABELS, COALITION_DYNAMICS_LABELS, VOTING_PATTERNS_LABELS, CROSS_SESSION_INTELLIGENCE_LABELS, SYNTHESIS_SUMMARY_LABELS, DOCUMENT_ANALYSIS_LABELS, SIGNIFICANCE_SCORING_LABELS, INSTALL_APP_LABELS, UPDATE_AVAILABLE_LABELS, UPDATE_REFRESH_CTA_LABELS, UPDATE_DISMISS_LABELS, OFFLINE_TITLE_LABELS, OFFLINE_BODY_LABELS, OFFLINE_RETRY_LABELS, BUILD_INFO_COMMIT_LABELS, BUILD_INFO_DEPLOYED_LABELS, HEADER_CTA_SPONSOR_LABELS, HEADER_CTA_BECOME_SPONSOR_LABELS, HEADER_CTA_SECURITY_LABELS, FOOTER_NEWS_LABELS, FOOTER_DASHBOARD_LABELS, FOOTER_ANALYSIS_REPORTS_LABELS, FOOTER_API_DOCS_LABELS, FOOTER_COMPANY_TAGLINE_LABELS, LANGUAGE_SELECTION_ARIA_LABELS, FOOTER_TRUST_BADGES_ARIA_LABELS, } from './language-ui.js';
12
12
  export type { AISection, RelationshipLabels, RelatedAnalysisStrings } from './language-ui.js';
13
+ export type { ProgressiveDisclosureStrings } from './language-ui.js';
13
14
  export { WEEK_AHEAD_TITLES, MONTH_AHEAD_TITLES, WEEKLY_REVIEW_TITLES, MONTHLY_REVIEW_TITLES, MOTIONS_TITLES, BREAKING_NEWS_TITLES, COMMITTEE_REPORTS_TITLES, PROPOSITIONS_TITLES, PROPOSITIONS_STRINGS, EDITORIAL_STRINGS, MOTIONS_STRINGS, WEEK_AHEAD_STRINGS, WEEK_AHEAD_STAKEHOLDER_STRINGS, BREAKING_STRINGS, DEEP_ANALYSIS_STRINGS, COMMITTEE_ANALYSIS_CONTENT_STRINGS, SWOT_STRINGS, DASHBOARD_STRINGS, SWOT_BUILDER_STRINGS, DASHBOARD_BUILDER_STRINGS, LOCALIZED_KEYWORDS, MONTH_IN_REVIEW_STRINGS, ANALYSIS_QUALITY_LABELS, ANALYSIS_INSIGHTS_HEADING, } from './language-articles.js';
14
15
  //# sourceMappingURL=languages.d.ts.map
@@ -10,6 +10,6 @@
10
10
  * - **language-articles** — Article-type title generators and body-text strings
11
11
  */
12
12
  export { ALL_LANGUAGES, LANGUAGE_PRESETS, LANGUAGE_FLAGS, LANGUAGE_NAMES, getLocalizedString, isSupportedLanguage, getTextDirection, } from './language-core.js';
13
- export { PAGE_TITLES, PAGE_DESCRIPTIONS, SECTION_HEADINGS, NO_ARTICLES_MESSAGES, SKIP_LINK_TEXTS, ARTICLE_TYPE_LABELS, READ_TIME_LABELS, BACK_TO_NEWS_LABELS, ARTICLE_NAV_LABELS, RELATED_ARTICLES_NAV_LABELS, BREADCRUMB_HOME_LABELS, BREADCRUMB_NEWS_LABELS, TIMELINE_HEADINGS, COMPARISON_BEFORE_LABELS, COMPARISON_AFTER_LABELS, KEY_FIGURES_HEADINGS, AI_SECTION_CONTENT, FILTER_LABELS, SOURCES_HEADING_LABELS, HEADER_SUBTITLE_LABELS, THEME_TOGGLE_LABELS, FOOTER_ABOUT_HEADING_LABELS, FOOTER_ABOUT_TEXT_LABELS, FOOTER_QUICK_LINKS_LABELS, FOOTER_BUILT_BY_LABELS, FOOTER_LANGUAGES_LABELS, FOOTER_HOME_LABELS, FOOTER_SITEMAP_LABELS, FOOTER_RSS_LABELS, FOOTER_GITHUB_REPO_LABELS, FOOTER_LICENSE_LABELS, FOOTER_EUROPARL_LABELS, FOOTER_LINKEDIN_LABELS, FOOTER_SECURITY_POLICY_LABELS, FOOTER_CONTACT_LABELS, FOOTER_DISCLAIMER_LABELS, FOOTER_REPORT_ISSUES_LABELS, FOOTER_ARTICLES_AVAILABLE_LABELS, FOOTER_POLITICAL_INTELLIGENCE_LABELS, TOC_ARIA_LABELS, TRADECRAFT_HEADING_LABELS, TRADECRAFT_INTRO_LABELS, TRADECRAFT_METHODOLOGIES_LABELS, TRADECRAFT_TEMPLATES_LABELS, ANALYSIS_INDEX_HEADING_LABELS, ANALYSIS_INDEX_INTRO_LABELS, ANALYSIS_INDEX_COL_SECTION_LABELS, ANALYSIS_INDEX_COL_ARTIFACT_LABELS, ANALYSIS_INDEX_COL_PATH_LABELS, KEY_TAKEAWAYS_HEADING_LABELS, SUPPLEMENTARY_HEADING_LABELS, SECTION_TITLE_LABELS, RELATED_ANALYSIS_LABELS, ANALYSIS_TRANSPARENCY_LABELS, ANALYSIS_SUMMARY_LABELS, METHODOLOGY_LABELS, TRANSPARENCY_DISCLOSURE_LABELS, CLASSIFICATION_ANALYSIS_LABELS, THREAT_ASSESSMENT_LABELS, RISK_SCORING_LABELS, DEEP_ANALYSIS_LABELS, VIEW_SOURCE_LABELS, VIEW_SOURCE_MARKDOWN_LABELS, ARTICLE_TYPE_ICONS, OPEN_SOURCE_NOTE_LABELS, AI_ANALYSIS_GUIDE_LABELS, SWOT_FRAMEWORK_LABELS, RISK_METHODOLOGY_LABELS, THREAT_FRAMEWORK_LABELS, CLASSIFICATION_GUIDE_LABELS, STYLE_GUIDE_LABELS, SIGNIFICANCE_CLASSIFICATION_LABELS, ACTOR_MAPPING_LABELS, FORCES_ANALYSIS_LABELS, IMPACT_MATRIX_LABELS, POLITICAL_THREAT_LANDSCAPE_LABELS, ACTOR_THREAT_PROFILING_LABELS, CONSEQUENCE_TREES_LABELS, LEGISLATIVE_DISRUPTION_LABELS, RISK_MATRIX_LABELS, QUANTITATIVE_SWOT_LABELS, POLITICAL_CAPITAL_RISK_LABELS, LEGISLATIVE_VELOCITY_RISK_LABELS, AGENT_RISK_WORKFLOW_LABELS, STAKEHOLDER_IMPACT_LABELS, COALITION_DYNAMICS_LABELS, VOTING_PATTERNS_LABELS, CROSS_SESSION_INTELLIGENCE_LABELS, SYNTHESIS_SUMMARY_LABELS, DOCUMENT_ANALYSIS_LABELS, SIGNIFICANCE_SCORING_LABELS, INSTALL_APP_LABELS, UPDATE_AVAILABLE_LABELS, UPDATE_REFRESH_CTA_LABELS, UPDATE_DISMISS_LABELS, OFFLINE_TITLE_LABELS, OFFLINE_BODY_LABELS, OFFLINE_RETRY_LABELS, BUILD_INFO_COMMIT_LABELS, BUILD_INFO_DEPLOYED_LABELS, HEADER_CTA_SPONSOR_LABELS, HEADER_CTA_BECOME_SPONSOR_LABELS, HEADER_CTA_SECURITY_LABELS, FOOTER_NEWS_LABELS, FOOTER_DASHBOARD_LABELS, FOOTER_ANALYSIS_REPORTS_LABELS, FOOTER_API_DOCS_LABELS, FOOTER_COMPANY_TAGLINE_LABELS, LANGUAGE_SELECTION_ARIA_LABELS, FOOTER_TRUST_BADGES_ARIA_LABELS, } from './language-ui.js';
13
+ export { PAGE_TITLES, PAGE_DESCRIPTIONS, SECTION_HEADINGS, NO_ARTICLES_MESSAGES, SKIP_LINK_TEXTS, ARTICLE_TYPE_LABELS, READ_TIME_LABELS, PROGRESSIVE_DISCLOSURE_LABELS, BACK_TO_NEWS_LABELS, ARTICLE_NAV_LABELS, RELATED_ARTICLES_NAV_LABELS, BREADCRUMB_HOME_LABELS, BREADCRUMB_NEWS_LABELS, TIMELINE_HEADINGS, COMPARISON_BEFORE_LABELS, COMPARISON_AFTER_LABELS, KEY_FIGURES_HEADINGS, AI_SECTION_CONTENT, FILTER_LABELS, SOURCES_HEADING_LABELS, HEADER_SUBTITLE_LABELS, THEME_TOGGLE_LABELS, FOOTER_ABOUT_HEADING_LABELS, FOOTER_ABOUT_TEXT_LABELS, FOOTER_QUICK_LINKS_LABELS, FOOTER_BUILT_BY_LABELS, FOOTER_LANGUAGES_LABELS, FOOTER_HOME_LABELS, FOOTER_SITEMAP_LABELS, FOOTER_RSS_LABELS, FOOTER_GITHUB_REPO_LABELS, FOOTER_LICENSE_LABELS, FOOTER_EUROPARL_LABELS, FOOTER_LINKEDIN_LABELS, FOOTER_SECURITY_POLICY_LABELS, FOOTER_CONTACT_LABELS, FOOTER_DISCLAIMER_LABELS, FOOTER_REPORT_ISSUES_LABELS, FOOTER_ARTICLES_AVAILABLE_LABELS, FOOTER_POLITICAL_INTELLIGENCE_LABELS, TOC_ARIA_LABELS, TRADECRAFT_HEADING_LABELS, TRADECRAFT_INTRO_LABELS, TRADECRAFT_METHODOLOGIES_LABELS, TRADECRAFT_TEMPLATES_LABELS, ANALYSIS_INDEX_HEADING_LABELS, ANALYSIS_INDEX_INTRO_LABELS, ANALYSIS_INDEX_COL_SECTION_LABELS, ANALYSIS_INDEX_COL_ARTIFACT_LABELS, ANALYSIS_INDEX_COL_PATH_LABELS, KEY_TAKEAWAYS_HEADING_LABELS, SUPPLEMENTARY_HEADING_LABELS, SECTION_TITLE_LABELS, RELATED_ANALYSIS_LABELS, ANALYSIS_TRANSPARENCY_LABELS, ANALYSIS_SUMMARY_LABELS, METHODOLOGY_LABELS, TRANSPARENCY_DISCLOSURE_LABELS, CLASSIFICATION_ANALYSIS_LABELS, THREAT_ASSESSMENT_LABELS, RISK_SCORING_LABELS, DEEP_ANALYSIS_LABELS, VIEW_SOURCE_LABELS, VIEW_SOURCE_MARKDOWN_LABELS, ARTICLE_TYPE_ICONS, OPEN_SOURCE_NOTE_LABELS, AI_ANALYSIS_GUIDE_LABELS, SWOT_FRAMEWORK_LABELS, RISK_METHODOLOGY_LABELS, THREAT_FRAMEWORK_LABELS, CLASSIFICATION_GUIDE_LABELS, STYLE_GUIDE_LABELS, SIGNIFICANCE_CLASSIFICATION_LABELS, ACTOR_MAPPING_LABELS, FORCES_ANALYSIS_LABELS, IMPACT_MATRIX_LABELS, POLITICAL_THREAT_LANDSCAPE_LABELS, ACTOR_THREAT_PROFILING_LABELS, CONSEQUENCE_TREES_LABELS, LEGISLATIVE_DISRUPTION_LABELS, RISK_MATRIX_LABELS, QUANTITATIVE_SWOT_LABELS, POLITICAL_CAPITAL_RISK_LABELS, LEGISLATIVE_VELOCITY_RISK_LABELS, AGENT_RISK_WORKFLOW_LABELS, STAKEHOLDER_IMPACT_LABELS, COALITION_DYNAMICS_LABELS, VOTING_PATTERNS_LABELS, CROSS_SESSION_INTELLIGENCE_LABELS, SYNTHESIS_SUMMARY_LABELS, DOCUMENT_ANALYSIS_LABELS, SIGNIFICANCE_SCORING_LABELS, INSTALL_APP_LABELS, UPDATE_AVAILABLE_LABELS, UPDATE_REFRESH_CTA_LABELS, UPDATE_DISMISS_LABELS, OFFLINE_TITLE_LABELS, OFFLINE_BODY_LABELS, OFFLINE_RETRY_LABELS, BUILD_INFO_COMMIT_LABELS, BUILD_INFO_DEPLOYED_LABELS, HEADER_CTA_SPONSOR_LABELS, HEADER_CTA_BECOME_SPONSOR_LABELS, HEADER_CTA_SECURITY_LABELS, FOOTER_NEWS_LABELS, FOOTER_DASHBOARD_LABELS, FOOTER_ANALYSIS_REPORTS_LABELS, FOOTER_API_DOCS_LABELS, FOOTER_COMPANY_TAGLINE_LABELS, LANGUAGE_SELECTION_ARIA_LABELS, FOOTER_TRUST_BADGES_ARIA_LABELS, } from './language-ui.js';
14
14
  export { WEEK_AHEAD_TITLES, MONTH_AHEAD_TITLES, WEEKLY_REVIEW_TITLES, MONTHLY_REVIEW_TITLES, MOTIONS_TITLES, BREAKING_NEWS_TITLES, COMMITTEE_REPORTS_TITLES, PROPOSITIONS_TITLES, PROPOSITIONS_STRINGS, EDITORIAL_STRINGS, MOTIONS_STRINGS, WEEK_AHEAD_STRINGS, WEEK_AHEAD_STAKEHOLDER_STRINGS, BREAKING_STRINGS, DEEP_ANALYSIS_STRINGS, COMMITTEE_ANALYSIS_CONTENT_STRINGS, SWOT_STRINGS, DASHBOARD_STRINGS, SWOT_BUILDER_STRINGS, DASHBOARD_BUILDER_STRINGS, LOCALIZED_KEYWORDS, MONTH_IN_REVIEW_STRINGS, ANALYSIS_QUALITY_LABELS, ANALYSIS_INSIGHTS_HEADING, } from './language-articles.js';
15
15
  //# sourceMappingURL=languages.js.map
@@ -11,6 +11,7 @@
11
11
  * - `article-category-labels.ts` — `ARTICLE_TYPE_LABELS`, `ARTICLE_TYPE_ICONS`
12
12
  * - `accessibility.ts` — `SKIP_LINK_TEXTS`, `TOC_ARIA_LABELS`, language-switcher / footer trust-badge ARIA labels
13
13
  * - `reading-time.ts` — `READ_TIME_LABELS` (per-language pluralization)
14
+ * - `progressive-disclosure.ts` — `PROGRESSIVE_DISCLOSURE_LABELS` (reading layers, expand CTAs, reading-time line, TOC layer badge)
14
15
  * - `ai-content.ts` — `AI_SECTION_CONTENT` + `AISection` interface
15
16
  * - `related-analysis.ts` — `SECTION_TITLE_LABELS`, `RELATED_ANALYSIS_LABELS`
16
17
  * - `tradecraft-cards.ts` — tradecraft / analysis-index card labels
@@ -24,6 +25,7 @@ export { FOOTER_ABOUT_HEADING_LABELS, FOOTER_ABOUT_TEXT_LABELS, FOOTER_QUICK_LIN
24
25
  export { ARTICLE_TYPE_LABELS, ARTICLE_TYPE_ICONS, HE_DEEP_ANALYSIS, } from './article-category-labels.js';
25
26
  export { SKIP_LINK_TEXTS, TOC_ARIA_LABELS, LANGUAGE_SELECTION_ARIA_LABELS, FOOTER_TRUST_BADGES_ARIA_LABELS, } from './accessibility.js';
26
27
  export { READ_TIME_LABELS } from './reading-time.js';
28
+ export { PROGRESSIVE_DISCLOSURE_LABELS, type ProgressiveDisclosureStrings, } from './progressive-disclosure.js';
27
29
  export { AI_SECTION_CONTENT, type AISection } from './ai-content.js';
28
30
  export { SECTION_TITLE_LABELS, RELATED_ANALYSIS_LABELS, type RelationshipLabels, type RelatedAnalysisStrings, } from './related-analysis.js';
29
31
  export { TRADECRAFT_HEADING_LABELS, TRADECRAFT_INTRO_LABELS, TRADECRAFT_METHODOLOGIES_LABELS, TRADECRAFT_TEMPLATES_LABELS, ANALYSIS_INDEX_HEADING_LABELS, ANALYSIS_INDEX_INTRO_LABELS, ANALYSIS_INDEX_COL_SECTION_LABELS, ANALYSIS_INDEX_COL_ARTIFACT_LABELS, ANALYSIS_INDEX_COL_PATH_LABELS, VIEW_SOURCE_LABELS, VIEW_SOURCE_MARKDOWN_LABELS, OPEN_SOURCE_NOTE_LABELS, } from './tradecraft-cards.js';
@@ -13,6 +13,7 @@
13
13
  * - `article-category-labels.ts` — `ARTICLE_TYPE_LABELS`, `ARTICLE_TYPE_ICONS`
14
14
  * - `accessibility.ts` — `SKIP_LINK_TEXTS`, `TOC_ARIA_LABELS`, language-switcher / footer trust-badge ARIA labels
15
15
  * - `reading-time.ts` — `READ_TIME_LABELS` (per-language pluralization)
16
+ * - `progressive-disclosure.ts` — `PROGRESSIVE_DISCLOSURE_LABELS` (reading layers, expand CTAs, reading-time line, TOC layer badge)
16
17
  * - `ai-content.ts` — `AI_SECTION_CONTENT` + `AISection` interface
17
18
  * - `related-analysis.ts` — `SECTION_TITLE_LABELS`, `RELATED_ANALYSIS_LABELS`
18
19
  * - `tradecraft-cards.ts` — tradecraft / analysis-index card labels
@@ -26,6 +27,7 @@ export { FOOTER_ABOUT_HEADING_LABELS, FOOTER_ABOUT_TEXT_LABELS, FOOTER_QUICK_LIN
26
27
  export { ARTICLE_TYPE_LABELS, ARTICLE_TYPE_ICONS, HE_DEEP_ANALYSIS, } from './article-category-labels.js';
27
28
  export { SKIP_LINK_TEXTS, TOC_ARIA_LABELS, LANGUAGE_SELECTION_ARIA_LABELS, FOOTER_TRUST_BADGES_ARIA_LABELS, } from './accessibility.js';
28
29
  export { READ_TIME_LABELS } from './reading-time.js';
30
+ export { PROGRESSIVE_DISCLOSURE_LABELS, } from './progressive-disclosure.js';
29
31
  export { AI_SECTION_CONTENT } from './ai-content.js';
30
32
  export { SECTION_TITLE_LABELS, RELATED_ANALYSIS_LABELS, } from './related-analysis.js';
31
33
  export { TRADECRAFT_HEADING_LABELS, TRADECRAFT_INTRO_LABELS, TRADECRAFT_METHODOLOGIES_LABELS, TRADECRAFT_TEMPLATES_LABELS, ANALYSIS_INDEX_HEADING_LABELS, ANALYSIS_INDEX_INTRO_LABELS, ANALYSIS_INDEX_COL_SECTION_LABELS, ANALYSIS_INDEX_COL_ARTIFACT_LABELS, ANALYSIS_INDEX_COL_PATH_LABELS, VIEW_SOURCE_LABELS, VIEW_SOURCE_MARKDOWN_LABELS, OPEN_SOURCE_NOTE_LABELS, } from './tradecraft-cards.js';
@@ -0,0 +1,40 @@
1
+ /**
2
+ * @module Constants/UI/ProgressiveDisclosure
3
+ * @description Localized chrome for the article progressive-disclosure
4
+ * surface — the three reading layers (quick / full analysis / complete
5
+ * intelligence), the visible `<summary>` expand CTAs, the reading-time
6
+ * estimate line, and the Table-of-Contents layer badge.
7
+ *
8
+ * A single `PROGRESSIVE_DISCLOSURE_LABELS` map is shared by every call
9
+ * site (the layer wrappers in `aggregator/progressive-disclosure.ts`, the
10
+ * reading-time line in `aggregator/html/shell.ts`, and the TOC badge in
11
+ * `aggregator/html/toc.ts`) so the layer vocabulary stays consistent and
12
+ * each phrase is translated exactly once.
13
+ */
14
+ import type { LanguageMap } from '../../types/index.js';
15
+ /** Per-language UI strings for the article progressive-disclosure chrome. */
16
+ export interface ProgressiveDisclosureStrings {
17
+ /** Name of the always-visible quick-read layer (reading-time + aria). */
18
+ readonly quickRead: string;
19
+ /** Name of the cumulative full-analysis layer (reading-time + aria). */
20
+ readonly fullAnalysis: string;
21
+ /** Name of the cumulative complete-intelligence layer (reading-time + aria). */
22
+ readonly completeIntelligence: string;
23
+ /** Visible CTA on the analysis `<summary>` toggle (a `↓` glyph is appended in markup). */
24
+ readonly expandAnalysis: string;
25
+ /** Visible CTA on the intelligence `<summary>` toggle (a `↓` glyph is appended in markup). */
26
+ readonly expandIntelligence: string;
27
+ /** Abbreviation for minutes used in the reading-time line (includes leading space for non-CJK locales). */
28
+ readonly minutesAbbr: string;
29
+ /** Aria-label for the reading-time estimate paragraph. */
30
+ readonly readingTimeAria: string;
31
+ /** Aria-label prefix for the TOC layer badge (rendered as e.g. "Layer L1"). */
32
+ readonly layerBadge: string;
33
+ }
34
+ /**
35
+ * Localised progressive-disclosure strings across all 14 supported
36
+ * languages. Reused by the layer wrappers, the reading-time line, and the
37
+ * TOC layer badge so the layer vocabulary is translated only once.
38
+ */
39
+ export declare const PROGRESSIVE_DISCLOSURE_LABELS: LanguageMap<ProgressiveDisclosureStrings>;
40
+ //# sourceMappingURL=progressive-disclosure.d.ts.map