diff --git a/src/about_tab.cpp b/src/about_tab.cpp index a76af24..c450ae7 100644 --- a/src/about_tab.cpp +++ b/src/about_tab.cpp @@ -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); diff --git a/src/file_browser.cpp b/src/file_browser.cpp index 4f03b31..c12d6a7 100644 --- a/src/file_browser.cpp +++ b/src/file_browser.cpp @@ -4,6 +4,7 @@ */ #include "file_browser.h" +#include "i18n.h" #include #include @@ -78,7 +79,7 @@ std::vector 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; } diff --git a/src/i18n.cpp b/src/i18n.cpp new file mode 100644 index 0000000..3f36e39 --- /dev/null +++ b/src/i18n.cpp @@ -0,0 +1,264 @@ +/* + SWR INI Tool - Switchroot INI Configuration Editor + Copyright (C) 2026 Switchroot +*/ + +#include "i18n.h" + +#ifdef __SWITCH__ +#include +#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 diff --git a/src/i18n.h b/src/i18n.h new file mode 100644 index 0000000..0af3439 --- /dev/null +++ b/src/i18n.h @@ -0,0 +1,57 @@ +/* + SWR INI Tool - Switchroot INI Configuration Editor + Copyright (C) 2026 Switchroot +*/ + +#pragma once + +#include + +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 diff --git a/src/main.cpp b/src/main.cpp index eba21c9..36caa2f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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; diff --git a/src/main_frame.cpp b/src/main_frame.cpp index 86e2fec..f78b9e7 100644 --- a/src/main_frame.cpp +++ b/src/main_frame.cpp @@ -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() diff --git a/src/os_config_tab.cpp b/src/os_config_tab.cpp index 6d2a93f..229f2c4 100644 --- a/src/os_config_tab.cpp +++ b/src/os_config_tab.cpp @@ -4,6 +4,7 @@ */ #include "os_config_tab.h" +#include "i18n.h" #include "oc_defs.h" #include @@ -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; diff --git a/src/os_config_tab.h b/src/os_config_tab.h index 28b082e..1c8d1b2 100644 --- a/src/os_config_tab.h +++ b/src/os_config_tab.h @@ -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 diff --git a/src/settings_tab.cpp b/src/settings_tab.cpp index ff25ab7..fb297a5 100644 --- a/src/settings_tab.cpp +++ b/src/settings_tab.cpp @@ -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()); } );