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();
|
||||||
|
}
|
||||||
247
html/lang/README.md
Normal file
247
html/lang/README.md
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
# NebuleAir i18n System
|
||||||
|
|
||||||
|
Lightweight internationalization (i18n) system for NebuleAir web interface.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Offline-first**: Works completely offline with local JSON translation files
|
||||||
|
- **Database-backed**: Language preference stored in SQLite `config_table`
|
||||||
|
- **Automatic**: Translations apply on page load and when language changes
|
||||||
|
- **Simple API**: Easy-to-use data attributes and JavaScript API
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### 1. Include i18n.js in your HTML page
|
||||||
|
|
||||||
|
```html
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
```
|
||||||
|
|
||||||
|
The i18n system will automatically initialize when the page loads.
|
||||||
|
|
||||||
|
### 2. Add translation keys to HTML elements
|
||||||
|
|
||||||
|
Use the `data-i18n` attribute to mark elements for translation:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<h1 data-i18n="page.title">Titre en français</h1>
|
||||||
|
<p data-i18n="page.description">Description en français</p>
|
||||||
|
<button data-i18n="common.submit">Soumettre</button>
|
||||||
|
```
|
||||||
|
|
||||||
|
The text content serves as a fallback if translations aren't loaded.
|
||||||
|
|
||||||
|
### 3. Add translations to JSON files
|
||||||
|
|
||||||
|
Edit `lang/fr.json` and `lang/en.json`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"page": {
|
||||||
|
"title": "Mon Titre",
|
||||||
|
"description": "Ma description"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"submit": "Soumettre"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Translation keys use dot notation for nested objects.
|
||||||
|
|
||||||
|
## Translation Files
|
||||||
|
|
||||||
|
- **`fr.json`**: French translations (default)
|
||||||
|
- **`en.json`**: English translations
|
||||||
|
|
||||||
|
### File Structure Example
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"save": "Enregistrer",
|
||||||
|
"cancel": "Annuler",
|
||||||
|
"delete": "Supprimer"
|
||||||
|
},
|
||||||
|
"navigation": {
|
||||||
|
"home": "Accueil",
|
||||||
|
"settings": "Paramètres"
|
||||||
|
},
|
||||||
|
"sensors": {
|
||||||
|
"title": "Capteurs",
|
||||||
|
"description": "Liste des capteurs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## JavaScript API
|
||||||
|
|
||||||
|
### Get Current Language
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const currentLang = i18n.currentLang; // 'fr' or 'en'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Change Language Programmatically
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
await i18n.setLanguage('en'); // Switch to English
|
||||||
|
```
|
||||||
|
|
||||||
|
### Get Translation in JavaScript
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const translation = i18n.get('sensors.title'); // Returns translated string
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manual Translation Application
|
||||||
|
|
||||||
|
If you dynamically create HTML elements, call `applyTranslations()` after adding them to the DOM:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// Create new element
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.setAttribute('data-i18n', 'mypage.newElement');
|
||||||
|
div.textContent = 'Fallback text';
|
||||||
|
document.body.appendChild(div);
|
||||||
|
|
||||||
|
// Apply translations
|
||||||
|
i18n.applyTranslations();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Listen for Language Changes
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
document.addEventListener('languageChanged', (event) => {
|
||||||
|
console.log('Language changed to:', event.detail.language);
|
||||||
|
// Reload dynamic content, update charts, etc.
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Special Cases
|
||||||
|
|
||||||
|
### Input Placeholders
|
||||||
|
|
||||||
|
For input fields, the translation applies to the `placeholder` attribute:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<input type="text" data-i18n="form.emailPlaceholder" placeholder="Email...">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Button Values
|
||||||
|
|
||||||
|
For input buttons, the translation applies to the `value` attribute:
|
||||||
|
|
||||||
|
```html
|
||||||
|
<input type="submit" data-i18n="common.submit" value="Submit">
|
||||||
|
```
|
||||||
|
|
||||||
|
### Dynamic Content
|
||||||
|
|
||||||
|
For content created with JavaScript (like sensor cards), add `data-i18n` attributes to your template strings and call `i18n.applyTranslations()` after inserting into the DOM.
|
||||||
|
|
||||||
|
## Example: Migrating an Existing Page
|
||||||
|
|
||||||
|
### Before (French only):
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Capteurs</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>Liste des capteurs</h1>
|
||||||
|
<button onclick="getData()">Obtenir les données</button>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
### After (Multilingual):
|
||||||
|
|
||||||
|
```html
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title data-i18n="sensors.pageTitle">Capteurs</title>
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1 data-i18n="sensors.title">Liste des capteurs</h1>
|
||||||
|
<button onclick="getData()" data-i18n="common.getData">Obtenir les données</button>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add to `lang/fr.json`:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sensors": {
|
||||||
|
"pageTitle": "Capteurs",
|
||||||
|
"title": "Liste des capteurs"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"getData": "Obtenir les données"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Add to `lang/en.json`:**
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"sensors": {
|
||||||
|
"pageTitle": "Sensors",
|
||||||
|
"title": "Sensor List"
|
||||||
|
},
|
||||||
|
"common": {
|
||||||
|
"getData": "Get Data"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Backend Integration
|
||||||
|
|
||||||
|
### Get Language Preference
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch('launcher.php?type=get_language');
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(data.language); // 'fr' or 'en'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Set Language Preference
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const response = await fetch('launcher.php?type=set_language&language=en');
|
||||||
|
const data = await response.json();
|
||||||
|
console.log(data.success); // true
|
||||||
|
```
|
||||||
|
|
||||||
|
Language preference is stored in SQLite `config_table` with key `language`.
|
||||||
|
|
||||||
|
## Completed Pages
|
||||||
|
|
||||||
|
- ✅ **sensors.html** - Fully translated with French/English support
|
||||||
|
|
||||||
|
## TODO: Pages to Migrate
|
||||||
|
|
||||||
|
- ⏳ index.html
|
||||||
|
- ⏳ admin.html
|
||||||
|
- ⏳ wifi.html
|
||||||
|
- ⏳ saraR4.html
|
||||||
|
- ⏳ map.html
|
||||||
|
|
||||||
|
## Tips
|
||||||
|
|
||||||
|
1. **Reuse common translations**: Put frequently used strings (buttons, actions, status messages) in the `common` section
|
||||||
|
2. **Keep keys descriptive**: Use `sensors.bme280.title` instead of `s1` for maintainability
|
||||||
|
3. **Test both languages**: Always verify that both French and English translations display correctly
|
||||||
|
4. **Fallback text**: Always provide fallback text in HTML for graceful degradation
|
||||||
|
|
||||||
|
## Support
|
||||||
|
|
||||||
|
For issues or questions about the i18n system, refer to the implementation in:
|
||||||
|
- `/html/assets/js/i18n.js` - Core translation library
|
||||||
|
- `/html/lang/fr.json` - French translations
|
||||||
|
- `/html/lang/en.json` - English translations
|
||||||
|
- `/html/sensors.html` - Example implementation
|
||||||
53
html/lang/en.json
Normal file
53
html/lang/en.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"getData": "Get Data",
|
||||||
|
"loading": "Loading...",
|
||||||
|
"error": "Error",
|
||||||
|
"startRecording": "Start recording",
|
||||||
|
"stopRecording": "Stop recording"
|
||||||
|
},
|
||||||
|
"sensors": {
|
||||||
|
"title": "Measurement Sensors",
|
||||||
|
"description": "Your NebuleAir sensor is equipped with one or more probes that measure environmental variables. Measurements are automatic, but you can verify their operation here.",
|
||||||
|
"npm": {
|
||||||
|
"title": "NextPM",
|
||||||
|
"description": "Particulate matter sensor.",
|
||||||
|
"headerUart": "UART Port"
|
||||||
|
},
|
||||||
|
"bme280": {
|
||||||
|
"title": "BME280 Temp/Humidity Sensor",
|
||||||
|
"description": "Temperature and humidity sensor on I2C port.",
|
||||||
|
"headerI2c": "I2C Port",
|
||||||
|
"temp": "Temperature",
|
||||||
|
"hum": "Humidity",
|
||||||
|
"press": "Pressure"
|
||||||
|
},
|
||||||
|
"noise": {
|
||||||
|
"title": "Decibel Meter",
|
||||||
|
"description": "Noise sensor on I2C port.",
|
||||||
|
"headerI2c": "I2C Port"
|
||||||
|
},
|
||||||
|
"envea": {
|
||||||
|
"title": "Envea Probe",
|
||||||
|
"description": "Gas sensor."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"wifi": {
|
||||||
|
"title": "WIFI Connection",
|
||||||
|
"description": "WIFI connection is not mandatory but it allows you to perform updates and enable remote control.",
|
||||||
|
"status": "Status",
|
||||||
|
"connected": "Connected",
|
||||||
|
"hotspot": "Hotspot",
|
||||||
|
"disconnected": "Disconnected",
|
||||||
|
"scan": "Scan",
|
||||||
|
"connect": "Connect",
|
||||||
|
"enterPassword": "Enter password for"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"title": "Administration",
|
||||||
|
"parameters": "Parameters (config)",
|
||||||
|
"deviceName": "Device Name",
|
||||||
|
"deviceID": "Device ID",
|
||||||
|
"modemVersion": "Modem Version"
|
||||||
|
}
|
||||||
|
}
|
||||||
53
html/lang/fr.json
Normal file
53
html/lang/fr.json
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
{
|
||||||
|
"common": {
|
||||||
|
"getData": "Obtenir les données",
|
||||||
|
"loading": "Chargement...",
|
||||||
|
"error": "Erreur",
|
||||||
|
"startRecording": "Démarrer l'enregistrement",
|
||||||
|
"stopRecording": "Arrêter l'enregistrement"
|
||||||
|
},
|
||||||
|
"sensors": {
|
||||||
|
"title": "Les sondes de mesure",
|
||||||
|
"description": "Votre capteur NebuleAir est équipé de une ou plusieurs sondes qui permettent de mesurer certaines variables environnementales. La mesure est automatique mais vous pouvez ici vous assurer de leur bon fonctionnement.",
|
||||||
|
"npm": {
|
||||||
|
"title": "NextPM",
|
||||||
|
"description": "Capteur particules fines.",
|
||||||
|
"headerUart": "Port UART"
|
||||||
|
},
|
||||||
|
"bme280": {
|
||||||
|
"title": "Capteur Temp/Humidité BME280",
|
||||||
|
"description": "Capteur température et humidité sur le port I2C.",
|
||||||
|
"headerI2c": "Port I2C",
|
||||||
|
"temp": "Température",
|
||||||
|
"hum": "Humidité",
|
||||||
|
"press": "Pression"
|
||||||
|
},
|
||||||
|
"noise": {
|
||||||
|
"title": "Sonomètre",
|
||||||
|
"description": "Capteur bruit sur le port I2C.",
|
||||||
|
"headerI2c": "Port I2C"
|
||||||
|
},
|
||||||
|
"envea": {
|
||||||
|
"title": "Sonde Envea",
|
||||||
|
"description": "Capteur gaz."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"wifi": {
|
||||||
|
"title": "Connexion WIFI",
|
||||||
|
"description": "La connexion WIFI n'est pas obligatoire mais elle vous permet d'effectuer des mises à jour et d'activer le contrôle à distance.",
|
||||||
|
"status": "Statut",
|
||||||
|
"connected": "Connecté",
|
||||||
|
"hotspot": "Point d'accès",
|
||||||
|
"disconnected": "Déconnecté",
|
||||||
|
"scan": "Scanner",
|
||||||
|
"connect": "Se connecter",
|
||||||
|
"enterPassword": "Entrer le mot de passe pour"
|
||||||
|
},
|
||||||
|
"admin": {
|
||||||
|
"title": "Administration",
|
||||||
|
"parameters": "Paramètres (config)",
|
||||||
|
"deviceName": "Nom de l'appareil",
|
||||||
|
"deviceID": "ID de l'appareil",
|
||||||
|
"modemVersion": "Version du modem"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -77,6 +77,46 @@ if ($type == "get_config_sqlite") {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET language preference from SQLite
|
||||||
|
if ($type == "get_language") {
|
||||||
|
try {
|
||||||
|
$db = new PDO("sqlite:$database_path");
|
||||||
|
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
|
||||||
|
$stmt = $db->prepare("SELECT value FROM config_table WHERE key = 'language'");
|
||||||
|
$stmt->execute();
|
||||||
|
$result = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
|
||||||
|
$language = $result ? $result['value'] : 'fr'; // Default to French
|
||||||
|
echo json_encode(['language' => $language]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['language' => 'fr', 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SET language preference in SQLite
|
||||||
|
if ($type == "set_language") {
|
||||||
|
$language = $_GET['language'];
|
||||||
|
|
||||||
|
// Validate language (only allow fr or en)
|
||||||
|
if (!in_array($language, ['fr', 'en'])) {
|
||||||
|
echo json_encode(['success' => false, 'error' => 'Invalid language']);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$db = new PDO("sqlite:$database_path");
|
||||||
|
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||||
|
|
||||||
|
$stmt = $db->prepare("UPDATE config_table SET value = ? WHERE key = 'language'");
|
||||||
|
$stmt->execute([$language]);
|
||||||
|
|
||||||
|
echo json_encode(['success' => true, 'language' => $language]);
|
||||||
|
} catch (Exception $e) {
|
||||||
|
echo json_encode(['success' => false, 'error' => $e->getMessage()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -49,8 +49,8 @@
|
|||||||
</aside>
|
</aside>
|
||||||
<!-- Main content -->
|
<!-- Main content -->
|
||||||
<main class="col-md-10 ms-sm-auto col-lg-11 offset-md-2 offset-lg-1 px-md-4">
|
<main class="col-md-10 ms-sm-auto col-lg-11 offset-md-2 offset-lg-1 px-md-4">
|
||||||
<h1 class="mt-4">Les sondes de mesure</h1>
|
<h1 class="mt-4" data-i18n="sensors.title">Les sondes de mesure</h1>
|
||||||
<p>Votre capteur NebuleAir est équipé de une ou plusieurs sondes qui permettent de mesurer certaines variables environnementales. La mesure
|
<p data-i18n="sensors.description">Votre capteur NebuleAir est équipé de une ou plusieurs sondes qui permettent de mesurer certaines variables environnementales. La mesure
|
||||||
est automatique mais vous pouvez ici vous assurer de leur bon fonctionnement.
|
est automatique mais vous pouvez ici vous assurer de leur bon fonctionnement.
|
||||||
</p>
|
</p>
|
||||||
<div class="row mb-3" id="card-container"></div>
|
<div class="row mb-3" id="card-container"></div>
|
||||||
@@ -64,6 +64,8 @@
|
|||||||
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
<script src="assets/jquery/jquery-3.7.1.min.js"></script>
|
||||||
<!-- Link Bootstrap JS and Popper.js locally -->
|
<!-- Link Bootstrap JS and Popper.js locally -->
|
||||||
<script src="assets/js/bootstrap.bundle.js"></script>
|
<script src="assets/js/bootstrap.bundle.js"></script>
|
||||||
|
<!-- i18n translation system -->
|
||||||
|
<script src="assets/js/i18n.js"></script>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
@@ -313,13 +315,13 @@ error: function(xhr, status, error) {
|
|||||||
const cardHTML = `
|
const cardHTML = `
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header" data-i18n="sensors.npm.headerUart">
|
||||||
Port UART
|
Port UART
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">NextPM</h5>
|
<h5 class="card-title" data-i18n="sensors.npm.title">NextPM</h5>
|
||||||
<p class="card-text">Capteur particules fines.</p>
|
<p class="card-text" data-i18n="sensors.npm.description">Capteur particules fines.</p>
|
||||||
<button class="btn btn-primary" onclick="getNPM_values('ttyAMA5')">Get Data</button>
|
<button class="btn btn-primary" onclick="getNPM_values('ttyAMA5')" data-i18n="common.getData">Get Data</button>
|
||||||
<br>
|
<br>
|
||||||
<div id="loading_ttyAMA5" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
<div id="loading_ttyAMA5" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
<table class="table table-striped-columns">
|
<table class="table table-striped-columns">
|
||||||
@@ -337,13 +339,13 @@ error: function(xhr, status, error) {
|
|||||||
const i2C_BME_HTML = `
|
const i2C_BME_HTML = `
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header" data-i18n="sensors.bme280.headerI2c">
|
||||||
Port I2C
|
Port I2C
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">BME280 Temp/Hum sensor</h5>
|
<h5 class="card-title" data-i18n="sensors.bme280.title">BME280 Temp/Hum sensor</h5>
|
||||||
<p class="card-text">Capteur température et humidité sur le port I2C.</p>
|
<p class="card-text" data-i18n="sensors.bme280.description">Capteur température et humidité sur le port I2C.</p>
|
||||||
<button class="btn btn-primary mb-1" onclick="getBME280_values()">Get Data</button>
|
<button class="btn btn-primary mb-1" onclick="getBME280_values()" data-i18n="common.getData">Get Data</button>
|
||||||
<br>
|
<br>
|
||||||
<div id="loading_BME280" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
<div id="loading_BME280" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
<table class="table table-striped-columns">
|
<table class="table table-striped-columns">
|
||||||
@@ -361,16 +363,16 @@ error: function(xhr, status, error) {
|
|||||||
const i2C_HTML = `
|
const i2C_HTML = `
|
||||||
<div class="col-sm-3">
|
<div class="col-sm-3">
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<div class="card-header">
|
<div class="card-header" data-i18n="sensors.noise.headerI2c">
|
||||||
Port I2C
|
Port I2C
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Decibel Meter</h5>
|
<h5 class="card-title" data-i18n="sensors.noise.title">Decibel Meter</h5>
|
||||||
<p class="card-text">Capteur bruit sur le port I2C.</p>
|
<p class="card-text" data-i18n="sensors.noise.description">Capteur bruit sur le port I2C.</p>
|
||||||
<button class="btn btn-primary mb-1" onclick="getNoise_values()">Get Data</button>
|
<button class="btn btn-primary mb-1" onclick="getNoise_values()" data-i18n="common.getData">Get Data</button>
|
||||||
<br>
|
<br>
|
||||||
<button class="btn btn-success" onclick="startNoise()">Start recording</button>
|
<button class="btn btn-success" onclick="startNoise()" data-i18n="common.startRecording">Start recording</button>
|
||||||
<button class="btn btn-danger" onclick="stopNoise()">Stop recording</button>
|
<button class="btn btn-danger" onclick="stopNoise()" data-i18n="common.stopRecording">Stop recording</button>
|
||||||
<div id="loading_noise" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
<div id="loading_noise" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
<table class="table table-striped-columns">
|
<table class="table table-striped-columns">
|
||||||
<tbody id="data-table-body_noise"></tbody>
|
<tbody id="data-table-body_noise"></tbody>
|
||||||
@@ -409,8 +411,8 @@ error: function(xhr, status, error) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h5 class="card-title">Sonde Envea ${name}</h5>
|
<h5 class="card-title">Sonde Envea ${name}</h5>
|
||||||
<p class="card-text">Capteur gas.</p>
|
<p class="card-text" data-i18n="sensors.envea.description">Capteur gas.</p>
|
||||||
<button class="btn btn-primary" onclick="getENVEA_values('${port}','${name}')">Get Data</button>
|
<button class="btn btn-primary" onclick="getENVEA_values('${port}','${name}')" data-i18n="common.getData">Get Data</button>
|
||||||
<div id="loading_envea${name}" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
<div id="loading_envea${name}" class="spinner-border spinner-border-sm" style="display: none;" role="status"></div>
|
||||||
<table class="table table-striped-columns">
|
<table class="table table-striped-columns">
|
||||||
<tbody id="data-table-body_envea${name}"></tbody>
|
<tbody id="data-table-body_envea${name}"></tbody>
|
||||||
@@ -421,6 +423,9 @@ error: function(xhr, status, error) {
|
|||||||
container.innerHTML += cardHTML; // Ajouter la carte au conteneur
|
container.innerHTML += cardHTML; // Ajouter la carte au conteneur
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Apply translations to dynamically created Envea cards
|
||||||
|
i18n.applyTranslations();
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
@@ -432,6 +437,9 @@ error: function(xhr, status, error) {
|
|||||||
|
|
||||||
}//end if envea
|
}//end if envea
|
||||||
|
|
||||||
|
// Apply translations to all dynamically created sensor cards
|
||||||
|
i18n.applyTranslations();
|
||||||
|
|
||||||
} // end createSensorCards function
|
} // end createSensorCards function
|
||||||
|
|
||||||
//get local RTC
|
//get local RTC
|
||||||
|
|||||||
@@ -12,6 +12,12 @@
|
|||||||
<span id="pageTitle_plus_ID" class="position-absolute top-50 start-50 translate-middle">Texte au milieu</span>
|
<span id="pageTitle_plus_ID" class="position-absolute top-50 start-50 translate-middle">Texte au milieu</span>
|
||||||
-->
|
-->
|
||||||
|
|
||||||
|
<!-- Language Switcher -->
|
||||||
|
<select class="form-select form-select-sm me-2" id="languageSwitcher" style="width: auto; background-color: transparent; color: white; border-color: white;" onchange="i18n.setLanguage(this.value)">
|
||||||
|
<option value="fr">🇫🇷 FR</option>
|
||||||
|
<option value="en">🇬🇧 EN</option>
|
||||||
|
</select>
|
||||||
|
|
||||||
<button type="button" class="btn btn-outline-light" id="RTC_time">loading...</button>
|
<button type="button" class="btn btn-outline-light" id="RTC_time">loading...</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -50,7 +50,8 @@ config_entries = [
|
|||||||
("BME280", "0", "bool"),
|
("BME280", "0", "bool"),
|
||||||
("MPPT", "0", "bool"),
|
("MPPT", "0", "bool"),
|
||||||
("NOISE", "0", "bool"),
|
("NOISE", "0", "bool"),
|
||||||
("modem_version", "XXX", "str")
|
("modem_version", "XXX", "str"),
|
||||||
|
("language", "fr", "str")
|
||||||
]
|
]
|
||||||
|
|
||||||
for key, value, value_type in config_entries:
|
for key, value, value_type in config_entries:
|
||||||
|
|||||||
Reference in New Issue
Block a user