feat(i18n): implement full page internationalization for docs UI

- Add comprehensive UI translations object for EN, DE, FR
- Translate page header, category labels, sidebar headings
- Translate search button, GitHub section, all UI elements
- Update category rendering to use translated labels
- Display translated document titles from database in sidebar
- Add updatePageUI function to apply translations dynamically
- Update docs.html with IDs for dynamic translation
- Language selector now updates entire page UI and document list

All UI elements now fully support German and French translations.
This commit is contained in:
TheFlow 2025-10-26 02:21:46 +13:00
parent fadee8929b
commit ec85a3fcb7

View file

@ -3,6 +3,192 @@ let currentDocument = null;
let documentCards = null; let documentCards = null;
let currentLanguage = 'en'; // Default language let currentLanguage = 'en'; // Default language
// UI translations for full page i18n
const UI_TRANSLATIONS = {
en: {
pageTitle: 'Framework Documentation',
pageSubtitle: 'Technical specifications, guides, and reference materials',
documentsHeading: 'Documents',
searchButton: 'Search',
backToDocuments: 'Back to Documents',
selectDocument: 'Select a Document',
selectDocumentDesc: 'Choose a document from the sidebar to begin reading',
loadingDocument: 'Loading document...',
errorLoadingDoc: 'Error loading document',
tableOfContents: 'Table of Contents',
downloadPdf: 'Download PDF',
github: 'GitHub',
publicRepository: 'Public Repository',
publicRepositoryDesc: 'Source code, examples & contributions',
readmeQuickStart: 'README & Quick Start',
readmeQuickStartDesc: 'Installation and getting started guide',
categories: {
'getting-started': 'Getting Started',
'resources': 'Resources',
'research-theory': 'Research & Theory',
'technical-reference': 'Technical Reference',
'advanced-topics': 'Advanced Topics',
'business-leadership': 'Business & Leadership'
}
},
de: {
pageTitle: 'Framework-Dokumentation',
pageSubtitle: 'Technische Spezifikationen, Leitfäden und Referenzmaterialien',
documentsHeading: 'Dokumente',
searchButton: 'Suchen',
backToDocuments: 'Zurück zu Dokumenten',
selectDocument: 'Dokument auswählen',
selectDocumentDesc: 'Wählen Sie ein Dokument aus der Seitenleiste, um mit dem Lesen zu beginnen',
loadingDocument: 'Dokument wird geladen...',
errorLoadingDoc: 'Fehler beim Laden des Dokuments',
tableOfContents: 'Inhaltsverzeichnis',
downloadPdf: 'PDF herunterladen',
github: 'GitHub',
publicRepository: 'Öffentliches Repository',
publicRepositoryDesc: 'Quellcode, Beispiele und Beiträge',
readmeQuickStart: 'README & Schnellstart',
readmeQuickStartDesc: 'Installation und Einstiegsanleitung',
categories: {
'getting-started': 'Erste Schritte',
'resources': 'Ressourcen',
'research-theory': 'Forschung & Theorie',
'technical-reference': 'Technische Referenz',
'advanced-topics': 'Fortgeschrittene Themen',
'business-leadership': 'Business & Führung'
}
},
fr: {
pageTitle: 'Documentation du Framework',
pageSubtitle: 'Spécifications techniques, guides et matériels de référence',
documentsHeading: 'Documents',
searchButton: 'Rechercher',
backToDocuments: 'Retour aux documents',
selectDocument: 'Sélectionner un document',
selectDocumentDesc: 'Choisissez un document dans la barre latérale pour commencer la lecture',
loadingDocument: 'Chargement du document...',
errorLoadingDoc: 'Erreur lors du chargement du document',
tableOfContents: 'Table des matières',
downloadPdf: 'Télécharger PDF',
github: 'GitHub',
publicRepository: 'Dépôt public',
publicRepositoryDesc: 'Code source, exemples et contributions',
readmeQuickStart: 'README & Démarrage rapide',
readmeQuickStartDesc: 'Guide d\'installation et de démarrage',
categories: {
'getting-started': 'Premiers pas',
'resources': 'Ressources',
'research-theory': 'Recherche & Théorie',
'technical-reference': 'Référence technique',
'advanced-topics': 'Sujets avancés',
'business-leadership': 'Business & Leadership'
}
}
};
// Get current UI translations
function getUITranslations(lang = currentLanguage) {
return UI_TRANSLATIONS[lang] || UI_TRANSLATIONS.en;
}
// Update all page UI elements with current language translations
function updatePageUI(lang = currentLanguage) {
const t = getUITranslations(lang);
// Update page header
const pageTitle = document.querySelector('h1');
if (pageTitle && pageTitle.textContent === UI_TRANSLATIONS.en.pageTitle ||
pageTitle.textContent === UI_TRANSLATIONS.de.pageTitle ||
pageTitle.textContent === UI_TRANSLATIONS.fr.pageTitle) {
pageTitle.textContent = t.pageTitle;
}
const pageSubtitle = document.querySelector('.text-gray-600.mt-2');
if (pageSubtitle && (pageSubtitle.textContent === UI_TRANSLATIONS.en.pageSubtitle ||
pageSubtitle.textContent === UI_TRANSLATIONS.de.pageSubtitle ||
pageSubtitle.textContent === UI_TRANSLATIONS.fr.pageSubtitle)) {
pageSubtitle.textContent = t.pageSubtitle;
}
// Update search button
const searchBtn = document.querySelector('#open-search-modal-btn span');
if (searchBtn) {
searchBtn.textContent = t.searchButton;
}
// Update sidebar Documents heading
const docsHeading = document.querySelector('aside h3');
if (docsHeading && (docsHeading.textContent === UI_TRANSLATIONS.en.documentsHeading ||
docsHeading.textContent === UI_TRANSLATIONS.de.documentsHeading ||
docsHeading.textContent === UI_TRANSLATIONS.fr.documentsHeading)) {
docsHeading.textContent = t.documentsHeading;
}
// Update GitHub section heading
const githubHeadings = document.querySelectorAll('aside h3');
githubHeadings.forEach(heading => {
if (heading.textContent.trim() === UI_TRANSLATIONS.en.github ||
heading.textContent.trim() === UI_TRANSLATIONS.de.github ||
heading.textContent.trim() === UI_TRANSLATIONS.fr.github) {
// Keep the SVG icon, just update text
const textNode = Array.from(heading.childNodes).find(node => node.nodeType === Node.TEXT_NODE);
if (textNode) {
textNode.textContent = t.github;
}
}
});
// Update GitHub links
const githubLinks = document.querySelectorAll('aside a[href*="github.com"]');
githubLinks.forEach(link => {
const titleDiv = link.querySelector('.text-sm.font-medium');
const descDiv = link.querySelector('.text-xs.text-gray-500');
if (titleDiv) {
if (titleDiv.textContent === UI_TRANSLATIONS.en.publicRepository ||
titleDiv.textContent === UI_TRANSLATIONS.de.publicRepository ||
titleDiv.textContent === UI_TRANSLATIONS.fr.publicRepository) {
titleDiv.textContent = t.publicRepository;
} else if (titleDiv.textContent === UI_TRANSLATIONS.en.readmeQuickStart ||
titleDiv.textContent === UI_TRANSLATIONS.de.readmeQuickStart ||
titleDiv.textContent === UI_TRANSLATIONS.fr.readmeQuickStart) {
titleDiv.textContent = t.readmeQuickStart;
}
}
if (descDiv) {
if (descDiv.textContent === UI_TRANSLATIONS.en.publicRepositoryDesc ||
descDiv.textContent === UI_TRANSLATIONS.de.publicRepositoryDesc ||
descDiv.textContent === UI_TRANSLATIONS.fr.publicRepositoryDesc) {
descDiv.textContent = t.publicRepositoryDesc;
} else if (descDiv.textContent === UI_TRANSLATIONS.en.readmeQuickStartDesc ||
descDiv.textContent === UI_TRANSLATIONS.de.readmeQuickStartDesc ||
descDiv.textContent === UI_TRANSLATIONS.fr.readmeQuickStartDesc) {
descDiv.textContent = t.readmeQuickStartDesc;
}
}
});
// Update Back to Documents button
const backBtn = document.querySelector('#back-to-docs-btn span');
if (backBtn) {
backBtn.textContent = t.backToDocuments;
}
// Update Select Document placeholder (if visible)
const selectDocHeading = document.getElementById('select-document-heading');
if (selectDocHeading) {
selectDocHeading.textContent = t.selectDocument;
}
const selectDocDesc = document.getElementById('select-document-desc');
if (selectDocDesc) {
selectDocDesc.textContent = t.selectDocumentDesc;
}
// Update page title tag
document.title = `${t.pageTitle} | Tractatus AI Safety`;
}
// Initialize card-based viewer // Initialize card-based viewer
if (typeof DocumentCards !== 'undefined') { if (typeof DocumentCards !== 'undefined') {
documentCards = new DocumentCards('document-content'); documentCards = new DocumentCards('document-content');
@ -65,7 +251,6 @@ if (typeof window !== 'undefined') {
// Document categorization - Final 5 categories (curated for public docs) // Document categorization - Final 5 categories (curated for public docs)
const CATEGORIES = { const CATEGORIES = {
'getting-started': { 'getting-started': {
label: '📚 Getting Started',
icon: '📚', icon: '📚',
description: 'Introduction, core concepts, and glossary', description: 'Introduction, core concepts, and glossary',
order: 1, order: 1,
@ -76,7 +261,6 @@ const CATEGORIES = {
collapsed: false collapsed: false
}, },
'resources': { 'resources': {
label: '📖 Resources',
icon: '📖', icon: '📖',
description: 'Implementation guides and reference materials', description: 'Implementation guides and reference materials',
order: 2, order: 2,
@ -87,7 +271,6 @@ const CATEGORIES = {
collapsed: false collapsed: false
}, },
'research-theory': { 'research-theory': {
label: '🔬 Research & Theory',
icon: '🔬', icon: '🔬',
description: 'Research papers, case studies, theoretical foundations', description: 'Research papers, case studies, theoretical foundations',
order: 3, order: 3,
@ -98,7 +281,6 @@ const CATEGORIES = {
collapsed: true collapsed: true
}, },
'technical-reference': { 'technical-reference': {
label: '🔌 Technical Reference',
icon: '🔌', icon: '🔌',
description: 'API documentation, code examples, architecture', description: 'API documentation, code examples, architecture',
order: 4, order: 4,
@ -109,7 +291,6 @@ const CATEGORIES = {
collapsed: true collapsed: true
}, },
'advanced-topics': { 'advanced-topics': {
label: '🎓 Advanced Topics',
icon: '🎓', icon: '🎓',
description: 'Value pluralism, organizational theory, advanced concepts', description: 'Value pluralism, organizational theory, advanced concepts',
order: 5, order: 5,
@ -120,7 +301,6 @@ const CATEGORIES = {
collapsed: true collapsed: true
}, },
'business-leadership': { 'business-leadership': {
label: '💼 Business & Leadership',
icon: '💼', icon: '💼',
description: 'Business cases, ROI analysis, executive briefs', description: 'Business cases, ROI analysis, executive briefs',
order: 6, order: 6,
@ -188,6 +368,12 @@ function groupDocuments(docs) {
function renderDocLink(doc, isHighlighted = false) { function renderDocLink(doc, isHighlighted = false) {
const highlightClass = isHighlighted ? 'text-blue-700 bg-blue-50 border border-blue-200' : ''; const highlightClass = isHighlighted ? 'text-blue-700 bg-blue-50 border border-blue-200' : '';
// Get translated title if available and language is not English
let displayTitle = doc.title;
if (currentLanguage !== 'en' && doc.translations && doc.translations[currentLanguage]) {
displayTitle = doc.translations[currentLanguage].title || doc.title;
}
// Determine if PDF download is available and get PDF path // Determine if PDF download is available and get PDF path
// First check if document has explicit download_formats.pdf // First check if document has explicit download_formats.pdf
let pdfPath = null; let pdfPath = null;
@ -213,7 +399,7 @@ function renderDocLink(doc, isHighlighted = false) {
<div class="relative mb-1"> <div class="relative mb-1">
<button class="doc-link w-full text-left px-3 py-2 ${paddingClass} rounded text-sm hover:bg-blue-50 transition ${highlightClass}" <button class="doc-link w-full text-left px-3 py-2 ${paddingClass} rounded text-sm hover:bg-blue-50 transition ${highlightClass}"
data-slug="${doc.slug}"> data-slug="${doc.slug}">
<div class="font-medium text-gray-900">${doc.title}</div> <div class="font-medium text-gray-900">${displayTitle}</div>
</button> </button>
${hasPDF ? ` ${hasPDF ? `
<a href="${pdfPath}" <a href="${pdfPath}"
@ -253,11 +439,14 @@ async function loadDocuments() {
const sortedCategories = Object.entries(CATEGORIES) const sortedCategories = Object.entries(CATEGORIES)
.sort((a, b) => a[1].order - b[1].order); .sort((a, b) => a[1].order - b[1].order);
const t = getUITranslations(currentLanguage);
sortedCategories.forEach(([categoryId, category]) => { sortedCategories.forEach(([categoryId, category]) => {
const docs = grouped[categoryId] || []; const docs = grouped[categoryId] || [];
if (docs.length === 0) return; if (docs.length === 0) return;
const isCollapsed = category.collapsed || false; const isCollapsed = category.collapsed || false;
const categoryLabel = t.categories[categoryId] || categoryId;
// Category header // Category header
html += ` html += `
@ -267,7 +456,7 @@ async function loadDocuments() {
data-collapsed="${isCollapsed}"> data-collapsed="${isCollapsed}">
<span class="flex items-center gap-2"> <span class="flex items-center gap-2">
<span class="category-icon">${category.icon}</span> <span class="category-icon">${category.icon}</span>
<span>${category.label.replace(category.icon, '').trim()}</span> <span>${categoryLabel}</span>
</span> </span>
<svg class="category-arrow w-5 h-5 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="category-arrow w-5 h-5 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
@ -420,6 +609,8 @@ async function loadDocument(slug, lang = null) {
try { try {
isLoading = true; isLoading = true;
const t = getUITranslations(language);
// Show loading state // Show loading state
const contentEl = document.getElementById('document-content'); const contentEl = document.getElementById('document-content');
contentEl.innerHTML = ` contentEl.innerHTML = `
@ -428,7 +619,7 @@ async function loadDocument(slug, lang = null) {
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle> <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg> </svg>
<p class="text-gray-600">Loading document...</p> <p class="text-gray-600">${t.loadingDocument}</p>
</div> </div>
`; `;
@ -566,12 +757,13 @@ async function loadDocument(slug, lang = null) {
} catch (error) { } catch (error) {
console.error('Error loading document:', error); console.error('Error loading document:', error);
const t = getUITranslations(currentLanguage);
document.getElementById('document-content').innerHTML = ` document.getElementById('document-content').innerHTML = `
<div class="text-center py-12"> <div class="text-center py-12">
<svg class="h-12 w-12 text-red-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> <svg class="h-12 w-12 text-red-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/> <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg> </svg>
<h3 class="text-lg font-medium text-gray-900 mb-2">Error loading document</h3> <h3 class="text-lg font-medium text-gray-900 mb-2">${t.errorLoadingDoc}</h3>
<p class="text-sm text-gray-600">${error.message}</p> <p class="text-sm text-gray-600">${error.message}</p>
</div> </div>
`; `;
@ -641,7 +833,7 @@ function closeToCModal() {
// Initialize // Initialize
loadDocuments(); loadDocuments();
// Initialize language selector // Initialize language selector and detect initial language
(function initLanguageSelector() { (function initLanguageSelector() {
const selector = document.getElementById('language-selector'); const selector = document.getElementById('language-selector');
if (!selector) return; if (!selector) return;
@ -651,8 +843,11 @@ loadDocuments();
selector.value = initialLang; selector.value = initialLang;
currentLanguage = initialLang; currentLanguage = initialLang;
// Update page UI with initial language
updatePageUI(initialLang);
// Handle language change // Handle language change
selector.addEventListener('change', (e) => { selector.addEventListener('change', async (e) => {
const newLang = e.target.value; const newLang = e.target.value;
// Save to localStorage // Save to localStorage
@ -661,16 +856,16 @@ loadDocuments();
// Update current language // Update current language
currentLanguage = newLang; currentLanguage = newLang;
// Reload current document in new language // Update all page UI elements
if (currentDocument) { updatePageUI(newLang);
loadDocument(currentDocument.slug, newLang);
} else { // Reload document list to show translated category labels and document titles
// If no document loaded yet, just update URL const currentSlug = currentDocument ? currentDocument.slug : null;
const urlParams = new URLSearchParams(window.location.search); await loadDocuments();
const currentDoc = urlParams.get('doc');
if (currentDoc) { // If a document was loaded, reload it in the new language
loadDocument(currentDoc, newLang); if (currentSlug) {
} loadDocument(currentSlug, newLang);
} }
}); });
})(); })();