feat: User-Interface I18n System#728
Conversation
- Refactored I18n system to move string arrays into a separate file (I18nStrings.cpp/h). - Removed Chinese and Japanese language support. - Added Spanish, Italian, Swedish, and French as template languages. - Renamed TR() macro to i18n() for better clarity. - Performed a comprehensive sweep of the codebase to wrap all hardcoded UI strings in i18n() calls. - Updated LanguageSelectActivity to dynamically support the new language list. - Updated documentation in docs/i18n.md. Co-authored-by: Uri-Tauber <[email protected]>
|
Could someone with write access please add |
CaptainFrito
left a comment
There was a problem hiding this comment.
Very smart implementation 👍🏻
|
Hit me up when we can start translating 🙂 |
|
Thumbs up! Now I see we had a very similar idea. If you want to try mine solution, I have a firmware.bin there for you to try and some pictures of my reader with it. |
1eb388d to
0b3919d
Compare
I18n Engine Ready!The Python automation is now working. This PR establishes the core infrastructure for multi-language support. How to TranslateNo manual synchronization is needed. All changes happen in the spreadsheet: Edit: Open lib/I18n/translations.csv in Excel or LibreOffice. Translate: Add your text to the corresponding language column. Sync: Run the script to update the code: Details are available in docs/i18n.md. Next StepsI suggest we merge this PR now to establish the workflow (@CaptainFrito @daveallie). Afterwards, Translators can open individual PRs for their specific languages. Until this PR will be merged, feel free to attach your version of the CSV here, and I'll update the PR. |
lib/I18n/I18nKeys.h
Outdated
| enum class StrId : uint16_t { | ||
| CROSSPOINT, | ||
| BOOTING, | ||
| SLEEPING, |
There was a problem hiding this comment.
nits, but I would prefer defining those as macro, something like:
#define TEXT_CROSSPOINT i18n(0)
#define TEXT_BOOTING i18n(1)
#define TEXT_SLEEPING i18n(2)so that the downstream code will look a bit more intuitive, like:
drawCenteredText(TEXT_CROSSPOINT, ...);There was a problem hiding this comment.
I appreciate the suggestions!
The current i18n(KEY) pattern matches how other modern i18n libraries work (Qt's tr() for example), so it's a well-established pattern. That said, I understand it's a matter of preference.
If there's strong preference for the TEXT_ pattern from other maintainers, I'm happy to adjust.
There was a problem hiding this comment.
I think there are 2 problems with the i18n(KEY) though, firstly it may conflict with other macro or variable naming. Something like AUTH_FAILED can likely introduce conflicts with another enum or#define having the same name
I know namespace enum like i18n(String::LOADING) can also be the solution, but still, it's more verbose than something like T_LOADING or TEXT_LOADING
Secondly, other frameworks uses shorter naming for the text function, like t(), tr(), _e() for the translation function to make DX a bit better. i18n is a bit too verbose I think.
There was a problem hiding this comment.
@ngxson You’re right about the second point. Renaming it to tr() would simplify things and make it more clear to developers, especially those who have worked with Qt.
Regarding the first point: Just to clarify, i18n() is actually already a macro
(defined as #define i18n(id) I18n::getInstance().get(StrId::id) in I18n.h).
This means when we write:
drawCenteredText(i18n(CROSSPOINT), ...);The preprocessor expands it to:
drawCenteredText(I18n::getInstance().get(StrId::CROSSPOINT), ...);There was a problem hiding this comment.
I don't mean it's a problem with the macro, but the enum is the problem.
Generic enum naming like the AUTH_FAILED example I mentioned above can always conflict with any piece of code that declares another enum or macro having the same AUTH_FAILED value, especially C code where they cannot use scope.
At least it is a real problem that I experienced a real production code, so a common practice is to prefix the enum value no matter what.
There was a problem hiding this comment.
It think either STR_ or TEXT_ sounds good. I don't usually add number to variable name (just personal preference though)
Even better, since the discussion above moves i18n() to something shorter like tr(), maybe we can call the whole thing TR_BACK (which expands to something like i18n(STR_ID_BACK) ?)
There was a problem hiding this comment.
@ngxson I Updated the PR according to your suggestions.
There was a problem hiding this comment.
Looks better now, just one more concern is that maybe writing tr(STR_...) potentially making the whole tr(STR_ as a prefix? Just look a bit verbose to me, but not a very big problem.
My suggestion is:
#define TR_BACK tr(STR_BACK)
#define TR_CONFIRM tr(STR_CONFIRM)
...
// then use it: drawText(TR_BACK, ...)These macros can be autogenerated on the same I18nKeys.h header
There was a problem hiding this comment.
I work a lot with Qt, so, in my eyes, the tr(STR_CONFIRM) style feels more intuitive than TR_CONFIRM macro. If that’s acceptable, I’d like to keep it as is.
There was a problem hiding this comment.
I usually prefer brevity for variable names that use very frequently, but no strong opinion in this case. Maybe asking the team @crosspoint-reader/firmware-maintainers as this seems to be an important decision.
lib/I18n/translations.csv
Outdated
There was a problem hiding this comment.
IMO a csv may not be a very good idea. For example, if 2 translators open 2 PRs for the 2 different languages, each PR will still modify the same line, thus making diff harder to merge.
Maybe something like JSON or YAML can be a better choice?
en:
CROSSPOINT: CrossPoint
BOOTING: BootingOr:
CROSSPOINT:
en: Crosspoint
fr: ...There was a problem hiding this comment.
@ngxson Handling merge conflicts in the CSV is straightforward via a quick column copy-paste in LibreOffice or Excel. I chose CSV because it’s much more accessible for non-technical contributors compared to JSON or YAML, which usually require a specialized editors (like Dadroit) to avoid syntax errors.
There was a problem hiding this comment.
Here's my first stab at German. (German is notorious for having long words, so I'm happy to check this on my device later and shrink strings further down.)
There was a problem hiding this comment.
Thanks @DavidOrtmann!
I will wait with updating the PR until you finished the shrinking.
There was a problem hiding this comment.
Is there a way to check the strings on-device yet?
There was a problem hiding this comment.
Thank you for your work! I'm glad I aggressively shorted strings before uploading (I added a column that showed length difference), will now work through anything that is too long still.
There was a problem hiding this comment.
By the way, we might need a different string for "Inverted", because this is something different in German (and maybe other languages) if it relates to the device's orientation or colours of an image.
I went through all menus (I hope) and adjusted some lengths in this version of the translation:
There was a problem hiding this comment.
@ngxson I started migrating to YAML, and while doing that I had an idea that might address the merging issue you mentioned, while keeping the workflow simple.
What if we rotate the CSV by 90 degrees — so languages become rows and strings become columns? That way, adding a new language or a new string would still be straightforward.
It would also allow translators without programming experience to contribute in a familiar spreadsheet-style format, without feeling intimidated by YAML.
If two translators submit PRs for different languages, the merge should happen automatically since each language would be a separate row. The only time manual conflict resolution would be needed is when two people work on the same language, which should be much less common.
What do you think?
There was a problem hiding this comment.
What if we rotate the CSV by 90 degrees — so languages become rows and strings become columns? That way, adding a new language or a new string would still be straightforward.
I imagine that won't be very readable on something like excel. Also, if 2 people change 2 different texts in the same language, git diff will still fail
It would also allow translators without programming experience to contribute in a familiar spreadsheet-style format, without feeling intimidated by YAML.
Honestly I don't see how yaml can be difficult to understand. It looks almost like a todo list and I'm pretty sure that it's intuitive enough that translator can also ask AI for help if needed.
I suggest we do this way: instead of assuming which one is easier, let's try making 2 demo, one yaml, one csv, and ask translators for opinions ?
There was a problem hiding this comment.
I would like to add how TRMNL does its localisation efforts, and it's working very well, imo: https://github.com/usetrmnl/trmnl-i18n/blob/main/lib/trmnl/i18n/locales/web_ui/en.yml
|
Anybody working on translations? I'm doing le French |
|
Here is a first stab at the french version. It didn't export with quotes except for the strings that have a coma, is that okay? |
@CaptainFrito Looks great. |
translation work is being discussed here #719 |
Do I understand correctly that to add the Russian language, I must add the Russian translation after the comma in each line? P.S. Sorry for the stupid question, I'm not a programmer, I'm just a person with a great desire to participate in the project |
Not a stupid question at all. You might find it easier to work with CSV files in Excel or LibreOffice. Essentially, yes—just add a comma and your translation at the end of each line. If any value contains a comma, wrap it in quotes so it isn’t interpreted as multiple cells. In Excel or LibreOffice, this is usually handled automatically when you add a new column. |
. Add German and French translations
Firmware file for testing: |
| STR_CAPS_OFF: "caps" | ||
| STR_OK_BUTTON: "OK" | ||
| STR_ON_MARKER: "[ВКЛ]" | ||
| STR_SLEEP_COVER_FILTER: "Фильтр обложки сна" |
There was a problem hiding this comment.
As the filter applies only to the Обложка (cover), the original value can be right. Something worth considering nonetheless :)
Yes, when I've started the review the PR was open 😆 |
## Summary This PR includes vocabulary and grammar fixes for Russian translation, originally made as review comments [here](#728). --- ### 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? _**NO**_
## Summary **What is the goal of this PR?** This PR introduces Internationalization (i18n) support, enabling users to switch the UI language dynamically. **What changes are included?** - Core Logic: Added I18n class (`lib/I18n/I18n.h/cpp`) to manage language state and string retrieval. - Data Structures: - `lib/I18n/I18nStrings.h/cpp`: Static string arrays for each supported language. - `lib/I18n/I18nKeys.h`: Enum definitions for type-safe string access. - `lib/I18n/translations.csv`: single source of truth. - Documentation: Added `docs/i18n.md` detailing the workflow for developers and translators. - New Settings activity: `src/activities/settings/LanguageSelectActivity.h/cpp` ## Additional Context This implementation (building on concepts from crosspoint-reader#505) prioritizes performance and memory efficiency. The core approach is to store all localized strings for each language in dedicated arrays and access them via enums. This provides O(1) access with zero runtime overhead, and avoids the heap allocations, hashing, and collision handling required by `std::map` or `std::unordered_map`. The main trade-off is that enums and string arrays must remain perfectly synchronized—any mismatch would result in incorrect strings being displayed in the UI. To eliminate this risk, I added a Python script that automatically generates `I18nStrings.h/.cpp` and `I18nKeys.h` from a CSV file, which will serve as the single source of truth for all translations. The full design and workflow are documented in `docs/i18n.md`. ### Next Steps - [x] Python script `generate_i18n.py` to auto-generate C++ files from CSV - [x] Populate translations.csv with initial translations. Currently available translations: English, Español, Français, Deutsch, Čeština, Português (Brasil), Русский, Svenska. Thanks, community! **Status:** EDIT: ready to be merged. As a proof of concept, the SPANISH strings currently mirror the English ones, but are fully uppercased. --- ### AI Usage Did you use AI tools to help write this code? _**< PARTIALLY >**_ I used AI for the black work of replacing strings with I18n references across the project, and for generating the documentation. EDIT: also some help with merging changes from master. --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: yeyeto2788 <[email protected]>
## Summary This PR includes vocabulary and grammar fixes for Russian translation, originally made as review comments [here](crosspoint-reader#728). --- ### 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? _**NO**_
## Summary * **What is the goal of this PR?** Update translators.md to include all the contributors from #728 --- ### 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? _**< NO >**_
## Summary * **What is the goal of this PR?** Update translators.md to include all the contributors from crosspoint-reader#728 --- ### 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? _**< NO >**_
## Summary * **What is the goal of this PR?** Fix a dangling pointer issue caused by using `.c_str()` on a temporary `std::string`. `basepath.substr()` creates a temporary `std::string`, and calling `.c_str()` on it returns a pointer to its internal buffer (not a copy). Since the temporary string is destroyed at the end of the full expression, `folderName` ends up holding a dangling pointer, leading to undefined behavior. To solve this, we stores the result in a persistent `std::string` object, ensuring the underlying buffer remains valid for the duration of its use. A similar pattern caused the behavior reported in #728 (comment) --- ### 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? _**< NO >**_
## Summary This PR includes vocabulary and grammar fixes for Russian translation, originally made as review comments [here](crosspoint-reader#728). --- ### 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? _**NO**_
## Summary * **What is the goal of this PR?** Update translators.md to include all the contributors from crosspoint-reader#728 --- ### 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? _**< NO >**_
## Summary * **What is the goal of this PR?** Fix a dangling pointer issue caused by using `.c_str()` on a temporary `std::string`. `basepath.substr()` creates a temporary `std::string`, and calling `.c_str()` on it returns a pointer to its internal buffer (not a copy). Since the temporary string is destroyed at the end of the full expression, `folderName` ends up holding a dangling pointer, leading to undefined behavior. To solve this, we stores the result in a persistent `std::string` object, ensuring the underlying buffer remains valid for the duration of its use. A similar pattern caused the behavior reported in crosspoint-reader#728 (comment) --- ### 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? _**< NO >**_
## Summary This PR includes vocabulary and grammar fixes for Russian translation, originally made as review comments [here](crosspoint-reader#728). --- ### 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? _**NO**_
## Summary * **What is the goal of this PR?** Update translators.md to include all the contributors from crosspoint-reader#728 --- ### 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? _**< NO >**_
## Summary * **What is the goal of this PR?** Fix a dangling pointer issue caused by using `.c_str()` on a temporary `std::string`. `basepath.substr()` creates a temporary `std::string`, and calling `.c_str()` on it returns a pointer to its internal buffer (not a copy). Since the temporary string is destroyed at the end of the full expression, `folderName` ends up holding a dangling pointer, leading to undefined behavior. To solve this, we stores the result in a persistent `std::string` object, ensuring the underlying buffer remains valid for the duration of its use. A similar pattern caused the behavior reported in crosspoint-reader#728 (comment) --- ### 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? _**< NO >**_
## Summary **What is the goal of this PR?** This PR introduces Internationalization (i18n) support, enabling users to switch the UI language dynamically. **What changes are included?** - Core Logic: Added I18n class (`lib/I18n/I18n.h/cpp`) to manage language state and string retrieval. - Data Structures: - `lib/I18n/I18nStrings.h/cpp`: Static string arrays for each supported language. - `lib/I18n/I18nKeys.h`: Enum definitions for type-safe string access. - `lib/I18n/translations.csv`: single source of truth. - Documentation: Added `docs/i18n.md` detailing the workflow for developers and translators. - New Settings activity: `src/activities/settings/LanguageSelectActivity.h/cpp` ## Additional Context This implementation (building on concepts from crosspoint-reader#505) prioritizes performance and memory efficiency. The core approach is to store all localized strings for each language in dedicated arrays and access them via enums. This provides O(1) access with zero runtime overhead, and avoids the heap allocations, hashing, and collision handling required by `std::map` or `std::unordered_map`. The main trade-off is that enums and string arrays must remain perfectly synchronized—any mismatch would result in incorrect strings being displayed in the UI. To eliminate this risk, I added a Python script that automatically generates `I18nStrings.h/.cpp` and `I18nKeys.h` from a CSV file, which will serve as the single source of truth for all translations. The full design and workflow are documented in `docs/i18n.md`. ### Next Steps - [x] Python script `generate_i18n.py` to auto-generate C++ files from CSV - [x] Populate translations.csv with initial translations. Currently available translations: English, Español, Français, Deutsch, Čeština, Português (Brasil), Русский, Svenska. Thanks, community! **Status:** EDIT: ready to be merged. As a proof of concept, the SPANISH strings currently mirror the English ones, but are fully uppercased. --- ### AI Usage Did you use AI tools to help write this code? _**< PARTIALLY >**_ I used AI for the black work of replacing strings with I18n references across the project, and for generating the documentation. EDIT: also some help with merging changes from master. --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: yeyeto2788 <[email protected]>
## Summary This PR includes vocabulary and grammar fixes for Russian translation, originally made as review comments [here](crosspoint-reader#728). --- ### 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? _**NO**_
## Summary * **What is the goal of this PR?** Update translators.md to include all the contributors from crosspoint-reader#728 --- ### 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? _**< NO >**_
## Summary * **What is the goal of this PR?** Fix a dangling pointer issue caused by using `.c_str()` on a temporary `std::string`. `basepath.substr()` creates a temporary `std::string`, and calling `.c_str()` on it returns a pointer to its internal buffer (not a copy). Since the temporary string is destroyed at the end of the full expression, `folderName` ends up holding a dangling pointer, leading to undefined behavior. To solve this, we stores the result in a persistent `std::string` object, ensuring the underlying buffer remains valid for the duration of its use. A similar pattern caused the behavior reported in crosspoint-reader#728 (comment) --- ### 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? _**< NO >**_
**What is the goal of this PR?** This PR introduces Internationalization (i18n) support, enabling users to switch the UI language dynamically. **What changes are included?** - Core Logic: Added I18n class (`lib/I18n/I18n.h/cpp`) to manage language state and string retrieval. - Data Structures: - `lib/I18n/I18nStrings.h/cpp`: Static string arrays for each supported language. - `lib/I18n/I18nKeys.h`: Enum definitions for type-safe string access. - `lib/I18n/translations.csv`: single source of truth. - Documentation: Added `docs/i18n.md` detailing the workflow for developers and translators. - New Settings activity: `src/activities/settings/LanguageSelectActivity.h/cpp` This implementation (building on concepts from crosspoint-reader#505) prioritizes performance and memory efficiency. The core approach is to store all localized strings for each language in dedicated arrays and access them via enums. This provides O(1) access with zero runtime overhead, and avoids the heap allocations, hashing, and collision handling required by `std::map` or `std::unordered_map`. The main trade-off is that enums and string arrays must remain perfectly synchronized—any mismatch would result in incorrect strings being displayed in the UI. To eliminate this risk, I added a Python script that automatically generates `I18nStrings.h/.cpp` and `I18nKeys.h` from a CSV file, which will serve as the single source of truth for all translations. The full design and workflow are documented in `docs/i18n.md`. - [x] Python script `generate_i18n.py` to auto-generate C++ files from CSV - [x] Populate translations.csv with initial translations. Currently available translations: English, Español, Français, Deutsch, Čeština, Português (Brasil), Русский, Svenska. Thanks, community! **Status:** EDIT: ready to be merged. As a proof of concept, the SPANISH strings currently mirror the English ones, but are fully uppercased. --- Did you use AI tools to help write this code? _**< PARTIALLY >**_ I used AI for the black work of replacing strings with I18n references across the project, and for generating the documentation. EDIT: also some help with merging changes from master. --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: yeyeto2788 <[email protected]>
## Summary This PR includes vocabulary and grammar fixes for Russian translation, originally made as review comments [here](crosspoint-reader#728). --- ### 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? _**NO**_
## Summary * **What is the goal of this PR?** Update translators.md to include all the contributors from crosspoint-reader#728 --- ### 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? _**< NO >**_
## Summary * **What is the goal of this PR?** Fix a dangling pointer issue caused by using `.c_str()` on a temporary `std::string`. `basepath.substr()` creates a temporary `std::string`, and calling `.c_str()` on it returns a pointer to its internal buffer (not a copy). Since the temporary string is destroyed at the end of the full expression, `folderName` ends up holding a dangling pointer, leading to undefined behavior. To solve this, we stores the result in a persistent `std::string` object, ensuring the underlying buffer remains valid for the duration of its use. A similar pattern caused the behavior reported in crosspoint-reader#728 (comment) --- ### 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? _**< NO >**_
## Summary * **What is the goal of this PR?** Update translators.md to include all the contributors from crosspoint-reader#728 --- ### 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? _**< NO >**_
## Summary * **What is the goal of this PR?** Fix a dangling pointer issue caused by using `.c_str()` on a temporary `std::string`. `basepath.substr()` creates a temporary `std::string`, and calling `.c_str()` on it returns a pointer to its internal buffer (not a copy). Since the temporary string is destroyed at the end of the full expression, `folderName` ends up holding a dangling pointer, leading to undefined behavior. To solve this, we stores the result in a persistent `std::string` object, ensuring the underlying buffer remains valid for the duration of its use. A similar pattern caused the behavior reported in crosspoint-reader#728 (comment) --- ### 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? _**< NO >**_




Summary
What is the goal of this PR?
This PR introduces Internationalization (i18n) support, enabling users to switch the UI language dynamically.
What changes are included?
Core Logic: Added I18n class (
lib/I18n/I18n.h/cpp) to manage language state and string retrieval.Data Structures:
lib/I18n/I18nStrings.h/cpp: Static string arrays for each supported language.lib/I18n/I18nKeys.h: Enum definitions for type-safe string access.lib/I18n/translations.csv: single source of truth.Documentation: Added
docs/i18n.mddetailing the workflow for developers and translators.New Settings activity:
src/activities/settings/LanguageSelectActivity.h/cppAdditional Context
This implementation (building on concepts from #505) prioritizes performance and memory efficiency.
The core approach is to store all localized strings for each language in dedicated arrays and access them via enums. This provides O(1) access with zero runtime overhead, and avoids the heap allocations, hashing, and collision handling required by
std::maporstd::unordered_map.The main trade-off is that enums and string arrays must remain perfectly synchronized—any mismatch would result in incorrect strings being displayed in the UI.
To eliminate this risk, I added a Python script that automatically generates
I18nStrings.h/.cppandI18nKeys.hfrom a CSV file, which will serve as the single source of truth for all translations. The full design and workflow are documented indocs/i18n.md.Next Steps
generate_i18n.pyto auto-generate C++ files from CSVCurrently available translations: English, Español, Français, Deutsch, Čeština, Português (Brasil), Русский, Svenska.
Thanks, community!
Status: EDIT: ready to be merged.
As a proof of concept, the SPANISH strings currently mirror the English ones, but are fully uppercased.
AI Usage
Did you use AI tools to help write this code? < PARTIALLY >
I used AI for the black work of replacing strings with I18n references across the project, and for generating the documentation. EDIT: also some help with merging changes from master.