diff --git a/docs/i18n.md b/docs/i18n.md index 5c2f531a..fdcb510c 100644 --- a/docs/i18n.md +++ b/docs/i18n.md @@ -52,7 +52,7 @@ A file looks like this: ```yaml _language_name: "Español" -_language_code: "SPANISH" +_language_code: "ES" _order: "1" STR_CROSSPOINT: "CrossPoint" @@ -62,7 +62,7 @@ STR_BROWSE_FILES: "Buscar archivos" **Metadata keys** (prefixed with `_`): - `_language_name` — Native display name shown to the user (e.g. "Français") -- `_language_code` — C++ enum name (e.g. "FRENCH"). Must be a valid C++ identifier. +- `_language_code` — C++ enum name (e.g. "FR"). Please use the [ISO Code](https://en.wikipedia.org/wiki/List_of_ISO_639_language_codes) of the language. Must be a valid C++ identifier. - `_order` — Controls the position in the Language enum (English is always 0) **Rules:** @@ -128,7 +128,7 @@ Create `lib/I18n/translations/italian.yaml`: ```yaml _language_name: "Italiano" -_language_code: "ITALIAN" +_language_code: "IT" _order: "7" STR_CROSSPOINT: "CrossPoint" @@ -175,7 +175,7 @@ renderer.drawText(font, x, y, tr(STR_BROWSE_FILES)); Serial.printf("Status: %s\n", tr(STR_CONNECTED)); // I18N - Shorthand for I18n::getInstance() -I18N.setLanguage(Language::SPANISH); +I18N.setLanguage(Language::ES); Language lang = I18N.getLanguage(); // === Full API === @@ -189,7 +189,7 @@ const char* text = I18N.get(StrId::STR_SETTINGS_TITLE); // Direct call const char* text = I18N[StrId::STR_SETTINGS_TITLE]; // Operator overload // Set language -I18N.setLanguage(Language::SPANISH); +I18N.setLanguage(Language::ES); // Get current language Language lang = I18N.getLanguage(); @@ -201,7 +201,7 @@ I18N.saveSettings(); I18N.loadSettings(); // Get character set for font subsetting (static method) -const char* chars = I18n::getCharacterSet(Language::FRENCH); +const char* chars = I18n::getCharacterSet(Language::FR); ``` --- diff --git a/lib/I18n/I18n.cpp b/lib/I18n/I18n.cpp index 9b00d8c0..55cf8fc4 100644 --- a/lib/I18n/I18n.cpp +++ b/lib/I18n/I18n.cpp @@ -89,7 +89,7 @@ void I18n::loadSettings() { const char* I18n::getCharacterSet(Language lang) { const auto langIndex = static_cast(lang); if (langIndex >= static_cast(Language::_COUNT)) { - lang = Language::ENGLISH; // Fallback to first language + lang = Language::EN; // Fallback to first language } return CHARACTER_SETS[static_cast(lang)]; diff --git a/lib/I18n/I18n.h b/lib/I18n/I18n.h index e546635e..347b7fcc 100644 --- a/lib/I18n/I18n.h +++ b/lib/I18n/I18n.h @@ -32,7 +32,7 @@ class I18n { static const char* getCharacterSet(Language lang); private: - I18n() : _language(Language::ENGLISH) {} + I18n() : _language(Language::EN) {} Language _language; }; diff --git a/lib/I18n/translations/belarusian.yaml b/lib/I18n/translations/belarusian.yaml index 47be7882..5114c54b 100644 --- a/lib/I18n/translations/belarusian.yaml +++ b/lib/I18n/translations/belarusian.yaml @@ -1,5 +1,5 @@ _language_name: "Беларуская" -_language_code: "BELARUSIAN" +_language_code: "BE" _order: "11" STR_CROSSPOINT: "CrossPoint" diff --git a/lib/I18n/translations/catalan.yaml b/lib/I18n/translations/catalan.yaml index b04fb704..a76888a8 100644 --- a/lib/I18n/translations/catalan.yaml +++ b/lib/I18n/translations/catalan.yaml @@ -1,5 +1,5 @@ _language_name: "Català" -_language_code: "CATALAN" +_language_code: "CA" _order: "9" STR_CROSSPOINT: "CrossPoint" diff --git a/lib/I18n/translations/czech.yaml b/lib/I18n/translations/czech.yaml index 0f6f6f30..46019b1c 100644 --- a/lib/I18n/translations/czech.yaml +++ b/lib/I18n/translations/czech.yaml @@ -1,5 +1,5 @@ _language_name: "Čeština" -_language_code: "CZECH" +_language_code: "CS" _order: "4" STR_CROSSPOINT: "CrossPoint" diff --git a/lib/I18n/translations/english.yaml b/lib/I18n/translations/english.yaml index c77e44fd..71067b5a 100644 --- a/lib/I18n/translations/english.yaml +++ b/lib/I18n/translations/english.yaml @@ -1,5 +1,5 @@ _language_name: "English" -_language_code: "ENGLISH" +_language_code: "EN" _order: "0" STR_CROSSPOINT: "CrossPoint" diff --git a/lib/I18n/translations/french.yaml b/lib/I18n/translations/french.yaml index 84c1a30e..559e5837 100644 --- a/lib/I18n/translations/french.yaml +++ b/lib/I18n/translations/french.yaml @@ -1,5 +1,5 @@ _language_name: "Français" -_language_code: "FRENCH" +_language_code: "FR" _order: "2" STR_CROSSPOINT: "CrossPoint" diff --git a/lib/I18n/translations/german.yaml b/lib/I18n/translations/german.yaml index ddb23663..641c7d39 100644 --- a/lib/I18n/translations/german.yaml +++ b/lib/I18n/translations/german.yaml @@ -1,5 +1,5 @@ _language_name: "Deutsch" -_language_code: "GERMAN" +_language_code: "DE" _order: "3" STR_CROSSPOINT: "CrossPoint" diff --git a/lib/I18n/translations/italian.yaml b/lib/I18n/translations/italian.yaml index 1da5f74a..a92ed4ac 100644 --- a/lib/I18n/translations/italian.yaml +++ b/lib/I18n/translations/italian.yaml @@ -1,5 +1,5 @@ _language_name: "Italiano" -_language_code: "ITALIAN" +_language_code: "IT" _order: "12" STR_CROSSPOINT: "CrossPoint" diff --git a/lib/I18n/translations/portuguese.yaml b/lib/I18n/translations/portuguese.yaml index 4f694048..f404e172 100644 --- a/lib/I18n/translations/portuguese.yaml +++ b/lib/I18n/translations/portuguese.yaml @@ -1,5 +1,5 @@ _language_name: "Português (Brasil)" -_language_code: "PORTUGUESE" +_language_code: "PT" _order: "5" STR_CROSSPOINT: "CrossPoint" diff --git a/lib/I18n/translations/romanian.yaml b/lib/I18n/translations/romanian.yaml index 76b669b8..6266ed26 100644 --- a/lib/I18n/translations/romanian.yaml +++ b/lib/I18n/translations/romanian.yaml @@ -1,5 +1,5 @@ _language_name: "Română" -_language_code: "ROMANIAN" +_language_code: "RO" _order: "8" STR_CROSSPOINT: "CrossPoint" diff --git a/lib/I18n/translations/russian.yaml b/lib/I18n/translations/russian.yaml index 7096f9b8..b028a862 100644 --- a/lib/I18n/translations/russian.yaml +++ b/lib/I18n/translations/russian.yaml @@ -1,5 +1,5 @@ _language_name: "Русский" -_language_code: "RUSSIAN" +_language_code: "RU" _order: "6" STR_CROSSPOINT: "CrossPoint" diff --git a/lib/I18n/translations/spanish.yaml b/lib/I18n/translations/spanish.yaml index fd7dc3cb..4718d43f 100644 --- a/lib/I18n/translations/spanish.yaml +++ b/lib/I18n/translations/spanish.yaml @@ -1,5 +1,5 @@ _language_name: "Español" -_language_code: "SPANISH" +_language_code: "ES" _order: "1" STR_CROSSPOINT: "CrossPoint" diff --git a/lib/I18n/translations/swedish.yaml b/lib/I18n/translations/swedish.yaml index 8f434e88..1973e69a 100644 --- a/lib/I18n/translations/swedish.yaml +++ b/lib/I18n/translations/swedish.yaml @@ -1,5 +1,5 @@ _language_name: "Svenska" -_language_code: "SWEDISH" +_language_code: "SV" _order: "7" STR_CROSSPOINT: "Crosspoint" diff --git a/lib/I18n/translations/ukrainian.yaml b/lib/I18n/translations/ukrainian.yaml index a187cb58..6857ba0a 100644 --- a/lib/I18n/translations/ukrainian.yaml +++ b/lib/I18n/translations/ukrainian.yaml @@ -1,5 +1,5 @@ _language_name: "Українська" -_language_code: "UKRAINIAN" +_language_code: "UK" _order: "10" STR_CROSSPOINT: "CrossPoint" diff --git a/scripts/gen_i18n.py b/scripts/gen_i18n.py index 5d67acfb..6880c079 100755 --- a/scripts/gen_i18n.py +++ b/scripts/gen_i18n.py @@ -9,7 +9,7 @@ Reads YAML files from a translations directory (one file per language) and gener Each YAML file must contain: _language_name: "Native Name" (e.g. "Español") - _language_code: "ENUM_NAME" (e.g. "SPANISH") + _language_code: "ENUM_NAME" (e.g. "ES") STR_KEY: "translation text" The English file is the reference. Missing keys in other languages are @@ -108,7 +108,7 @@ def load_translations( ) -> Tuple[List[str], List[str], List[str], Dict[str, List[str]]]: """ Read every YAML file in *translations_dir* and return: - language_codes e.g. ["ENGLISH", "SPANISH", ...] + language_codes e.g. ["EN", "ES", ...] language_names e.g. ["English", "Español", ...] string_keys ordered list of STR_* keys (from English) translations {key: [translation_per_language]} @@ -131,12 +131,12 @@ def load_translations( # Identify the English file (must exist) english_file = None for name, data in parsed.items(): - if data.get("_language_code", "").upper() == "ENGLISH": + if data.get("_language_code", "").upper() == "EN": english_file = name break if english_file is None: - raise ValueError("No YAML file with _language_code: ENGLISH found") + raise ValueError("No YAML file with _language_code: EN found") # Order: English first, then by _order metadata (falls back to filename) def sort_key(fname: str) -> Tuple[int, int, str]: @@ -220,7 +220,7 @@ LANG_ABBREVIATIONS = { "العربية": "AR", "arabic": "AR", "עברית": "HE", "hebrew": "HE", "فارسی": "FA", "persian": "FA", - "čeština": "CZ", + "čeština": "CS", } @@ -438,6 +438,31 @@ def generate_keys_header( "constexpr uint8_t getLanguageCount() " "{ return static_cast(Language::_COUNT); }" ) + lines.append("") + + # Sorted language indices for display order + # (English first, then by language code alphabetically) + english_idx = languages.index("EN") + rest = sorted( + (i for i in range(len(languages)) if i != english_idx), + key=lambda i: languages[i], + ) + sorted_indices = [english_idx] + rest + comment_names = ", ".join(language_names[i] for i in sorted_indices) + lines.append("// Sorted language indices by code (auto-generated by gen_i18n.py)") + lines.append(f"// Order: {comment_names}") + lines.append( + "constexpr uint8_t SORTED_LANGUAGE_INDICES[] = {" + f"{', '.join(str(i) for i in sorted_indices)}" + "};" + ) + lines.append("") + lines.append( + "static_assert(sizeof(SORTED_LANGUAGE_INDICES) / sizeof(SORTED_LANGUAGE_INDICES[0]) == getLanguageCount()," + ) + lines.append( + ' "SORTED_LANGUAGE_INDICES size mismatch");' + ) _write_file(output_path, lines) diff --git a/src/activities/settings/LanguageSelectActivity.cpp b/src/activities/settings/LanguageSelectActivity.cpp index f8e3347c..349a021f 100644 --- a/src/activities/settings/LanguageSelectActivity.cpp +++ b/src/activities/settings/LanguageSelectActivity.cpp @@ -3,16 +3,22 @@ #include #include +#include +#include + +#include "I18nKeys.h" #include "MappedInputManager.h" #include "fontIds.h" void LanguageSelectActivity::onEnter() { Activity::onEnter(); - totalItems = getLanguageCount(); - // Set current selection based on current language - selectedIndex = static_cast(I18N.getLanguage()); + const auto currentLang = static_cast(I18N.getLanguage()); + const auto* begin = std::begin(SORTED_LANGUAGE_INDICES); + const auto* end = std::end(SORTED_LANGUAGE_INDICES); + const auto* it = std::find(begin, end, currentLang); + selectedIndex = (it != end) ? std::distance(begin, it) : 0; requestUpdate(); } @@ -45,7 +51,7 @@ void LanguageSelectActivity::loop() { void LanguageSelectActivity::handleSelection() { { RenderLock lock(*this); - I18N.setLanguage(static_cast(selectedIndex)); + I18N.setLanguage(static_cast(SORTED_LANGUAGE_INDICES[selectedIndex])); } // Return to previous page @@ -61,13 +67,16 @@ void LanguageSelectActivity::render(Activity::RenderLock&&) { GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_LANGUAGE)); + // Current language marker const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing; - const int currentLang = static_cast(I18N.getLanguage()); + const auto currentLang = static_cast(I18N.getLanguage()); GUI.drawList( renderer, Rect{0, contentTop, pageWidth, contentHeight}, totalItems, selectedIndex, - [this](int index) { return I18N.getLanguageName(static_cast(index)); }, nullptr, nullptr, - [this, currentLang](int index) { return index == currentLang ? tr(STR_SET) : ""; }, true); + [this](int index) { return I18N.getLanguageName(static_cast(SORTED_LANGUAGE_INDICES[index])); }, + nullptr, nullptr, + [this, currentLang](int index) { return SORTED_LANGUAGE_INDICES[index] == currentLang ? tr(STR_SET) : ""; }, + true); // Button hints const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN)); diff --git a/src/activities/settings/LanguageSelectActivity.h b/src/activities/settings/LanguageSelectActivity.h index be0df480..6af007de 100644 --- a/src/activities/settings/LanguageSelectActivity.h +++ b/src/activities/settings/LanguageSelectActivity.h @@ -31,5 +31,5 @@ class LanguageSelectActivity final : public Activity { std::function onBack; ButtonNavigator buttonNavigator; int selectedIndex = 0; - int totalItems = 0; + constexpr static uint8_t totalItems = getLanguageCount(); };