Add comprehensive i18n support for Norwegian and English

- Add language toggle button with EN/NO options in top-right corner
- Implement complete translation system with localStorage persistence
- Add proper Norwegian translations with correct capitalization
- Support real-time language switching without page reload
- Integrate translations with GDPR consent system and notifications
- Update all UI text, modal content, and status messages
- Fix JavaScript syntax errors with escaped apostrophes
- Follow Norwegian writing conventions for compound words

Features:
• Language preference persisted between visits
• All content translates instantly when switching
• Modal consent flow fully localized
• Button text and notifications use selected language
• HTML lang attribute updates for accessibility

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2025-09-08 13:14:50 +02:00
commit 45b4248662

View file

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" id="htmlRoot">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@ -152,6 +152,45 @@
background: #ff9800;
}
.language-toggle {
position: fixed;
top: 20px;
right: 20px;
background: rgba(255, 255, 255, 0.9);
backdrop-filter: blur(10px);
padding: 8px;
border-radius: 20px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
z-index: 998;
display: flex;
gap: 4px;
}
.lang-btn {
padding: 6px 12px;
border: none;
border-radius: 15px;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
background: transparent;
color: #666;
}
.lang-btn.active {
background: #667eea;
color: white;
}
.lang-btn:hover {
background: rgba(102, 126, 234, 0.1);
}
.lang-btn.active:hover {
background: #5a6fd8;
}
@media (max-width: 768px) {
.hero h1 {
font-size: 2.5rem;
@ -166,6 +205,10 @@
position: static;
margin-bottom: 2rem;
}
.language-toggle {
position: static;
margin-bottom: 1rem;
}
}
.tracking-consent-container {
position: fixed;
@ -279,6 +322,11 @@
</style>
</head>
<body>
<div class="language-toggle">
<button class="lang-btn active" onclick="setLanguage('en')" id="langEn">EN</button>
<button class="lang-btn" onclick="setLanguage('no')" id="langNo">NO</button>
</div>
<div class="tracking-status" id="trackingStatus">
<div class="status-dot minimal" id="statusDot"></div>
<span id="statusText">Privacy Mode: Minimal Tracking</span>
@ -378,6 +426,164 @@
</div>
<script>
// Internationalization
const translations = {
en: {
title: '🍪 Privacy-First Demo | GDPR Compliant Tracking',
description: 'Experience GDPR-compliant optional tracking. Privacy protected by default with enhanced features available only when you choose to enable them.',
statusMinimal: 'Privacy Mode: Minimal Tracking',
statusEnhanced: 'Privacy Mode: Enhanced Tracking',
gdprBadge: 'GDPR Compliant by Default',
heroTitle: 'Privacy-First Demo',
heroSubtitle: 'Experience truly optional tracking. Your privacy is protected by default,<br>with enhanced features available only when <em>you</em> choose to enable them.',
howItWorks: 'How It Works',
howItWorksDesc: 'This demo shows how modern websites can respect your privacy while still providing great experiences:',
featurePrivacy: 'Default: Privacy Protected',
featurePrivacyDesc: 'Only essential cookies, anonymized analytics',
featureTracking: 'Optional: Enhanced Tracking',
featureTrackingDesc: 'Detailed analytics, personalization when you opt-in',
featureControl: 'Full Control',
featureControlDesc: 'Toggle anytime with the button below',
featureCookies: 'Smart Cookie Management',
featureCookiesDesc: 'Automatic cleanup when disabled',
tryItOut: 'Try it out!',
tryItOutDesc: 'Click the colorful button in the bottom-right corner to see the consent flow in action. 👇',
wantToUse: 'Want to use this in your project?',
viewSource: 'View source code and documentation at',
buttonText: 'Plz trac me!',
buttonTextOn: 'Tracking ON',
modalTitle: 'Optional Enhanced Tracking',
modalDesc: 'We respect your privacy! By default, we only use essential cookies and basic analytics. If you would like to help us improve our service with enhanced tracking (detailed analytics, personalization, marketing cookies), you can opt in below.',
modalWhatWeTrack: 'What we will track:',
modalItem1: '• Detailed page interactions',
modalItem2: '• User preferences for personalization',
modalItem3: '• Marketing campaign effectiveness',
modalItem4: '• Enhanced usage analytics',
modalCanChange: 'You can change your mind anytime by clicking the tracking button again.',
modalNo: 'No, thanks',
modalYes: 'Yes, track me!',
notificationEnabled: 'Enhanced tracking enabled! Thanks for helping us improve.',
notificationDisabled: 'Switched to minimal tracking. Your privacy is protected.',
confirmDisable: 'Disable enhanced tracking? We will switch back to minimal analytics only.'
},
no: {
title: '🍪 Personvern-først demo | GDPR-kompatibel sporing',
description: 'Opplev GDPR-kompatibel valgfri sporing. Personvernet er beskyttet som standard med utvidede funksjoner tilgjengelig kun når du velger å aktivere dem.',
statusMinimal: 'Personvernmodus: minimal sporing',
statusEnhanced: 'Personvernmodus: utvidet sporing',
gdprBadge: 'GDPR-kompatibelt som standard',
heroTitle: 'Personvern-først demo',
heroSubtitle: 'Opplev virkelig valgfri sporing. Ditt personvern er beskyttet som standard,<br>med utvidede funksjoner tilgjengelig kun når <em>du</em> velger å aktivere dem.',
howItWorks: 'Hvordan det fungerer',
howItWorksDesc: 'Denne demoen viser hvordan moderne nettsteder kan respektere ditt personvern mens de fortsatt gir gode opplevelser:',
featurePrivacy: 'Standard: personvernbeskyttelse',
featurePrivacyDesc: 'Kun essensielle cookies, anonymisert analyse',
featureTracking: 'Valgfritt: utvidet sporing',
featureTrackingDesc: 'Detaljert analyse, personalisering når du velger det',
featureControl: 'Full kontroll',
featureControlDesc: 'Slå på/av når som helst med knappen nedenfor',
featureCookies: 'Smart cookie-håndtering',
featureCookiesDesc: 'Automatisk opprydding når deaktivert',
tryItOut: 'Prøv det ut!',
tryItOutDesc: 'Klikk på den fargerike knappen i nederste høyre hjørne for å se samtykke-flyten i aksjon. 👇',
wantToUse: 'Vil du bruke dette i ditt prosjekt?',
viewSource: 'Se kildekode og dokumentasjon på',
buttonText: 'Spor meg takk!',
buttonTextOn: 'Sporing PÅ',
modalTitle: 'Valgfri utvidet sporing',
modalDesc: 'Vi respekterer ditt personvern! Som standard bruker vi kun essensielle cookies og grunnleggende analyse. Hvis du ønsker å hjelpe oss forbedre tjenesten vår med utvidet sporing (detaljert analyse, personalisering, markedsføringscookies), kan du velge det nedenfor.',
modalWhatWeTrack: 'Hva vi vil spore:',
modalItem1: '• Detaljerte sideinteraksjoner',
modalItem2: '• Brukerpreferanser for personalisering',
modalItem3: '• Effektivitet av markedsføringskampanjer',
modalItem4: '• Utvidet bruksanalyse',
modalCanChange: 'Du kan ombestemme deg når som helst ved å klikke på sporingsknappen igjen.',
modalNo: 'Nei takk',
modalYes: 'Ja, spor meg!',
notificationEnabled: 'Utvidet sporing aktivert! Takk for at du hjelper oss forbedre.',
notificationDisabled: 'Byttet til minimal sporing. Ditt personvern er beskyttet.',
confirmDisable: 'Deaktiver utvidet sporing? Vi bytter tilbake til kun minimal analyse.'
}
};
let currentLang = localStorage.getItem('gdpr-language') || 'en';
function setLanguage(lang) {
currentLang = lang;
localStorage.setItem('gdpr-language', lang);
document.getElementById('htmlRoot').lang = lang;
// Update button states
document.getElementById('langEn').classList.toggle('active', lang === 'en');
document.getElementById('langNo').classList.toggle('active', lang === 'no');
updateTranslations();
// Update tracking consent if it exists
if (window.trackingConsent) {
window.trackingConsent.updateButtonState();
}
}
function updateTranslations() {
const t = translations[currentLang];
// Update page title and meta
document.title = t.title;
document.querySelector('meta[name="description"]').content = t.description;
// Update status text
const statusText = document.getElementById('statusText');
const isTracking = localStorage.getItem('gdpr-tracking-consent') === 'true';
statusText.textContent = isTracking ? t.statusEnhanced : t.statusMinimal;
// Update all text elements
const updates = {
'.privacy-badge span': t.gdprBadge,
'.hero h1': t.heroTitle,
'.hero .subtitle': t.heroSubtitle,
'.demo-info h2': '🎯 ' + t.howItWorks,
'.demo-info > p': t.howItWorksDesc,
'.feature:nth-child(1) strong': t.featurePrivacy,
'.feature:nth-child(1) small': t.featurePrivacyDesc,
'.feature:nth-child(2) strong': t.featureTracking,
'.feature:nth-child(2) small': t.featureTrackingDesc,
'.feature:nth-child(3) strong': t.featureControl,
'.feature:nth-child(3) small': t.featureControlDesc,
'.feature:nth-child(4) strong': t.featureCookies,
'.feature:nth-child(4) small': t.featureCookiesDesc,
'#consentModal h3': t.modalTitle,
'.consent-btn.secondary': t.modalNo,
'.consent-btn.primary': t.modalYes
};
Object.entries(updates).forEach(([selector, text]) => {
const element = document.querySelector(selector);
if (element) element.innerHTML = text;
});
// Update complex elements
const tryItOutP = document.querySelector('.demo-info p:last-of-type');
if (tryItOutP && tryItOutP.querySelector('strong')) {
tryItOutP.innerHTML = `<strong>${t.tryItOut}</strong> ${t.tryItOutDesc}`;
}
const projectLink = document.querySelector('.demo-info > div:last-child p');
if (projectLink) {
projectLink.innerHTML = `💡 <strong>${t.wantToUse}</strong><br>${t.viewSource} <a href="https://kode.naiv.no/olemd/gdpr" target="_blank" rel="noopener" style="color: #667eea; text-decoration: none; font-weight: 600;">kode.naiv.no/olemd/gdpr</a>`;
}
const modalP = document.querySelector('#consentModal p:first-of-type');
if (modalP) {
modalP.innerHTML = t.modalDesc + '<br><br><strong>' + t.modalWhatWeTrack + '</strong><br>' + t.modalItem1 + '<br>' + t.modalItem2 + '<br>' + t.modalItem3 + '<br>' + t.modalItem4;
}
const modalLastP = document.querySelector('#consentModal p:last-of-type');
if (modalLastP && modalLastP.textContent.includes('change your mind')) {
modalLastP.textContent = t.modalCanChange;
}
}
class GDPRTrackingConsent {
constructor() {
this.storageKey = 'gdpr-tracking-consent';
@ -429,16 +635,17 @@
const btnText = document.getElementById('btnText');
const statusText = document.getElementById('statusText');
const statusDot = document.getElementById('statusDot');
const t = translations[currentLang];
if (this.isTrackingEnabled) {
btn.classList.add('enabled');
btnText.textContent = 'Tracking ON';
statusText.textContent = 'Privacy Mode: Enhanced Tracking';
btnText.textContent = t.buttonTextOn;
statusText.textContent = t.statusEnhanced;
statusDot.className = 'status-dot';
} else {
btn.classList.remove('enabled');
btnText.textContent = 'Plz trac me!';
statusText.textContent = 'Privacy Mode: Minimal Tracking';
btnText.textContent = t.buttonText;
statusText.textContent = t.statusMinimal;
statusDot.className = 'status-dot minimal';
}
}
@ -452,7 +659,8 @@
}
showDisableConfirm() {
if (confirm('Disable enhanced tracking? We\'ll switch back to minimal analytics only.')) {
const t = translations[currentLang];
if (confirm(t.confirmDisable)) {
this.disableTracking();
}
}
@ -463,7 +671,7 @@
this.closeConsentModal();
this.enableEnhancedTracking();
this.showNotification('Enhanced tracking enabled! Thanks for helping us improve.');
this.showNotification(translations[currentLang].notificationEnabled);
}
disableTracking() {
@ -471,7 +679,7 @@
this.updateButtonState();
this.enableMinimalTracking();
this.showNotification('Switched to minimal tracking. Your privacy is protected.');
this.showNotification(translations[currentLang].notificationDisabled);
}
enableMinimalTracking() {
@ -560,6 +768,9 @@
}
document.addEventListener('DOMContentLoaded', () => {
// Initialize language
setLanguage(currentLang);
window.trackingConsent = new GDPRTrackingConsent();
});