diff --git a/.env.example b/.env.example index 3127bfc..2fd3396 100644 --- a/.env.example +++ b/.env.example @@ -198,11 +198,24 @@ API_ADMIN_KEY= # External web port WEB_PORT=8080 +# API endpoint URL for the web dashboard +# Default: http://localhost:8000 +# API_BASE_URL=http://localhost:8000 + +# API key for web dashboard queries (optional) +# If API_READ_KEY is set on the API, provide it here +# API_KEY= + # Default theme for the web dashboard (dark or light) # Users can override via the theme toggle; their preference is saved in localStorage # Default: dark # WEB_THEME=dark +# Locale/language for the web dashboard +# Default: en +# Supported: en (see src/meshcore_hub/web/static/locales/ for available translations) +# WEB_LOCALE=en + # Enable admin interface at /a/ (requires auth proxy in front) # Default: false # WEB_ADMIN_ENABLED=false @@ -221,6 +234,9 @@ TZ=UTC # ------------------- # Displayed on the web dashboard homepage +# Network domain name (optional) +# NETWORK_DOMAIN= + # Network display name NETWORK_NAME=MeshCore Network diff --git a/AGENTS.md b/AGENTS.md index 7e57fd1..5d29768 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -453,13 +453,103 @@ The web dashboard is a Single Page Application. Pages are ES modules loaded by t - Use `pageColors` from `components.js` for section-specific colors (reads CSS custom properties from `app.css`) - Return a cleanup function if the page creates resources (e.g., Leaflet maps, Chart.js instances) +### Internationalization (i18n) + +The web dashboard supports internationalization via JSON translation files. The default language is English. + +**Translation files location:** `src/meshcore_hub/web/static/locales/` + +**Key files:** +- `en.json` - English translations (reference implementation) +- `languages.md` - Comprehensive translation reference guide for translators + +**Using translations in JavaScript:** + +Import the `t()` function from `components.js`: + +```javascript +import { t } from '../components.js'; + +// Simple translation +const label = t('common.save'); // "Save" + +// Translation with variable interpolation +const title = t('common.add_entity', { entity: t('entities.node') }); // "Add Node" + +// Composed patterns for consistency +const emptyMsg = t('common.no_entity_found', { entity: t('entities.nodes').toLowerCase() }); // "No nodes found" +``` + +**Translation architecture:** + +1. **Entity-based composition:** Core entity names (`entities.*`) are referenced by composite patterns for consistency +2. **Reusable patterns:** Common UI patterns (`common.*`) use `{{variable}}` interpolation for dynamic content +3. **Separation of concerns:** + - Keys without `_label` suffix = table headers (title case, no colon) + - Keys with `_label` suffix = inline labels (sentence case, with colon) + +**When adding/modifying translations:** + +1. **Add new keys** to `en.json` following existing patterns: + - Use composition when possible (reference `entities.*` in `common.*` patterns) + - Group related keys by section (e.g., `admin_members.*`, `admin_node_tags.*`) + - Use `{{variable}}` syntax for dynamic content + +2. **Update `languages.md`** with: + - Key name, English value, and usage context + - Variable descriptions if using interpolation + - Notes about HTML content or special formatting + +3. **Add tests** in `tests/test_common/test_i18n.py`: + - Test new interpolation patterns + - Test required sections if adding new top-level sections + - Test composed patterns with entity references + +4. **Run i18n tests:** + ```bash + pytest tests/test_common/test_i18n.py -v + ``` + +**Best practices:** + +- **Avoid duplication:** Use `common.*` patterns instead of duplicating similar strings +- **Compose with entities:** Reference `entities.*` keys in patterns rather than hardcoding entity names +- **Preserve variables:** Keep `{{variable}}` placeholders unchanged when translating +- **Test composition:** Verify patterns work with all entity types (singular/plural, lowercase/uppercase) +- **Document context:** Always update `languages.md` so translators understand usage + +**Example - adding a new entity and patterns:** + +```javascript +// 1. Add entity to en.json +"entities": { + "sensor": "Sensor" +} + +// 2. Use with existing common patterns +t('common.add_entity', { entity: t('entities.sensor') }) // "Add Sensor" +t('common.no_entity_found', { entity: t('entities.sensors').toLowerCase() }) // "No sensors found" + +// 3. Update languages.md with context +// 4. Add test to test_i18n.py +``` + +**Translation loading:** + +The i18n system (`src/meshcore_hub/common/i18n.py`) loads translations on startup: +- Defaults to English (`en`) +- Falls back to English for missing keys +- Returns the key itself if translation not found + +For full translation guidelines, see `src/meshcore_hub/web/static/locales/languages.md`. + ### Adding a New Database Model 1. Create model in `common/models/` 2. Export in `common/models/__init__.py` -3. Create Alembic migration: `alembic revision --autogenerate -m "description"` +3. Create Alembic migration: `meshcore-hub db revision --autogenerate -m "description"` 4. Review and adjust migration file -5. Test migration: `alembic upgrade head` +5. Test migration: `meshcore-hub db upgrade` ### Running the Development Environment @@ -481,7 +571,7 @@ pytest # Run specific component meshcore-hub api --reload meshcore-hub collector -meshcore-hub interface --mode receiver --mock +meshcore-hub interface receiver --mock ``` ## Environment Variables diff --git a/README.md b/README.md index db8c72f..bb53493 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,23 @@ Python 3.14+ platform for managing and orchestrating MeshCore mesh networks. ![MeshCore Hub Web Dashboard](docs/images/web.png) +## 🌍 Help Translate MeshCore Hub + +**We need volunteers to translate the web dashboard into other languages!** + +MeshCore Hub includes full internationalization (i18n) support with a composable translation system. We're looking for open source volunteers to contribute and maintain language packs for their native languages. + +**Current translations:** +- 🇬🇧 English (complete) + +**How to contribute:** +1. Check out the [Translation Reference Guide](src/meshcore_hub/web/static/locales/languages.md) for complete documentation +2. Copy `src/meshcore_hub/web/static/locales/en.json` to your language code (e.g., `es.json`, `fr.json`, `de.json`) +3. Translate all values while preserving variables (`{{variable}}`) and HTML tags +4. Submit a pull request with your translation + +Even partial translations are welcome! The system falls back to English for missing keys, so you can translate incrementally. + ## Overview MeshCore Hub provides a complete solution for monitoring, collecting, and interacting with MeshCore mesh networks. It consists of multiple components that work together: @@ -70,6 +87,7 @@ flowchart LR - **Command Dispatch**: Send messages and advertisements via the API - **Node Tagging**: Add custom metadata to nodes for organization - **Web Dashboard**: Visualize network status, node locations, and message history +- **Internationalization**: Full i18n support with composable translation patterns - **Docker Ready**: Single image with all components, easy deployment ## Getting Started @@ -248,7 +266,7 @@ pip install -e ".[dev]" meshcore-hub db upgrade # Start components (in separate terminals) -meshcore-hub interface --mode receiver --port /dev/ttyUSB0 +meshcore-hub interface receiver --port /dev/ttyUSB0 meshcore-hub collector meshcore-hub api meshcore-hub web @@ -339,9 +357,12 @@ The collector automatically cleans up old event data and inactive nodes: | `WEB_HOST` | `0.0.0.0` | Web server bind address | | `WEB_PORT` | `8080` | Web server port | | `API_BASE_URL` | `http://localhost:8000` | API endpoint URL | +| `API_KEY` | *(none)* | API key for web dashboard queries (optional) | | `WEB_THEME` | `dark` | Default theme (`dark` or `light`). Users can override via theme toggle in navbar. | +| `WEB_LOCALE` | `en` | Locale/language for the web dashboard (e.g., `en`, `es`, `fr`) | | `WEB_ADMIN_ENABLED` | `false` | Enable admin interface at /a/ (requires auth proxy) | | `TZ` | `UTC` | Timezone for displaying dates/times (e.g., `America/New_York`, `Europe/London`) | +| `NETWORK_DOMAIN` | *(none)* | Network domain name (optional) | | `NETWORK_NAME` | `MeshCore Network` | Display name for the network | | `NETWORK_CITY` | *(none)* | City where network is located | | `NETWORK_COUNTRY` | *(none)* | Country code (ISO 3166-1 alpha-2) | @@ -632,7 +653,9 @@ meshcore-hub/ │ ├── api/ # REST API │ └── web/ # Web dashboard │ ├── templates/ # Jinja2 templates (SPA shell) -│ └── static/js/spa/ # SPA frontend (ES modules, lit-html) +│ └── static/ +│ ├── js/spa/ # SPA frontend (ES modules, lit-html) +│ └── locales/ # Translation files (en.json, languages.md) ├── tests/ # Test suite ├── alembic/ # Database migrations ├── etc/ # Configuration files (mosquitto.conf) diff --git a/docker-compose.yml b/docker-compose.yml index c633a57..29e41cf 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -251,6 +251,8 @@ services: - API_KEY=${API_ADMIN_KEY:-${API_READ_KEY:-}} - WEB_HOST=0.0.0.0 - WEB_PORT=8080 + - WEB_THEME=${WEB_THEME:-dark} + - WEB_LOCALE=${WEB_LOCALE:-en} - WEB_ADMIN_ENABLED=${WEB_ADMIN_ENABLED:-false} - NETWORK_NAME=${NETWORK_NAME:-MeshCore Network} - NETWORK_CITY=${NETWORK_CITY:-} diff --git a/src/meshcore_hub/web/static/js/spa/pages/admin/members.js b/src/meshcore_hub/web/static/js/spa/pages/admin/members.js index 8230e9a..f03b42f 100644 --- a/src/meshcore_hub/web/static/js/spa/pages/admin/members.js +++ b/src/meshcore_hub/web/static/js/spa/pages/admin/members.js @@ -78,8 +78,8 @@ export async function render(container, params, router) { ` : html`
-

${t('admin_members.no_members_yet')}

-

${t('admin_members.no_members_hint')}

+

${t('common.no_entity_yet', { entity: t('entities.members').toLowerCase() })}

+

${t('admin_members.empty_state_hint')}

`; litRender(html` @@ -208,10 +208,10 @@ ${flashHtml} @@ -124,7 +124,7 @@ export async function render(container, params, router) {
-

${t('admin_node_tags.tags_count', { count: tags.length })}

+

${t('entities.tags')} (${tags.length})

${tagsTableHtml}
@@ -188,7 +188,7 @@ export async function render(container, params, router) {