Implement lightweight offline i18n system with French/English support

**Core System:**
- Add i18n.js translation library with data-attribute support
- Create translation files (fr.json, en.json) with offline support
- Store language preference in SQLite config_table
- Add backend endpoints for get/set language

**UI Features:**
- Add language switcher dropdown to topbar (🇫🇷 FR / 🇬🇧 EN)
- Auto-sync language selection across all pages
- Support for static HTML and dynamically created elements

**Implementation:**
- Migrate sensors.html as working example
- Add data-i18n attributes to all UI elements
- Support for buttons, inputs, and dynamic content
- Comprehensive README documentation in html/lang/

**Technical Details:**
- Works completely offline (local JSON files)
- No external dependencies
- Database-backed user preference
- Event-based language change notifications
- Automatic translation on page load

Next steps: Gradually migrate other pages (admin, wifi, index, etc.)

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
PaulVua
2026-01-05 18:10:06 +01:00
parent 906eaa851d
commit 163d60bf34
8 changed files with 561 additions and 24 deletions

129
html/assets/js/i18n.js Normal file
View File

@@ -0,0 +1,129 @@
/**
* NebuleAir i18n - Lightweight internationalization system
* Works offline with local JSON translation files
* Stores language preference in SQLite database
*/
const i18n = {
currentLang: 'fr', // Default language
translations: {},
/**
* Initialize i18n system
* Loads language preference from server and applies translations
*/
async init() {
try {
// Load language preference from server (SQLite database)
const response = await fetch('launcher.php?type=get_language');
const data = await response.json();
this.currentLang = data.language || 'fr';
} catch (error) {
console.warn('Could not load language preference, using default (fr):', error);
this.currentLang = 'fr';
}
// Load translations and apply
await this.loadTranslations(this.currentLang);
this.applyTranslations();
},
/**
* Load translation file for specified language
* @param {string} lang - Language code (fr, en)
*/
async loadTranslations(lang) {
try {
const response = await fetch(`lang/${lang}.json`);
this.translations = await response.json();
console.log(`Translations loaded for: ${lang}`);
} catch (error) {
console.error(`Failed to load translations for ${lang}:`, error);
}
},
/**
* Apply translations to all elements with data-i18n attribute
*/
applyTranslations() {
document.querySelectorAll('[data-i18n]').forEach(element => {
const key = element.getAttribute('data-i18n');
const translation = this.get(key);
if (translation) {
// Handle different element types
if (element.tagName === 'INPUT' || element.tagName === 'TEXTAREA') {
if (element.type === 'button' || element.type === 'submit') {
element.value = translation;
} else {
element.placeholder = translation;
}
} else {
element.textContent = translation;
}
} else {
console.warn(`Translation not found for key: ${key}`);
}
});
// Update HTML lang attribute
document.documentElement.lang = this.currentLang;
// Update language switcher dropdown
const languageSwitcher = document.getElementById('languageSwitcher');
if (languageSwitcher) {
languageSwitcher.value = this.currentLang;
}
},
/**
* Get translation by key (supports nested keys with dot notation)
* @param {string} key - Translation key (e.g., 'sensors.title')
* @returns {string} Translated string or key if not found
*/
get(key) {
const keys = key.split('.');
let value = this.translations;
for (const k of keys) {
if (value && typeof value === 'object' && k in value) {
value = value[k];
} else {
return key; // Return key if translation not found
}
}
return value;
},
/**
* Change language and reload translations
* @param {string} lang - Language code (fr, en)
*/
async setLanguage(lang) {
if (lang === this.currentLang) return;
this.currentLang = lang;
// Save to server (SQLite database)
try {
await fetch(`launcher.php?type=set_language&language=${lang}`);
} catch (error) {
console.error('Failed to save language preference:', error);
}
// Reload translations and apply
await this.loadTranslations(lang);
this.applyTranslations();
// Emit custom event for other scripts to react to language change
document.dispatchEvent(new CustomEvent('languageChanged', { detail: { language: lang } }));
}
};
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => i18n.init());
} else {
i18n.init();
}