A fork of https://github.com/crosspoint-reader/crosspoint-reader
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: sort languages in selection menu (#1071)

## Summary

* **What is the goal of this PR?** (e.g., Implements the new feature for
file uploading.)

Currently we are displaying the languages in the order they were added
(as in the `Language` enum). However, as new languages are coming in,
this will quickly be confusing to the users.

But we can't just change the ordering of the enum if we want to respect
bakwards compatibility.

So my proposal is to add a mapping of the alphabetical order of the
languages. I've made it so that it's generated by the `gen_i18n.py`
script, which will be used when a new language is added.


* **What changes are included?**

Added the array from the python script and changed
`LanguageSelectActivity` to use the indices from there. Also commited
the generated `I18nKeys.h`

## Additional Context

* Add any other information that might be helpful for the reviewer
(e.g., performance implications, potential risks,
specific areas to focus on).

I was wondering if there is a better way to sort it. Currently, it's by
unicode value and Czech and Russian are last, which I don't know it it's
the most intuitive.

The current order is:
`Català, Deutsch, English, Español, Français, Português (Brasil),
Română, Svenska, Čeština, Русский`

---

### AI Usage

While CrossPoint doesn't have restrictions on AI tools in contributing,
please be transparent about their usage as it
helps set the right context for reviewers.

Did you use AI tools to help write this code? _**< PARTIALLY >**_

authored by

ariel-lindemann and committed by
GitHub
7e214ea7 b695a48a

+68 -34
+6 -6
docs/i18n.md
··· 52 52 53 53 ```yaml 54 54 _language_name: "Español" 55 - _language_code: "SPANISH" 55 + _language_code: "ES" 56 56 _order: "1" 57 57 58 58 STR_CROSSPOINT: "CrossPoint" ··· 62 62 63 63 **Metadata keys** (prefixed with `_`): 64 64 - `_language_name` — Native display name shown to the user (e.g. "Français") 65 - - `_language_code` — C++ enum name (e.g. "FRENCH"). Must be a valid C++ identifier. 65 + - `_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. 66 66 - `_order` — Controls the position in the Language enum (English is always 0) 67 67 68 68 **Rules:** ··· 128 128 129 129 ```yaml 130 130 _language_name: "Italiano" 131 - _language_code: "ITALIAN" 131 + _language_code: "IT" 132 132 _order: "7" 133 133 134 134 STR_CROSSPOINT: "CrossPoint" ··· 175 175 Serial.printf("Status: %s\n", tr(STR_CONNECTED)); 176 176 177 177 // I18N - Shorthand for I18n::getInstance() 178 - I18N.setLanguage(Language::SPANISH); 178 + I18N.setLanguage(Language::ES); 179 179 Language lang = I18N.getLanguage(); 180 180 181 181 // === Full API === ··· 189 189 const char* text = I18N[StrId::STR_SETTINGS_TITLE]; // Operator overload 190 190 191 191 // Set language 192 - I18N.setLanguage(Language::SPANISH); 192 + I18N.setLanguage(Language::ES); 193 193 194 194 // Get current language 195 195 Language lang = I18N.getLanguage(); ··· 201 201 I18N.loadSettings(); 202 202 203 203 // Get character set for font subsetting (static method) 204 - const char* chars = I18n::getCharacterSet(Language::FRENCH); 204 + const char* chars = I18n::getCharacterSet(Language::FR); 205 205 ``` 206 206 207 207 ---
+1 -1
lib/I18n/I18n.cpp
··· 89 89 const char* I18n::getCharacterSet(Language lang) { 90 90 const auto langIndex = static_cast<size_t>(lang); 91 91 if (langIndex >= static_cast<size_t>(Language::_COUNT)) { 92 - lang = Language::ENGLISH; // Fallback to first language 92 + lang = Language::EN; // Fallback to first language 93 93 } 94 94 95 95 return CHARACTER_SETS[static_cast<size_t>(lang)];
+1 -1
lib/I18n/I18n.h
··· 32 32 static const char* getCharacterSet(Language lang); 33 33 34 34 private: 35 - I18n() : _language(Language::ENGLISH) {} 35 + I18n() : _language(Language::EN) {} 36 36 37 37 Language _language; 38 38 };
+1 -1
lib/I18n/translations/belarusian.yaml
··· 1 1 _language_name: "Беларуская" 2 - _language_code: "BELARUSIAN" 2 + _language_code: "BE" 3 3 _order: "11" 4 4 5 5 STR_CROSSPOINT: "CrossPoint"
+1 -1
lib/I18n/translations/catalan.yaml
··· 1 1 _language_name: "Català" 2 - _language_code: "CATALAN" 2 + _language_code: "CA" 3 3 _order: "9" 4 4 5 5 STR_CROSSPOINT: "CrossPoint"
+1 -1
lib/I18n/translations/czech.yaml
··· 1 1 _language_name: "Čeština" 2 - _language_code: "CZECH" 2 + _language_code: "CS" 3 3 _order: "4" 4 4 5 5 STR_CROSSPOINT: "CrossPoint"
+1 -1
lib/I18n/translations/english.yaml
··· 1 1 _language_name: "English" 2 - _language_code: "ENGLISH" 2 + _language_code: "EN" 3 3 _order: "0" 4 4 5 5 STR_CROSSPOINT: "CrossPoint"
+1 -1
lib/I18n/translations/french.yaml
··· 1 1 _language_name: "Français" 2 - _language_code: "FRENCH" 2 + _language_code: "FR" 3 3 _order: "2" 4 4 5 5 STR_CROSSPOINT: "CrossPoint"
+1 -1
lib/I18n/translations/german.yaml
··· 1 1 _language_name: "Deutsch" 2 - _language_code: "GERMAN" 2 + _language_code: "DE" 3 3 _order: "3" 4 4 5 5 STR_CROSSPOINT: "CrossPoint"
+1 -1
lib/I18n/translations/italian.yaml
··· 1 1 _language_name: "Italiano" 2 - _language_code: "ITALIAN" 2 + _language_code: "IT" 3 3 _order: "12" 4 4 5 5 STR_CROSSPOINT: "CrossPoint"
+1 -1
lib/I18n/translations/portuguese.yaml
··· 1 1 _language_name: "Português (Brasil)" 2 - _language_code: "PORTUGUESE" 2 + _language_code: "PT" 3 3 _order: "5" 4 4 5 5 STR_CROSSPOINT: "CrossPoint"
+1 -1
lib/I18n/translations/romanian.yaml
··· 1 1 _language_name: "Română" 2 - _language_code: "ROMANIAN" 2 + _language_code: "RO" 3 3 _order: "8" 4 4 5 5 STR_CROSSPOINT: "CrossPoint"
+1 -1
lib/I18n/translations/russian.yaml
··· 1 1 _language_name: "Русский" 2 - _language_code: "RUSSIAN" 2 + _language_code: "RU" 3 3 _order: "6" 4 4 5 5 STR_CROSSPOINT: "CrossPoint"
+1 -1
lib/I18n/translations/spanish.yaml
··· 1 1 _language_name: "Español" 2 - _language_code: "SPANISH" 2 + _language_code: "ES" 3 3 _order: "1" 4 4 5 5 STR_CROSSPOINT: "CrossPoint"
+1 -1
lib/I18n/translations/swedish.yaml
··· 1 1 _language_name: "Svenska" 2 - _language_code: "SWEDISH" 2 + _language_code: "SV" 3 3 _order: "7" 4 4 5 5 STR_CROSSPOINT: "Crosspoint"
+1 -1
lib/I18n/translations/ukrainian.yaml
··· 1 1 _language_name: "Українська" 2 - _language_code: "UKRAINIAN" 2 + _language_code: "UK" 3 3 _order: "10" 4 4 5 5 STR_CROSSPOINT: "CrossPoint"
+30 -5
scripts/gen_i18n.py
··· 9 9 10 10 Each YAML file must contain: 11 11 _language_name: "Native Name" (e.g. "Español") 12 - _language_code: "ENUM_NAME" (e.g. "SPANISH") 12 + _language_code: "ENUM_NAME" (e.g. "ES") 13 13 STR_KEY: "translation text" 14 14 15 15 The English file is the reference. Missing keys in other languages are ··· 108 108 ) -> Tuple[List[str], List[str], List[str], Dict[str, List[str]]]: 109 109 """ 110 110 Read every YAML file in *translations_dir* and return: 111 - language_codes e.g. ["ENGLISH", "SPANISH", ...] 111 + language_codes e.g. ["EN", "ES", ...] 112 112 language_names e.g. ["English", "Español", ...] 113 113 string_keys ordered list of STR_* keys (from English) 114 114 translations {key: [translation_per_language]} ··· 131 131 # Identify the English file (must exist) 132 132 english_file = None 133 133 for name, data in parsed.items(): 134 - if data.get("_language_code", "").upper() == "ENGLISH": 134 + if data.get("_language_code", "").upper() == "EN": 135 135 english_file = name 136 136 break 137 137 138 138 if english_file is None: 139 - raise ValueError("No YAML file with _language_code: ENGLISH found") 139 + raise ValueError("No YAML file with _language_code: EN found") 140 140 141 141 # Order: English first, then by _order metadata (falls back to filename) 142 142 def sort_key(fname: str) -> Tuple[int, int, str]: ··· 220 220 "العربية": "AR", "arabic": "AR", 221 221 "עברית": "HE", "hebrew": "HE", 222 222 "فارسی": "FA", "persian": "FA", 223 - "čeština": "CZ", 223 + "čeština": "CS", 224 224 } 225 225 226 226 ··· 437 437 lines.append( 438 438 "constexpr uint8_t getLanguageCount() " 439 439 "{ return static_cast<uint8_t>(Language::_COUNT); }" 440 + ) 441 + lines.append("") 442 + 443 + # Sorted language indices for display order 444 + # (English first, then by language code alphabetically) 445 + english_idx = languages.index("EN") 446 + rest = sorted( 447 + (i for i in range(len(languages)) if i != english_idx), 448 + key=lambda i: languages[i], 449 + ) 450 + sorted_indices = [english_idx] + rest 451 + comment_names = ", ".join(language_names[i] for i in sorted_indices) 452 + lines.append("// Sorted language indices by code (auto-generated by gen_i18n.py)") 453 + lines.append(f"// Order: {comment_names}") 454 + lines.append( 455 + "constexpr uint8_t SORTED_LANGUAGE_INDICES[] = {" 456 + f"{', '.join(str(i) for i in sorted_indices)}" 457 + "};" 458 + ) 459 + lines.append("") 460 + lines.append( 461 + "static_assert(sizeof(SORTED_LANGUAGE_INDICES) / sizeof(SORTED_LANGUAGE_INDICES[0]) == getLanguageCount()," 462 + ) 463 + lines.append( 464 + ' "SORTED_LANGUAGE_INDICES size mismatch");' 440 465 ) 441 466 442 467 _write_file(output_path, lines)
+16 -7
src/activities/settings/LanguageSelectActivity.cpp
··· 3 3 #include <GfxRenderer.h> 4 4 #include <I18n.h> 5 5 6 + #include <algorithm> 7 + #include <iterator> 8 + 9 + #include "I18nKeys.h" 6 10 #include "MappedInputManager.h" 7 11 #include "fontIds.h" 8 12 9 13 void LanguageSelectActivity::onEnter() { 10 14 Activity::onEnter(); 11 - 12 - totalItems = getLanguageCount(); 13 15 14 16 // Set current selection based on current language 15 - selectedIndex = static_cast<int>(I18N.getLanguage()); 17 + const auto currentLang = static_cast<uint8_t>(I18N.getLanguage()); 18 + const auto* begin = std::begin(SORTED_LANGUAGE_INDICES); 19 + const auto* end = std::end(SORTED_LANGUAGE_INDICES); 20 + const auto* it = std::find(begin, end, currentLang); 21 + selectedIndex = (it != end) ? std::distance(begin, it) : 0; 16 22 17 23 requestUpdate(); 18 24 } ··· 45 51 void LanguageSelectActivity::handleSelection() { 46 52 { 47 53 RenderLock lock(*this); 48 - I18N.setLanguage(static_cast<Language>(selectedIndex)); 54 + I18N.setLanguage(static_cast<Language>(SORTED_LANGUAGE_INDICES[selectedIndex])); 49 55 } 50 56 51 57 // Return to previous page ··· 61 67 62 68 GUI.drawHeader(renderer, Rect{0, metrics.topPadding, pageWidth, metrics.headerHeight}, tr(STR_LANGUAGE)); 63 69 70 + // Current language marker 64 71 const int contentTop = metrics.topPadding + metrics.headerHeight + metrics.verticalSpacing; 65 72 const int contentHeight = pageHeight - contentTop - metrics.buttonHintsHeight - metrics.verticalSpacing; 66 - const int currentLang = static_cast<int>(I18N.getLanguage()); 73 + const auto currentLang = static_cast<uint8_t>(I18N.getLanguage()); 67 74 GUI.drawList( 68 75 renderer, Rect{0, contentTop, pageWidth, contentHeight}, totalItems, selectedIndex, 69 - [this](int index) { return I18N.getLanguageName(static_cast<Language>(index)); }, nullptr, nullptr, 70 - [this, currentLang](int index) { return index == currentLang ? tr(STR_SET) : ""; }, true); 76 + [this](int index) { return I18N.getLanguageName(static_cast<Language>(SORTED_LANGUAGE_INDICES[index])); }, 77 + nullptr, nullptr, 78 + [this, currentLang](int index) { return SORTED_LANGUAGE_INDICES[index] == currentLang ? tr(STR_SET) : ""; }, 79 + true); 71 80 72 81 // Button hints 73 82 const auto labels = mappedInput.mapLabels(tr(STR_BACK), tr(STR_SELECT), tr(STR_DIR_UP), tr(STR_DIR_DOWN));
+1 -1
src/activities/settings/LanguageSelectActivity.h
··· 31 31 std::function<void()> onBack; 32 32 ButtonNavigator buttonNavigator; 33 33 int selectedIndex = 0; 34 - int totalItems = 0; 34 + constexpr static uint8_t totalItems = getLanguageCount(); 35 35 };