Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

MyAstroBoard - Internationalization (i18n) System

Overview

MyAstroBoard implements a structured multilingual system supporting English, French, Spanish, German, Italian, and Portuguese. The i18n system ensures user-facing content can be translated while preventing hardcoded strings.

Current Status: ✅ Active implementation for weather alerts, astro-weather dynamic content, and events API translations.

Architecture

Components

MyAstroBoard i18n System
│
├── Frontend (JavaScript)
│   ├── static/js/i18n.js               # Global i18n manager
│   ├── static/i18n/en.json             # English translations
│   ├── static/i18n/fr.json             # French translations
│   ├── static/i18n/xx.json             # Other translations
│   ├── static/manifest.webmanifest     # General webmanifest (English)
│   └── static/manifest.xx.webmanifest  # Translated webmanifest
│
└── Backend (Python)
    └── backend/utils/i18n_utils.py     # Translation utilities

Language Support

LanguageCodeStatusTranslationType
Englishen✅ ActiveHumanDefault
Frenchfr✅ ActiveHuman-
Spanishes✅ ActiveGoogle*-
Germande✅ ActiveGoogle*-
Italianit✅ ActiveGoogle*-
Portuguesept✅ ActiveGoogle*-

Note: Google is used in scripts/translate_i18n_values.py

Frontend Implementation

i18n.js - The Global Manager

The file static/js/i18n.js:

  • Loads translation files from static/i18n/[language].json
  • Detects browser language automatically
  • Manages language switching with persistent storage (localStorage)
  • Handles placeholders in translation strings
  • Provides fallback to English for missing keys
  • Dispatches events when language changes

Using Translations in HTML

<h2 data-i18n="astro_weather.section_title">Current Conditions</h2>
<p data-i18n="common.loading">Loading...</p>

Using Translations in JavaScript

const msg = i18n.t('common.loading');
const alert = i18n.t('weather_alerts.critical_dew_risk', { time: '14:30' });
const lang = i18n.getCurrentLanguage();
await i18n.setLanguage('fr');

Language Selector

Files involved:

  • templates/index.html
  • static/js/language_selector.js

Features:

  • ✅ Dropdown selector for English, French, Spanish, German, Italian, Portuguese
  • ✅ Automatic current-language selection
  • ✅ URL/localStorage persistence
  • ✅ i18nLanguageChanged event propagation
  • ✅ Responsive behavior

Backend Implementation

i18n_utils.py

The module backend/utils/i18n_utils.py provides:

  • Translation loading from JSON files
  • Dot-notation key lookup
  • Placeholder replacement
  • Language management per request
  • Alert creation helpers with translated content

Example:

from i18n_utils import I18nManager, get_translated_message

msg = get_translated_message('common.loading', language='fr')
manager = I18nManager('en')
caption = manager.t('weather_alerts.critical_dew_risk', time='14:30')

API Language Contract

For endpoints returning translated user-facing content:

  • Frontend sends current language via ?lang=<code>
  • Backend may fallback to Accept-Language, then default to en
  • Backend resolves placeholders before serialization
  • Responses should not return hardcoded English where i18n keys exist

Implemented examples:

  • /api/weather/alerts?lang=fr
  • /api/weather/astro-analysis?hours=24&lang=fr
  • /api/events/upcoming?lang=fr

Exception - MQTT: the MQTT connector’s background publish loop (backend/connectors/mqtt_payloads.py) has no request or session to read a language from, so next_event’s title/description are hardcoded to en and always render English prose, on purpose - this is not a bug. MQTT consumers that want localized text (e.g. a Home Assistant Lovelace card) should translate client-side off the always-English, always-untranslated structured attributes instead: event_type, and the per-event-kind fields eclipse_type, planet/planet2, shower_name, comet_name on next_event_attributes (see docs/HOME_ASSISTANT.md). top_target_attributes.object_type is the same pattern, already relied on by the card today.

Translation Keys Structure

Keys use dot notation:

namespace.section.key

Example:

{
  "common": {
    "loading": "Loading...",
    "error": "Error"
  },
  "astro_weather": {
    "section_title": "Current Conditions",
    "loading_message": "Loading...",
    "alerts_title": "Weather Alerts"
  }
}

Add new translation

Translate all keys

First you need to translate all json key stored in static/i18n

You can copy en.json as reference or use Google translation script, which will automatically translate and create the json.

py scripts/translate_i18n_values.py --lang pt

By safety, the language must be manually add in this file first.

parser.add_argument("--lang", choices=["es", "de", "it", "pt"], required=True)

After a long time, you will receive a feedback. Please be carefull with it!

stats: translated=1223 kept=186 failed=2

You can also make a quick check to see if there is no missing translation.

py scripts/translate_checker.py

Of course, Google Translation must be manually checked, specially if this is your native language

Declare new translation

This is a multi-step checklist. Every item must be updated or the new language will be partially broken.

1. backend/utils/i18n_utils.py - translation file registry

# Supported languages
DEFAULT_LANGUAGE = 'en'
_TRANSLATION_FILENAMES = {
    'en': 'en.json',
    'fr': 'fr.json',
    'es': 'es.json',
    'de': 'de.json',
    'it': 'it.json',
    'pt': 'pt.json',
}

2. backend/app.py - manifest route allowlist

The web_manifest_localized() route has an explicit allowlist. Adding the language here is mandatory - missing it returns 404 and breaks the PWA service worker install.

@app.route('/manifest.<lang>.webmanifest')
def web_manifest_localized(lang):
    allowed = {'fr', 'es', 'de', 'it', 'pt'}  # <- add new language code here
    if lang not in allowed:
        return '', 404
    ...

3. static/sw.js - service worker shell cache

Add both the manifest and the i18n JSON file to APP_SHELL_URLS so they are pre-cached on PWA install:

const APP_SHELL_URLS = [
    ...
    '/manifest.xx.webmanifest',   // <- add localized manifest
    ...
    '/static/i18n/xx.json',       // <- add i18n dictionary
    ...
];

4. templates/index.html - language selector and manifest switcher

Add the language option to the footer selector:

<select id="language-select-footer" class="form-select form-select-sm language-select" aria-label="Language">
    <option value="en">English</option>
    <option value="fr">Français</option>
    <option value="es">Español</option>
    <option value="de">Deutsch</option>
    <option value="it">Italiano</option>
    <option value="pt">Português</option>
    <!-- add new language here -->
</select>

Also add the language code to the supported array in the inline <script> block that switches the PWA manifest URL:

var supported = ['fr', 'es', 'de', 'it', 'pt'];  // <- add new language code

5. scripts/translate_i18n_values.py - translation script

Add the language code to the choices list before running the script:

parser.add_argument("--lang", choices=["es", "de", "it", "pt", "xx"], required=True)

Translate webmanifest

Webmanifest is used in the PWA app - translating it gives native-language shortcut labels and app description on the home screen.

Webmanifest files are located in static/manifest.xx.webmanifest, where xx is the language code.

Be carefull, only some keys must be translated:

"description"
"lang"
"screenshots"->"label" (all)
"shortcuts"->"name" (all)
"shortcuts"->"short_name" (all)
"shortcuts"->"description" (all)

Then, translated webmanifest have to be declared on index.html

<script>
  localStorage.setItem('myastroboard_app_version', '{{ version }}');

  (function() {
    var lang = localStorage.getItem('myastroboard_language') || '';
    var supported = ['fr', 'es', 'de', 'it', 'pt'];
    if (supported.indexOf(lang) !== -1) {
      document.getElementById('pwa-manifest').href = '/manifest.' + lang + '.webmanifest?v={{ version }}';
    }
  })();
</script>

Last updated 2026-11-13