feat(i18n): add German and French translations for all FAQ Q&As

- Added 28 FAQ question/answer translations to EN, DE, FR locale files
- Modified faq.js to dynamically load FAQs from i18n system
- Fixed service worker to never cache /locales/ translation files
- DeepL API used for professional German and French translations
- FAQ content now switches language correctly with language selector

Technical changes:
- Added ACTIVE_FAQ_DATA variable to faq.js for dynamic FAQ loading
- Created loadFAQsFromI18n() function with i18n event listeners
- Added /locales/ to service worker NEVER_CACHE_PATHS
- All 28 FAQs now fully translatable across all supported languages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
TheFlow 2025-10-26 14:42:31 +13:00
parent c42a81f57a
commit 9370870810
6 changed files with 1552 additions and 11 deletions

View file

@ -1425,7 +1425,7 @@ git commit -m "governance: tighten privacy boundaries for GDPR"
**Fix**:
\`\`\`javascript
// ❌ WRONG:
const client = new MongoClient('mongodb://admin:password123@localhost:27017');
const client = new MongoClient('mongodb://admin:xxx@localhost:27017');
// ✅ CORRECT:
const client = new MongoClient(process.env.MONGO_URI);
@ -2832,6 +2832,25 @@ See [Audit Guide](/downloads/implementation-guide.pdf) Section 9: "Regulatory Co
}
];
// Active FAQ data (can be overridden by i18n)
let ACTIVE_FAQ_DATA = FAQ_DATA;
/**
* Load FAQs from i18n translations if available
* Falls back to hardcoded FAQ_DATA if i18n not ready
*/
function loadFAQsFromI18n() {
if (window.i18nTranslations && window.i18nTranslations.faqs) {
ACTIVE_FAQ_DATA = window.i18nTranslations.faqs;
console.log('[FAQ] Loaded', ACTIVE_FAQ_DATA.length, 'FAQs from i18n');
return true;
} else {
ACTIVE_FAQ_DATA = FAQ_DATA;
console.log('[FAQ] Using hardcoded English FAQs (i18n not ready)');
return false;
}
}
// State management
let currentFilter = 'all';
let currentSearchQuery = '';
@ -2975,7 +2994,7 @@ function renderInlineFAQs() {
}
// Get top 6 most important FAQs (mix of all audiences)
const topFAQs = FAQ_DATA.filter(faq => [19, 12, 27, 13, 1, 2].includes(faq.id));
const topFAQs = ACTIVE_FAQ_DATA.filter(faq => [19, 12, 27, 13, 1, 2].includes(faq.id));
console.log(`[FAQ] Rendering ${topFAQs.length} inline FAQs (marked available: ${typeof marked !== 'undefined'})`);
@ -3105,6 +3124,14 @@ function setupViewAllButton() {
}
}
// Listen for i18n initialization BEFORE DOMContentLoaded
window.addEventListener('i18nInitialized', () => {
console.log('[FAQ] i18n initialized, loading FAQs');
if (loadFAQsFromI18n()) {
renderInlineFAQs();
}
});
// Initialize on page load
document.addEventListener('DOMContentLoaded', () => {
// Configure marked.js for better rendering
@ -3116,9 +3143,17 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
// Try to load FAQs from i18n (might not be ready yet)
const loaded = loadFAQsFromI18n();
// Render top 6 FAQs inline on page load
renderInlineFAQs();
// If i18n wasn't ready, the i18nInitialized event will trigger a re-render
if (!loaded) {
console.log('[FAQ] Waiting for i18n to initialize...');
}
// Setup all event listeners
setupModalListeners();
setupSearchListener();
@ -3136,9 +3171,9 @@ function renderFAQs() {
const resultsCount = document.getElementById('search-results-count');
// Filter by audience
let filtered = FAQ_DATA;
let filtered = ACTIVE_FAQ_DATA;
if (currentFilter !== 'all') {
filtered = FAQ_DATA.filter(faq => faq.audience.includes(currentFilter));
filtered = ACTIVE_FAQ_DATA.filter(faq => faq.audience.includes(currentFilter));
}
// Filter by search query
@ -3168,7 +3203,7 @@ function renderFAQs() {
// Update results count
const filterText = currentFilter === 'all' ? 'all questions' : `${currentFilter} questions`;
resultsCount.textContent = `Showing ${filtered.length} of ${FAQ_DATA.length} ${filterText}`;
resultsCount.textContent = `Showing ${filtered.length} of ${ACTIVE_FAQ_DATA.length} ${filterText}`;
console.log(`[FAQ] Rendering ${filtered.length} FAQs in modal (marked available: ${typeof marked !== 'undefined'})`);
@ -3350,3 +3385,13 @@ function setupFilterListeners() {
});
}
}
// Listen for language changes and reload FAQs
window.addEventListener('languageChanged', () => {
console.log('[FAQ] Language changed, reloading FAQs');
if (loadFAQsFromI18n()) {
renderInlineFAQs();
renderFAQs();
}
});

View file

@ -118,8 +118,6 @@ const I18n = {
// Expose translations globally for components like interactive-diagram
window.i18nTranslations = this.translations;
console.log(`[i18n] Loaded translations: common + ${pageName}`);
console.log(`[i18n] Footer translations present:`, !!this.translations.footer);
console.log(`[i18n] Footer.about_heading:`, this.translations.footer?.about_heading);
} catch (error) {
console.error(`[i18n] Error loading translations:`, error);
// Fallback to English if loading fails

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -13,7 +13,8 @@ const VERSION_CHECK_INTERVAL = 3600000; // 1 hour in milliseconds
const NEVER_CACHE_PATHS = [
'/js/admin/', // Admin JavaScript - always fresh
'/api/', // API calls
'/admin/' // Admin pages
'/admin/', // Admin pages
'/locales/' // Translation files - always fresh
];
// Assets to cache immediately on install