Files
twothatit 41ba835a99 Add i18n, branding, user management, health checks, and cleanup for deployment
- Multi-language support (EN/DE) with i18n engine and language files
- Configurable branding (name, subtitle, logo) in Settings
- Global default language and per-user language preference
- User management router with CRUD endpoints
- Customer status sync on start/stop/restart
- Health check fixes: derive status from container state, remove broken wget healthcheck
- Caddy reverse proxy and dashboard env templates for customer stacks
- Updated README with real hardware specs, prerequisites, and new features
- Removed .claude settings (JWT tokens) and build artifacts from tracking
- Updated .gitignore for .claude/ and Windows artifacts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-08 17:24:05 +01:00

101 lines
3.4 KiB
JavaScript

/**
* i18n - Internationalization for NetBird MSP Appliance
* Supports: English (en), German (de)
*/
let currentLanguage = null;
let systemDefaultLanguage = 'en';
const translations = {};
const SUPPORTED_LANGS = ['en', 'de'];
function setSystemDefault(lang) {
if (SUPPORTED_LANGS.includes(lang)) {
systemDefaultLanguage = lang;
}
}
function detectLanguage() {
const stored = localStorage.getItem('language');
if (stored && SUPPORTED_LANGS.includes(stored)) return stored;
// Fall back to system default (from server settings)
if (systemDefaultLanguage && SUPPORTED_LANGS.includes(systemDefaultLanguage)) return systemDefaultLanguage;
const browser = (navigator.language || '').toLowerCase();
if (browser.startsWith('de')) return 'de';
return 'en';
}
async function loadLanguage(lang) {
if (translations[lang]) return;
try {
const resp = await fetch(`/static/lang/${lang}.json`);
if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
translations[lang] = await resp.json();
} catch (err) {
console.error(`i18n: failed to load ${lang}`, err);
if (lang !== 'en') await loadLanguage('en');
}
}
function t(key, params) {
const lang = currentLanguage || 'en';
const dict = translations[lang] || translations['en'] || {};
let value = key.split('.').reduce((o, k) => (o && o[k] !== undefined) ? o[k] : null, dict);
if (value === null && lang !== 'en') {
const en = translations['en'] || {};
value = key.split('.').reduce((o, k) => (o && o[k] !== undefined) ? o[k] : null, en);
}
if (value === null) return key;
if (params && typeof value === 'string') {
value = value.replace(/\{(\w+)\}/g, (m, p) => params[p] !== undefined ? params[p] : m);
}
return value;
}
function applyTranslations() {
document.querySelectorAll('[data-i18n]').forEach(el => {
el.textContent = t(el.getAttribute('data-i18n'));
});
document.querySelectorAll('[data-i18n-placeholder]').forEach(el => {
el.placeholder = t(el.getAttribute('data-i18n-placeholder'));
});
document.querySelectorAll('[data-i18n-title]').forEach(el => {
el.title = t(el.getAttribute('data-i18n-title'));
});
document.querySelectorAll('[data-i18n-html]').forEach(el => {
el.innerHTML = t(el.getAttribute('data-i18n-html'));
});
}
function updateLanguageSwitcher() {
const btn = document.getElementById('language-switcher-btn');
if (btn) btn.textContent = (currentLanguage || 'en').toUpperCase();
document.querySelectorAll('[data-lang]').forEach(el => {
el.classList.toggle('active', el.getAttribute('data-lang') === currentLanguage);
});
}
async function setLanguage(lang) {
if (!SUPPORTED_LANGS.includes(lang)) lang = 'en';
if (!translations[lang]) await loadLanguage(lang);
currentLanguage = lang;
localStorage.setItem('language', lang);
document.documentElement.lang = lang;
updateLanguageSwitcher();
applyTranslations();
}
function getCurrentLanguage() {
return currentLanguage || 'en';
}
async function initI18n() {
const lang = detectLanguage();
await loadLanguage('en');
if (lang !== 'en') await loadLanguage(lang);
currentLanguage = lang;
document.documentElement.lang = lang;
updateLanguageSwitcher();
applyTranslations();
document.body.classList.remove('i18n-loading');
}