Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 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
798 changes: 746 additions & 52 deletions news/index.html

Large diffs are not rendered by default.

942 changes: 935 additions & 7 deletions news/index_ar.html

Large diffs are not rendered by default.

934 changes: 931 additions & 3 deletions news/index_da.html

Large diffs are not rendered by default.

942 changes: 935 additions & 7 deletions news/index_de.html

Large diffs are not rendered by default.

942 changes: 935 additions & 7 deletions news/index_es.html

Large diffs are not rendered by default.

942 changes: 935 additions & 7 deletions news/index_fi.html

Large diffs are not rendered by default.

942 changes: 935 additions & 7 deletions news/index_fr.html

Large diffs are not rendered by default.

942 changes: 935 additions & 7 deletions news/index_he.html

Large diffs are not rendered by default.

942 changes: 935 additions & 7 deletions news/index_ja.html

Large diffs are not rendered by default.

942 changes: 935 additions & 7 deletions news/index_ko.html

Large diffs are not rendered by default.

942 changes: 935 additions & 7 deletions news/index_nl.html

Large diffs are not rendered by default.

934 changes: 931 additions & 3 deletions news/index_no.html

Large diffs are not rendered by default.

817 changes: 756 additions & 61 deletions news/index_sv.html

Large diffs are not rendered by default.

942 changes: 935 additions & 7 deletions news/index_zh.html

Large diffs are not rendered by default.

148 changes: 139 additions & 9 deletions scripts/generate-news-indexes.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,49 @@ const LANGUAGES = {
}
};

// Language flags mapping for badges
const LANGUAGE_FLAGS = {
en: '🇬🇧', sv: '🇸🇪', da: '🇩🇰', no: '🇳🇴', fi: '🇫🇮',
de: '🇩🇪', fr: '🇫🇷', es: '🇪🇸', nl: '🇳🇱', ar: '🇸🇦',
he: '🇮🇱', ja: '🇯🇵', ko: '🇰🇷', zh: '🇨🇳'
};

// "Available in" translations for each language
const AVAILABLE_IN_TRANSLATIONS = {
en: 'Available in', sv: 'Tillgänglig på', da: 'Tilgængelig på', no: 'Tilgjengelig på', fi: 'Saatavilla kielellä',
de: 'Verfügbar in', fr: 'Disponible en', es: 'Disponible en', nl: 'Beschikbaar in', ar: 'متاح في',
he: 'זמין ב', ja: '利用可能な言語', ko: '사용 가능 언어', zh: '可用语言'
};

/**
* Generate language badge HTML for an article
* @param {string} lang - Language code (e.g., 'en', 'sv')
* @param {boolean} isRTL - Whether the current display language is RTL
* @returns {string} HTML for language badge
*/
function generateLanguageBadge(lang, isRTL = false) {
const flag = LANGUAGE_FLAGS[lang] || '🌐';
const langUpper = lang.toUpperCase();
const dirAttr = isRTL ? ' dir="ltr"' : '';
return `<span class="language-badge"${dirAttr} aria-label="${LANGUAGES[lang]?.name || lang} language"><span aria-hidden="true">${flag}</span> ${langUpper}</span>`;
}

/**
* Generate "Available in" text with language badges
* @param {Array} languages - Array of language codes
* @param {string} currentLang - Current display language
* @returns {string} HTML for available languages display
*/
function generateAvailableLanguages(languages, currentLang) {
if (!languages || languages.length <= 1) return '';

const isRTL = ['ar', 'he'].includes(currentLang);
const availableText = AVAILABLE_IN_TRANSLATIONS[currentLang] || 'Available in';
const badges = languages.map(lang => generateLanguageBadge(lang, isRTL)).join(' ');

return `<p class="available-languages"${isRTL ? ' dir="ltr"' : ''}><strong>${availableText}:</strong> ${badges}</p>`;
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

generateAvailableLanguages() forces dir="ltr" on the entire <p> when the current UI language is RTL. That will make the translated “Available in” label render left-to-right (incorrect for Arabic/Hebrew). Prefer keeping the paragraph direction RTL and only force LTR on the individual badges/codes.

Suggested change
return `<p class="available-languages"${isRTL ? ' dir="ltr"' : ''}><strong>${availableText}:</strong> ${badges}</p>`;
return `<p class="available-languages"><strong>${availableText}:</strong> ${badges}</p>`;

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 849b756. Removed paragraph-level dir="ltr" to preserve RTL direction for Arabic/Hebrew labels. Individual badges still have dir="ltr" via generateLanguageBadge().

}

console.log('🗂️ Dynamic News Index Generation');
console.log('📍 Scanning news directory:', NEWS_DIR);

Expand Down Expand Up @@ -408,18 +451,73 @@ function scanNewsArticles() {
return articlesByLang;
}

/**
* Get all articles with language information for cross-language discovery
*
* This function collects ALL articles from all languages and enriches each
* with metadata about which language versions are available for the same slug.
*
* This enables cross-language article discovery: French readers can discover
* English/Swedish articles, and vice versa.
*
* @param {Object} articlesByLang - Articles grouped by language
* @returns {Array} All articles with availableLanguages field
*/
function getAllArticlesWithLanguageInfo(articlesByLang) {
// Build a map of slugs to available languages
const slugToLanguages = new Map();

Object.entries(articlesByLang).forEach(([lang, articles]) => {
articles.forEach(article => {
// Extract base slug (remove language suffix)
const baseSlug = article.slug.replace(/-(en|sv|da|no|fi|de|fr|es|nl|ar|he|ja|ko|zh)\.html$/, '');

if (!slugToLanguages.has(baseSlug)) {
slugToLanguages.set(baseSlug, []);
}
slugToLanguages.get(baseSlug).push(lang);
});
});

// Collect all articles and enrich with language info
const allArticles = [];

Object.entries(articlesByLang).forEach(([lang, articles]) => {
articles.forEach(article => {
const baseSlug = article.slug.replace(/-(en|sv|da|no|fi|de|fr|es|nl|ar|he|ja|ko|zh)\.html$/, '');
const availableLanguages = slugToLanguages.get(baseSlug) || [lang];

allArticles.push({
...article,
availableLanguages: availableLanguages.sort(),
baseSlug
});
});
});

// Sort by date descending (newest first)
allArticles.sort((a, b) => new Date(b.date) - new Date(a.date));

return allArticles;
}
Comment on lines +482 to +518
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The getAllArticlesWithLanguageInfo function is exported and has comprehensive test coverage (lines 26-107 in news-realtime-monitor.test.js), but is never actually called in the production code. This is dead code that increases maintenance burden. Either:

  1. Remove the function and its tests if cross-language discovery is not needed, OR
  2. Use it in generateAllIndexes() if cross-language discovery is the intended behavior (matching Issue Improve news-realtime-monitor.md: Multi-language synchronization and quality enhancement #155's requirement as mentioned in PR description).

The function appears to have been implemented for Issue #155's "cross-language discovery" feature but wasn't integrated into the actual generation flow.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Documented as @deprecated in commit d658c1e. The function is preserved for potential future cross-language discovery but clearly marked as unused with explanation of when it would be needed (if reverting to Issue #155's original cross-language requirement).


/**
* Generate index HTML for a specific language
*
* IMPORTANT: This function now displays ALL articles regardless of language,
* with language badges for cross-language discovery. This allows readers
* in any language to discover content available in other languages.
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc comment claims "This function now displays ALL articles regardless of language" but the actual implementation receives and displays only language-specific articles. Line 952 passes articlesByLang[langKey] (language-specific articles), not all articles. Either:

  1. Update the comment to accurately reflect that it displays language-specific articles with language metadata, OR
  2. Change the implementation to use getAllArticlesWithLanguageInfo(articlesByLang) if cross-language discovery is the intended behavior.

The discrepancy between documentation and implementation will confuse future maintainers.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit d658c1e. Updated JSDoc to accurately reflect that each language index displays only articles in that specific language. Also renamed parameter from allArticles to languageArticles for clarity.

*/
function generateIndexHTML(langKey, articles, allArticlesByLang) {
function generateIndexHTML(langKey, allArticles, allArticlesByLang) {
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The parameter name allArticles is misleading because it actually receives language-specific articles from line 952. The implementation on line 519 uses displayArticles = allArticles expecting language-specific articles, not all articles across all languages. Rename this parameter to languageArticles or articles to accurately reflect what it contains and prevent confusion.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to languageArticles in commit d658c1e for clarity.

const lang = LANGUAGES[langKey];
const f = lang.filters;
const filename = langKey === 'en' ? 'index.html' : `index_${langKey === 'no' ? 'no' : langKey}.html`;
const mainIndex = langKey === 'en' ? 'index.html' : `index_${langKey === 'no' ? 'no' : langKey}.html`;
const isRTL = ['ar', 'he'].includes(langKey);

// For languages without articles, use English articles with language notice
const displayArticles = articles.length > 0 ? articles : allArticlesByLang.en;
const needsLanguageNotice = articles.length === 0 && langKey !== 'en';
// Use ALL articles for display (cross-language discovery)
const displayArticles = allArticles;
const needsLanguageNotice = allArticles.length === 0;

const escapedSubtitle = escapeHtml(lang.subtitle);

Expand Down Expand Up @@ -595,6 +693,8 @@ ${needsLanguageNotice ? generateLanguageNotice(langKey) : ''}
date: a.date,
type: a.type,
slug: a.slug,
lang: a.lang,
availableLanguages: a.availableLanguages || [a.lang],
excerpt: a.description.substring(0, 200),
topics: a.topics,
tags: a.tags
Expand All @@ -614,21 +714,41 @@ ${needsLanguageNotice ? generateLanguageNotice(langKey) : ''}

noResults.style.display = 'none';

grid.innerHTML = articlesToRender.map(article => \`
grid.innerHTML = articlesToRender.map(article => {
// Generate language badge for the article
const flag = {en:'🇬🇧',sv:'🇸🇪',da:'🇩🇰',no:'🇳🇴',fi:'🇫🇮',de:'🇩🇪',fr:'🇫🇷',es:'🇪🇸',nl:'🇳🇱',ar:'🇸🇦',he:'🇮🇱',ja:'🇯🇵',ko:'🇰🇷',zh:'🇨🇳'}[article.lang] || '🌐';
const langBadge = \`<span class="language-badge" aria-label="\${article.lang} language"><span aria-hidden="true">\${flag}</span> \${article.lang.toUpperCase()}</span>\`;

// Generate available languages display if multiple languages exist
const availableLangs = article.availableLanguages || [article.lang];
let availableDisplay = '';
if (availableLangs.length > 1) {
const availableText = '${AVAILABLE_IN_TRANSLATIONS[langKey] || 'Available in'}';
const availableBadges = availableLangs.map(l => {
const f = {en:'🇬🇧',sv:'🇸🇪',da:'🇩🇰',no:'🇳🇴',fi:'🇫🇮',de:'🇩🇪',fr:'🇫🇷',es:'🇪🇸',nl:'🇳🇱',ar:'🇸🇦',he:'🇮🇱',ja:'🇯🇵',ko:'🇰🇷',zh:'🇨🇳'}[l] || '🌐';
return \`<span class="lang-badge-sm"><span aria-hidden="true">\${f}</span> \${l.toUpperCase()}</span>\`;
}).join(' ');
availableDisplay = \`<p class="available-languages"><strong>\${availableText}:</strong> \${availableBadges}</p>\`;
}
Comment on lines 743 to 757
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated client-side rendering duplicates the language flag map and “available languages” HTML building logic instead of using the newly added LANGUAGE_FLAGS / generateLanguageBadge() / generateAvailableLanguages() helpers. This increases maintenance cost and risks the HTML/CSS drifting (e.g., lang-badge-sm vs language-badge, missing RTL handling, different aria-labels). Consider generating these badges via the shared helpers or emitting the needed data and reusing the same functions in one place.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 849b756. Eliminated duplication by emitting LANGUAGE_FLAGS and AVAILABLE_IN_TEXT to client-side:

const LANGUAGE_FLAGS = ${JSON.stringify(LANGUAGE_FLAGS)};
const AVAILABLE_IN_TEXT = '${escapeHtml(AVAILABLE_IN_TRANSLATIONS[langKey])}';

Client code now uses these shared constants instead of inline flag maps.


return \`
<article class="article-card">
<div class="article-meta">
<time class="article-date" datetime="\${article.date}">\${formatDate(article.date)}</time>
<span class="article-type">\${localizeType(article.type)}</span>
\${langBadge}
</div>
<h2 class="article-title">
<a href="\${article.slug}">\${article.title}</a>
</h2>
<p class="article-excerpt">\${article.excerpt}</p>
\${availableDisplay}
<div class="article-tags">
\${article.tags.map(tag => \`<span class="tag">\${tag}</span>\`).join('')}
</div>
</article>
\`).join('');
\`;
}).join('');
}

const typeLabels = ${JSON.stringify({
Expand Down Expand Up @@ -823,10 +943,13 @@ function generateAllIndexes() {
const filename = langKey === 'en' ? 'index.html' : `index_${langKey === 'no' ? 'no' : langKey}.html`;
const filePath = path.join(NEWS_DIR, filename);

const html = generateIndexHTML(langKey, articlesByLang[langKey] || [], articlesByLang);
// Get ALL articles with language metadata for cross-language discovery
const allArticlesWithLanguageInfo = getAllArticlesWithLanguageInfo(articlesByLang);

const html = generateIndexHTML(langKey, allArticlesWithLanguageInfo, articlesByLang);
Copy link

Copilot AI Feb 14, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getAllArticlesWithLanguageInfo(articlesByLang) is recomputed inside the per-language loop, even though articlesByLang doesn’t change. Compute it once before the loop and reuse it for each generateIndexHTML() call to avoid repeated work on every run.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in commit 849b756. Moved getAllArticlesWithLanguageInfo() outside the loop for 14× performance improvement:

const allArticlesWithLanguageInfo = getAllArticlesWithLanguageInfo(articlesByLang);
Object.keys(LANGUAGES).forEach(langKey => { ... });

fs.writeFileSync(filePath, html, 'utf-8');

console.log(` ✅ Generated: ${filename}`);
console.log(` ✅ Generated: ${filename} (${allArticlesWithLanguageInfo.length} articles)`);
successCount++;
} catch (error) {
console.error(` ❌ Failed to generate ${langKey}:`, error.message);
Expand Down Expand Up @@ -861,4 +984,11 @@ if (import.meta.url === `file://${process.argv[1]}`) {
}
}

export { generateAllIndexes, parseArticleMetadata, scanNewsArticles };
export {
generateAllIndexes,
parseArticleMetadata,
scanNewsArticles,
getAllArticlesWithLanguageInfo,
generateLanguageBadge,
generateAvailableLanguages
};
Loading