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:
129
html/assets/js/i18n.js
Normal file
129
html/assets/js/i18n.js
Normal 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();
|
||||
}
|
||||
Reference in New Issue
Block a user