feat: add UI translation system (i18n) with live language switching#735
Open
foXaCe wants to merge 3 commits into
Open
feat: add UI translation system (i18n) with live language switching#735foXaCe wants to merge 3 commits into
foXaCe wants to merge 3 commits into
Conversation
Adds a runtime translation layer so all user-facing strings can be
localized without restarting the app. English ships as the reference
language; French is bundled as a complete second locale (425 keys).
## Architecture
- New `workspace/all/common/i18n.{c,h}` — minimal i18n core:
- Hash table (FNV-1a, 1024 buckets, 32 KB arena)
- Parser for plain `key=value` `.lang` files (no JSON dep)
- Lookup `T(key)` returns the translated string, or the input
verbatim when no entry matches → safe fallback
- `I18N_init(code)` called from `GFX_init`; `I18N_reload(code)`
hot-swaps the table at runtime
- New `skeleton/SYSTEM/res/lang/{en,fr}.lang` (425 key pairs)
- `NextUISettings.language[8]` persisted in `minuisettings.txt`
(defaults to "en"; backward-compatible read)
## Coverage
All user-visible strings are passed through `T()`:
- Launcher: `workspace/all/nextui/nextui.c`
- In-game menu: `workspace/all/minarch/{ma_menu,ma_frontend_opts,
ma_saves,ra_integration}.c`
- Settings UI (`workspace/all/settings/*.cpp/hpp`): main menus,
Appearance, Display, Audio, System, FN switch, In-Game,
RetroAchievements (including sync overlay messages), About,
Bluetooth, WiFi, color picker, keyboard prompt
- Hardware hints in `workspace/all/common/api.c`
- Tools: `battery`, `clock`, `gametime`, `ledcontrol`, `minput`,
`bootlogo` (button hints + on-screen labels)
## Hot-reload (Settings only)
`Settings → System → Language` switches locale live without exit:
- `AbstractMenuItem::getName/getDesc` and `MenuItem::getLabel/getLabels`
now call `T()` at each access (storing i18n keys, not pre-translated
strings)
- `ScopedOverlay` and `MenuList::showOverlay` apply `T()` internally
- `getRawName()` added for `selectByName` so selection survives a
language change
- Other apps (launcher, minarch, tools) load the active language on
startup — since they're short-lived processes relaunched per action,
no hot-reload is required there
## Build
- `i18n.c` linked into every app that calls `T()`: `nextui`, `minarch`,
`settings`, `battery`, `clock`, `gametime`, `ledcontrol`, `minput`,
`bootlogo` (9 makefiles updated)
- Pure C module; safe to include from C++ via `extern "C"` (already
done in `settings/menu.hpp`)
## Compatibility
- Default behavior unchanged: with no `.lang` files installed or
`language=en`, every `T(key)` falls back to the literal key — which
for legacy strings is the original English text → zero regression
- New `language=` line appended to `minuisettings.txt` (older
consumers can simply ignore it)
- Adding a new locale = drop `<code>.lang` into `.system/res/lang/`;
it is auto-discovered by Settings via `discoverLanguages()`
Covers the ~30 options of the in-game Frontend menu (MENU → Options →
Frontend during gameplay) plus their option-value labels. Adds ~90 new
key pairs to en.lang and fr.lang (total: 514 keys).
## Changes
- workspace/all/minarch/ma_config.c
- Replace literal English strings in .name / .desc with i18n keys
(e.g. `"Screen Scaling"` → `"frontend.opt.screen_scaling"`)
- Replace label arrays (onoff_labels, scaling_labels, resample_labels,
rewind_enable_labels, rewind_compression_accel_labels,
ambient_labels, effect_labels, overlay_labels) with key-based
entries
- getScreenScalingDesc() returns a key instead of a literal
- workspace/all/minarch/ma_menu.c
- Wrap rendering sites in T() so the displayed text is translated at
each frame (live-reloadable):
- item->name (3 sites)
- item->values[item->value] (1 site)
- item->values[j] (size calc, 1 site)
- list/item->desc footer (size + blit, 2 sites)
- skeleton/SYSTEM/res/lang/en.lang
- skeleton/SYSTEM/res/lang/fr.lang
- +~90 paired keys covering option names, descriptions, and value
labels for the Frontend menu
…ollections)
The main launcher built its root menu through Entry_new(path, ENTRY_DIR),
which derives the displayed name from the directory path
(getDisplayName()), bypassing translation. Switch those root entries to
Entry_newNamed with i18n keys so the labels render in the active
language.
## Changes
- workspace/all/nextui/nextui.c
- getRoot(): Tools / Recently Played / Collections now built with
Entry_newNamed + T() keys
- getQuickEntries(): same fix for the Quickswitcher entries
- skeleton/SYSTEM/res/lang/en.lang
- skeleton/SYSTEM/res/lang/fr.lang
- +3 paired keys: launcher.tools, launcher.recently_played,
launcher.collections
Brings total to 517 paired keys.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds a runtime translation layer so all user-facing strings can be localized without restarting the app. English ships as the reference language; French is bundled as a complete second locale (425 keys).
The default behavior is unchanged for existing installs: with no
.langfiles present orlanguage=en, everyT(key)falls back to the literal — which for legacy strings is the original English text → zero regression.Architecture
workspace/all/common/i18n.{c,h}— minimal i18n corekey=value.langfiles (no JSON dependency)T(key)returns the translation, or the input verbatim when no entry matches (safe fallback)I18N_init(code)called fromGFX_init;I18N_reload(code)hot-swaps the table at runtimeskeleton/SYSTEM/res/lang/{en,fr}.lang(425 paired keys)NextUISettings.language[8]persisted inminuisettings.txt(defaults to\"en\"; backward-compatible read)Coverage
All user-visible strings flow through
T():nextui.cminarch/{ma_menu,ma_frontend_opts,ma_saves,ra_integration}.csettings/*.cpp/hpp): Appearance, Display, Audio, System, FN switch, In-Game, RetroAchievements (incl. sync overlay), About, Bluetooth, WiFi, color picker, keyboard promptcommon/api.cbattery,clock,gametime,ledcontrol,minput,bootlogoHot-reload (Settings only)
`Settings → System → Language` switches locale live without exit:
Other apps (launcher, minarch, tools) load the active language on startup — they are short-lived processes relaunched per action, so per-app hot-reload is unnecessary.
Build
`i18n.c` linked into every app that calls `T()`: nextui, minarch, settings, battery, clock, gametime, ledcontrol, minput, bootlogo (9 makefiles updated). Pure C module; safe to include from C++ via the existing `extern "C"` block in `settings/menu.hpp`.
Resource impact:
Compatibility
.langfiles installed — behavior identical to before this PR.lang` into `.system/res/lang/`; auto-discovered by Settings via `discoverLanguages()`Test plan
.lang` and translate valuesNotes
Happy to split this into smaller PRs (core i18n, launcher, settings, tools…) if that's preferred for review.