i18n: English/German from Switch system language

- Add I18n module: German when system language is DE, else English (ENUS/ENGB
  or any other language).
- Translate UI strings (tabs, settings, about, file browser, notifications,
  OC row titles, toggles Ein/Aus).
- Keep toggle/frequency/voltage list rows without subtitle descriptions
  like before.

Uses setGetSystemLanguage + setMakeLanguage; call I18n::init before
Application::init.

Made-with: Cursor
This commit is contained in:
Niklas Friesen
2026-03-30 20:09:38 +02:00
parent b5ba2d71df
commit b5296d6686
9 changed files with 361 additions and 47 deletions

View File

@@ -4,6 +4,7 @@
*/
#include "about_tab.h"
#include "i18n.h"
#include "logo.h"
AboutTab::AboutTab()
@@ -14,8 +15,7 @@ AboutTab::AboutTab()
// Subtitle
brls::Label *subTitle = new brls::Label(
brls::LabelStyle::REGULAR,
"Switchroot INI Configuration Editor\n"
"Edit your Linux, Android and Lakka OC settings without a PC!",
I18n::aboutSubtitle(),
true
);
subTitle->setHorizontalAlign(NVG_ALIGN_CENTER);
@@ -24,22 +24,17 @@ AboutTab::AboutTab()
// Copyright
brls::Label *copyright = new brls::Label(
brls::LabelStyle::DESCRIPTION,
"Licensed under GPL-3.0\n"
"Powered by Borealis UI framework\n"
"Based on the work of Switchroot\n"
"\u00A9 2026 NiklasCFW",
I18n::aboutCopyright(),
true
);
copyright->setHorizontalAlign(NVG_ALIGN_CENTER);
this->addView(copyright);
// Links
this->addView(new brls::Header("Links and Resources"));
this->addView(new brls::Header(I18n::headerLinksAndResources()));
brls::Label *links = new brls::Label(
brls::LabelStyle::SMALL,
"\uE016 NiklasCFW Docs for setup guides and documentation\n"
"\uE016 NiklasCFW's Discord server for support and community\n"
"\uE016 Source code available on the OmniNX GitHub",
I18n::aboutLinks(),
true
);
this->addView(links);

View File

@@ -4,6 +4,7 @@
*/
#include "file_browser.h"
#include "i18n.h"
#include <dirent.h>
#include <sys/stat.h>
@@ -78,7 +79,7 @@ std::vector<FileBrowser::DirEntry> FileBrowser::listDirectory(const std::string&
void FileBrowser::navigate(const std::string& dir)
{
this->currentDir = dir;
this->setTitle("Select INI \u2014 " + dir);
this->setTitle(std::string(I18n::fileBrowserTitlePrefix()) + dir);
// Create a fresh list (setContentView frees the old one)
this->list = new brls::List();
@@ -89,7 +90,7 @@ void FileBrowser::navigate(const std::string& dir)
if (entries.empty())
{
brls::ListItem* emptyItem = new brls::ListItem("No .ini files found in " + dir);
brls::ListItem* emptyItem = new brls::ListItem(I18n::fileBrowserNoIni(dir));
this->list->addView(emptyItem);
return;
}

264
src/i18n.cpp Normal file
View File

@@ -0,0 +1,264 @@
/*
SWR INI Tool - Switchroot INI Configuration Editor
Copyright (C) 2026 Switchroot
*/
#include "i18n.h"
#ifdef __SWITCH__
#include <switch.h>
#endif
namespace
{
bool gGerman = false;
#ifdef __SWITCH__
void detectSystemLanguage()
{
gGerman = false;
Result rc = setInitialize();
if (!R_SUCCEEDED(rc))
return;
u64 code = 0;
SetLanguage lang = SetLanguage_ENUS;
if (R_SUCCEEDED(setGetSystemLanguage(&code)) && R_SUCCEEDED(setMakeLanguage(code, &lang)))
{
if (lang == SetLanguage_DE)
gGerman = true;
}
setExit();
}
#endif
struct Triple
{
const char* key;
const char* en;
const char* de;
};
static const Triple OC_BOOL_LABEL[] = {
{"oc", "Overclocking", "Übertaktung"},
{"dvfsb", "CPU DVFS Boost", "CPU-DVFS-Boost"},
{"gpu_dvfsc", "GPU DVFS Scaling", "GPU-DVFS-Skalierung"},
{"usb3force", "Force USB 3.0", "USB 3.0 erzwingen"},
{"ddr200_enable", "DDR200 Enable", "DDR200 aktivieren"},
};
static const Triple OC_FREQ_LABEL[] = {
{"max_cpu_freq", "Max CPU Frequency", "Max. CPU-Taktfrequenz"},
{"max_gpu_freq", "Max GPU Frequency", "Max. GPU-Taktfrequenz"},
{"ram_oc", "RAM Frequency", "RAM-Taktfrequenz"},
};
static const Triple OC_VOLT_LABEL[] = {
{"ram_oc_vdd2", "RAM VDD2 Voltage", "RAM VDD2-Spannung"},
{"ram_oc_vddq", "RAM VDDQ Voltage", "RAM VDDQ-Spannung"},
};
const char* lookup(const Triple* table, size_t n, const std::string& key, const char* fallback)
{
for (size_t i = 0; i < n; i++)
{
if (key == table[i].key)
return gGerman ? table[i].de : table[i].en;
}
return fallback;
}
} // namespace
namespace I18n
{
void init()
{
#ifdef __SWITCH__
detectSystemLanguage();
#else
gGerman = false;
#endif
}
bool isGerman()
{
return gGerman;
}
const char* appTitle()
{
return gGerman ? "SWR INI-Tool" : "SWR INI Tool";
}
const char* tabSettings()
{
return gGerman ? "Einstellungen" : "Settings";
}
const char* tabAbout()
{
return gGerman ? "Über" : "About";
}
const char* wordOn()
{
return gGerman ? "Ein" : "ON";
}
const char* wordOff()
{
return gGerman ? "Aus" : "OFF";
}
std::string sectionEditLine(const std::string& section, const std::string& path)
{
if (gGerman)
return "Abschnitt [" + section + "] aus " + path;
return "Editing section [" + section + "] from " + path;
}
const char* headerToggleOptions()
{
return gGerman ? "Schalter" : "Toggle Options";
}
const char* headerFrequencySettings()
{
return gGerman ? "Frequenz" : "Frequency Settings";
}
const char* headerVoltageSettings()
{
return gGerman ? "Spannung" : "Voltage Settings";
}
const char* headerOther()
{
return gGerman ? "Sonstiges" : "Other";
}
const char* headerIniFilePaths()
{
return gGerman ? "INI-Dateipfade" : "INI File Paths";
}
const char* headerLinksAndResources()
{
return gGerman ? "Links und Infos" : "Links and Resources";
}
const char* errorListItemTitleIni()
{
return gGerman ? "\uE150 INI-Problem" : "\uE150 INI file not found";
}
std::string iniLoadErrorBody(const std::string& path)
{
if (gGerman)
return "Die Datei konnte nicht geladen werden:\n" + path + "\n\n"
"Prüfen Sie den Pfad in den Einstellungen.";
return "Could not load " + path + "\n\n"
"Make sure the INI file exists at the expected path.\n"
"You can configure paths in the Settings tab.";
}
std::string iniSectionErrorBody(const std::string& path, const std::string& osName)
{
if (gGerman)
return "Kein OS-Abschnitt in " + path + "\n\n"
"Die INI sollte z. B. einen Abschnitt ["
+ osName + " OC] enthalten.";
return "No OS section found in " + path + "\n\n"
"The INI file should contain a section like\n"
"[" + osName + " OC] with your configuration.";
}
const char* notifyConfigSaved()
{
return gGerman ? "\uE14B Konfiguration gespeichert" : "\uE14B Configuration saved";
}
const char* notifyConfigSaveError()
{
return gGerman ? "\uE150 Fehler beim Speichern!" : "\uE150 Error saving configuration!";
}
const char* notifyPathUpdated()
{
return gGerman ? "\uE14B Pfad aktualisiert" : "\uE14B Path updated";
}
const char* settingsIntro()
{
return gGerman ? "Wählen Sie, welche INI-Datei jeder Reiter liest und schreibt.\n"
"Änderungen gelten sofort."
: "Select which INI file each OS tab reads and writes.\n"
"Changes take effect immediately.";
}
const char* pathListDescriptionLakka()
{
return gGerman ? "Tippen, um eine INI-Datei zu wählen" : "Tap to browse for an INI file";
}
const char* fileBrowserTitlePrefix()
{
return gGerman ? "INI wählen \u2014 " : "Select INI \u2014 ";
}
std::string fileBrowserNoIni(const std::string& dir)
{
if (gGerman)
return "Keine .ini-Dateien in " + dir;
return "No .ini files found in " + dir;
}
const char* aboutSubtitle()
{
return gGerman ? "Switchroot INI-Konfiguration\n"
"Linux-, Android- und Lakka-OC ohne PC bearbeiten!"
: "Switchroot INI Configuration Editor\n"
"Edit your Linux, Android and Lakka OC settings without a PC!";
}
const char* aboutCopyright()
{
return gGerman ? "Lizenziert unter GPL-3.0\n"
"UI: Borealis\n"
"Basierend auf der Arbeit von Switchroot\n"
"\u00A9 2026 NiklasCFW"
: "Licensed under GPL-3.0\n"
"Powered by Borealis UI framework\n"
"Based on the work of Switchroot\n"
"\u00A9 2026 NiklasCFW";
}
const char* aboutLinks()
{
return gGerman ? "\uE016 NiklasCFW-Dokumentation: Anleitungen und Infos\n"
"\uE016 NiklasCFW-Discord: Fragen und Community\n"
"\uE016 Quellcode auf GitHub (OmniNX)"
: "\uE016 NiklasCFW Docs for setup guides and documentation\n"
"\uE016 NiklasCFW's Discord server for support and community\n"
"\uE016 Source code available on the OmniNX GitHub";
}
const char* ocBoolLabel(const std::string& key)
{
return lookup(OC_BOOL_LABEL, sizeof(OC_BOOL_LABEL) / sizeof(OC_BOOL_LABEL[0]), key, key.c_str());
}
const char* ocFreqLabel(const std::string& key)
{
return lookup(OC_FREQ_LABEL, sizeof(OC_FREQ_LABEL) / sizeof(OC_FREQ_LABEL[0]), key, key.c_str());
}
const char* ocVoltageLabel(const std::string& key)
{
return lookup(OC_VOLT_LABEL, sizeof(OC_VOLT_LABEL) / sizeof(OC_VOLT_LABEL[0]), key, key.c_str());
}
} // namespace I18n

57
src/i18n.h Normal file
View File

@@ -0,0 +1,57 @@
/*
SWR INI Tool - Switchroot INI Configuration Editor
Copyright (C) 2026 Switchroot
*/
#pragma once
#include <string>
namespace I18n
{
void init();
bool isGerman();
const char* appTitle();
const char* tabSettings();
const char* tabAbout();
const char* wordOn();
const char* wordOff();
std::string sectionEditLine(const std::string& section, const std::string& path);
const char* headerToggleOptions();
const char* headerFrequencySettings();
const char* headerVoltageSettings();
const char* headerOther();
const char* headerIniFilePaths();
const char* headerLinksAndResources();
const char* errorListItemTitleIni();
std::string iniLoadErrorBody(const std::string& path);
std::string iniSectionErrorBody(const std::string& path, const std::string& osName);
const char* notifyConfigSaved();
const char* notifyConfigSaveError();
const char* notifyPathUpdated();
const char* settingsIntro();
const char* pathListDescriptionLakka();
const char* fileBrowserTitlePrefix();
std::string fileBrowserNoIni(const std::string& dir);
const char* aboutSubtitle();
const char* aboutCopyright();
const char* aboutLinks();
const char* ocBoolLabel(const std::string& key);
const char* ocFreqLabel(const std::string& key);
const char* ocVoltageLabel(const std::string& key);
} // namespace I18n

View File

@@ -17,11 +17,14 @@
#include "main_frame.h"
#include "logo.h"
#include "app_config.h"
#include "i18n.h"
int main(int argc, char* argv[])
{
I18n::init();
// Init the app
if (!brls::Application::init("SWR INI Tool"))
if (!brls::Application::init(I18n::appTitle()))
{
brls::Logger::error("Unable to init Borealis application");
return EXIT_FAILURE;

View File

@@ -9,6 +9,7 @@
#include "settings_tab.h"
#include "about_tab.h"
#include "app_config.h"
#include "i18n.h"
#include "logo.h"
OsConfigTab* MainFrame::osTabs[(int)OsTarget::COUNT] = {};
@@ -17,7 +18,7 @@ MainFrame::MainFrame() : TabFrame()
{
AppConfig& cfg = AppConfig::get();
this->setTitle(APP_TITLE);
this->setTitle(I18n::appTitle());
brls::Image* headerIcon = new brls::Image(APP_ASSET("gui_icon.png"));
headerIcon->setScaleType(brls::ImageScaleType::FIT);
this->setIcon(headerIcon);
@@ -30,11 +31,11 @@ MainFrame::MainFrame() : TabFrame()
this->addTab("Android", osTabs[(int)OsTarget::ANDROID]);
this->addTab("Linux", osTabs[(int)OsTarget::LINUX]);
this->addTab("Lakka", osTabs[(int)OsTarget::LAKKA]);
this->addTab("Settings", new SettingsTab());
this->addTab(I18n::tabSettings(), new SettingsTab());
this->addSeparator();
this->addTab("About", new AboutTab());
this->addTab(I18n::tabAbout(), new AboutTab());
}
MainFrame::~MainFrame()

View File

@@ -4,6 +4,7 @@
*/
#include "os_config_tab.h"
#include "i18n.h"
#include "oc_defs.h"
#include <algorithm>
@@ -13,18 +14,14 @@ OsConfigTab::OsConfigTab(const std::string& osName, const std::string& iniPath)
{
if (!this->ini.load(iniPath))
{
buildErrorUI("Could not load " + iniPath + "\n\n"
"Make sure the INI file exists at the expected path.\n"
"You can configure paths in the Settings tab.");
buildErrorUI(I18n::errorListItemTitleIni(), I18n::iniLoadErrorBody(iniPath));
return;
}
this->osSection = this->ini.findOsSection();
if (this->osSection.empty())
{
buildErrorUI("No OS section found in " + iniPath + "\n\n"
"The INI file should contain a section like\n"
"[" + osName + " OC] with your configuration.");
buildErrorUI(I18n::errorListItemTitleIni(), I18n::iniSectionErrorBody(iniPath, osName));
return;
}
@@ -41,30 +38,26 @@ void OsConfigTab::reload(const std::string& newIniPath)
if (!this->ini.load(iniPath))
{
buildErrorUI("Could not load " + iniPath + "\n\n"
"Make sure the INI file exists at the expected path.\n"
"You can configure paths in the Settings tab.");
buildErrorUI(I18n::errorListItemTitleIni(), I18n::iniLoadErrorBody(iniPath));
return;
}
this->osSection = this->ini.findOsSection();
if (this->osSection.empty())
{
buildErrorUI("No OS section found in " + iniPath + "\n\n"
"The INI file should contain a section like\n"
"[" + osName + " OC] with your configuration.");
buildErrorUI(I18n::errorListItemTitleIni(), I18n::iniSectionErrorBody(iniPath, osName));
return;
}
buildUI();
}
void OsConfigTab::buildErrorUI(const std::string& message)
void OsConfigTab::buildErrorUI(const std::string& title, const std::string& message)
{
this->setSpacing(15);
// Use a ListItem (focusable) so borealis doesn't crash on controller nav
brls::ListItem *errorItem = new brls::ListItem("\uE150 INI file not found", message);
brls::ListItem *errorItem = new brls::ListItem(title, message);
this->addView(errorItem);
}
@@ -76,44 +69,44 @@ void OsConfigTab::buildUI()
// Section name info
brls::Label *sectionInfo = new brls::Label(
brls::LabelStyle::DESCRIPTION,
"Editing section [" + this->osSection + "] from " + this->iniPath,
I18n::sectionEditLine(this->osSection, this->iniPath),
true
);
this->addView(sectionInfo);
// ── Toggle Options ──
this->addView(new brls::Header("Toggle Options"));
this->addView(new brls::Header(I18n::headerToggleOptions()));
for (const auto& def : OC_BOOL_KEYS)
{
bool val = this->ini.getBool(this->osSection, def.key, false);
addBooleanToggle(def.label, "", def.key, val);
addBooleanToggle(I18n::ocBoolLabel(def.key), "", def.key, val);
}
// ── Frequency Settings ──
brls::Rectangle* spacer1 = new brls::Rectangle(nvgRGBA(0, 0, 0, 0));
spacer1->setHeight(30);
this->addView(spacer1);
this->addView(new brls::Header("Frequency Settings"));
this->addView(new brls::Header(I18n::headerFrequencySettings()));
for (const auto& def : OC_FREQ_KEYS)
{
int defVal = def.options.empty() ? 0 : (int)def.options.front();
uint32_t val = (uint32_t)this->ini.getInt(this->osSection, def.key, defVal);
addFreqDropdown(def.label, "", def.key, val, def.options);
addFreqDropdown(I18n::ocFreqLabel(def.key), "", def.key, val, def.options);
}
// ── Voltage Settings ──
brls::Rectangle* spacer2 = new brls::Rectangle(nvgRGBA(0, 0, 0, 0));
spacer2->setHeight(30);
this->addView(spacer2);
this->addView(new brls::Header("Voltage Settings"));
this->addView(new brls::Header(I18n::headerVoltageSettings()));
for (const auto& def : OC_VOLTAGE_KEYS)
{
int defVal = def.options.empty() ? 0 : (int)def.options.front();
uint32_t val = (uint32_t)this->ini.getInt(this->osSection, def.key, defVal);
addVoltageDropdown(def.label, "", def.key, val, def.options);
addVoltageDropdown(I18n::ocVoltageLabel(def.key), "", def.key, val, def.options);
}
// ── Other Keys (read-only info) ──
@@ -141,7 +134,7 @@ void OsConfigTab::buildUI()
brls::Rectangle* spacerOther = new brls::Rectangle(nvgRGBA(0, 0, 0, 0));
spacerOther->setHeight(30);
this->addView(spacerOther);
this->addView(new brls::Header("Other"));
this->addView(new brls::Header(I18n::headerOther()));
hasOtherKeys = true;
}
@@ -156,11 +149,11 @@ void OsConfigTab::saveAndNotify()
{
if (this->ini.save())
{
brls::Application::notify("\uE14B Configuration saved");
brls::Application::notify(I18n::notifyConfigSaved());
}
else
{
brls::Application::notify("\uE150 Error saving configuration!");
brls::Application::notify(I18n::notifyConfigSaveError());
}
}
@@ -168,7 +161,7 @@ void OsConfigTab::addBooleanToggle(const std::string& label, const std::string&
const std::string& key, bool currentValue)
{
brls::ToggleListItem *toggle = new brls::ToggleListItem(
label, currentValue, description, "ON", "OFF"
label, currentValue, description, I18n::wordOn(), I18n::wordOff()
);
std::string keyCopy = key;

View File

@@ -23,7 +23,7 @@ private:
std::string osSection;
void buildUI();
void buildErrorUI(const std::string& message);
void buildErrorUI(const std::string& title, const std::string& message);
void saveAndNotify();
// Create a toggle for a boolean key

View File

@@ -5,6 +5,7 @@
#include "settings_tab.h"
#include "file_browser.h"
#include "i18n.h"
#include "main_frame.h"
#include "os_config_tab.h"
@@ -15,14 +16,13 @@ SettingsTab::SettingsTab()
brls::Label *info = new brls::Label(
brls::LabelStyle::DESCRIPTION,
"Select which INI file each OS tab reads and writes.\n"
"Changes take effect immediately.",
I18n::settingsIntro(),
true
);
this->addView(info);
// ── INI File Paths ──
this->addView(new brls::Header("INI File Paths"));
this->addView(new brls::Header(I18n::headerIniFilePaths()));
addPathItem(OsTarget::ANDROID);
addPathItem(OsTarget::LINUX);
@@ -35,7 +35,7 @@ void SettingsTab::addPathItem(OsTarget target)
std::string currentPath = cfg.getPath(target);
std::string label = std::string(AppConfig::getLabel(target)) + " INI";
std::string description = (target == OsTarget::LAKKA) ? "Tap to browse for an INI file" : "";
std::string description = (target == OsTarget::LAKKA) ? I18n::pathListDescriptionLakka() : "";
brls::ListItem* item = new brls::ListItem(label, description);
item->setValue(currentPath);
@@ -59,7 +59,7 @@ void SettingsTab::addPathItem(OsTarget target)
if (tab)
tab->reload(selectedPath);
brls::Application::notify("\uE14B Path updated");
brls::Application::notify(I18n::notifyPathUpdated());
}
);