From 28411fcdd187c43b6fe0e68122d9b39b45747ba6 Mon Sep 17 00:00:00 2001 From: Yorunokyujitsu <164279972+Yorunokyujitsu@users.noreply.github.com> Date: Tue, 25 Nov 2025 19:10:45 +0900 Subject: [PATCH] i18n: Added translatable strings, new languages, and extended localization features. - Added sub-keys to better manage long strings, allowing either the sub-key value or the original text to be used. - Multi-line values are now supported in language.json to prevent overly long single lines. - Added word-order adjustment for certain Asian languages such as Japanese and Korean. - Added separate support for Simplified and Traditional Chinese. --- sphaira/include/i18n.hpp | 11 + sphaira/source/app.cpp | 326 ++++++++++++--------- sphaira/source/dumper.cpp | 2 +- sphaira/source/i18n.cpp | 114 +++++-- sphaira/source/location.cpp | 3 +- sphaira/source/title_info.cpp | 5 +- sphaira/source/ui/menus/appstore.cpp | 12 +- sphaira/source/ui/menus/filebrowser.cpp | 38 +-- sphaira/source/ui/menus/game_menu.cpp | 8 +- sphaira/source/ui/menus/game_meta_menu.cpp | 20 +- sphaira/source/ui/menus/game_nca_menu.cpp | 28 +- sphaira/source/ui/menus/gc_menu.cpp | 10 +- sphaira/source/ui/menus/ghdl.cpp | 6 +- sphaira/source/ui/menus/homebrew.cpp | 15 +- sphaira/source/ui/menus/irs_menu.cpp | 4 +- sphaira/source/ui/menus/main_menu.cpp | 41 +-- sphaira/source/ui/menus/mtp_menu.cpp | 2 +- sphaira/source/ui/menus/save_menu.cpp | 29 +- sphaira/source/ui/menus/themezer.cpp | 8 +- sphaira/source/ui/menus/usb_menu.cpp | 2 +- sphaira/source/ui/music_player.cpp | 4 +- sphaira/source/utils/devoptab.cpp | 47 +-- sphaira/source/utils/nsz_dumper.cpp | 2 +- 23 files changed, 452 insertions(+), 285 deletions(-) diff --git a/sphaira/include/i18n.hpp b/sphaira/include/i18n.hpp index 5ed8406..8422138 100644 --- a/sphaira/include/i18n.hpp +++ b/sphaira/include/i18n.hpp @@ -5,10 +5,21 @@ namespace sphaira::i18n { +enum class WordOrder { + PhraseName, // default: SVO (English, French, German, etc.) + NamePhrase // SOV (Japanese, Korean) +}; + bool init(long index); void exit(); std::string get(std::string_view str); +std::string get(std::string_view str, std::string_view fallback); + +WordOrder GetWordOrder(); +bool WordOrderLocale(); + +std::string Reorder(std::string_view phrase, std::string_view name); } // namespace sphaira::i18n diff --git a/sphaira/source/app.cpp b/sphaira/source/app.cpp index 857259c..86bf4a9 100644 --- a/sphaira/source/app.cpp +++ b/sphaira/source/app.cpp @@ -839,9 +839,10 @@ void App::SetReplaceHbmenuEnable(bool enable) { NacpStruct actual_hbmenu_nacp; if (R_FAILED(nro_get_nacp("/switch/hbmenu.nro", actual_hbmenu_nacp))) { App::Push( - "Failed to find /switch/hbmenu.nro\n" - "Use the Appstore to re-install hbmenu"_i18n, - "OK"_i18n + i18n::get("missing_hbmenu_info", + "Failed to find /switch/hbmenu.nro\n" + "Use the Appstore to re-install hbmenu" + ), "OK"_i18n ); return; } @@ -1960,18 +1961,23 @@ void App::DisplayThemeOptions(bool left_side) { options->Add("Music"_i18n, App::GetThemeMusicEnable(), [](bool& enable){ App::SetThemeMusicEnable(enable); - }, "Enable background music.\n" - "Each theme can have it's own music file. " - "If a theme does not set a music file, the default music is loaded instead (if it exists)."_i18n); + }, i18n::get("bgm_enable_info", + "Enable background music.\n" + "Each theme can have it's own music file. " + "If a theme does not set a music file, the default music is loaded instead (if it exists)." + ) + ); options->Add("Show IP address"_i18n, App::GetApp()->m_show_ip_addr, - "Shows the IP address in all menus, including the WiFi strength.\n\n" - "NOTE: The IP address will be hidden in applet mode due to the applet warning being displayed in it's place."_i18n + i18n::get("display_ip_info", + "Shows the IP address in all menus, including the WiFi strength.\n\n" + "NOTE: The IP address will be hidden in applet mode due to the applet warning being displayed in it's place." + ) ); // todo: add file picker for music here. // todo: add array to audio which has the list of supported extensions. - auto remove_music = options->Add("Remove Background Music", [](){ + auto remove_music = options->Add("Remove Background Music"_i18n, [](){ g_app->m_default_music.Set(""); audio::CloseSong(&g_app->m_background_music); }, "Removes the background music file"_i18n); @@ -2003,7 +2009,7 @@ void App::DisplayMenuOptions(bool left_side) { }, i18n::get(e.info)); if (e.IsInstall()) { - entry->Depends(App::GetInstallEnable, i18n::get(App::INSTALL_DEPENDS_STR), App::ShowEnableInstallPrompt); + entry->Depends(App::GetInstallEnable, i18n::get("enable_install_info", App::INSTALL_DEPENDS_STR), App::ShowEnableInstallPrompt); } } @@ -2033,8 +2039,10 @@ void App::DisplayMenuOptions(bool left_side) { } ); }, - "Launch the built-in web browser.\n\n", - "NOTE: The browser is very limted, some websites will fail to load and there's a 30 minute timeout which closes the browser"_i18n); + i18n::get("web_browser_info", + "Launch the built-in web browser.\n\n" + "NOTE: The browser is very limted, some websites will fail to load and there's a 30 minute timeout which closes the browser" + )); } } @@ -2064,18 +2072,21 @@ void App::DisplayAdvancedOptions(bool left_side) { options->Add("Replace hbmenu on exit"_i18n, App::GetReplaceHbmenuEnable(), [](bool& enable){ App::SetReplaceHbmenuEnable(enable); - }, "When enabled, it replaces /hbmenu.nro with Sphaira, creating a backup of hbmenu to /switch/hbmenu.nro\n\n" \ - "Disabling will give you the option to restore hbmenu."_i18n); + }, i18n::get("hbmenu_replace_info", + "When enabled, it replaces /hbmenu.nro with Sphaira, creating a backup of hbmenu to /switch/hbmenu.nro\n\n" + "Disabling will give you the option to restore hbmenu.")); options->Add("Add / modify mounts"_i18n, [](){ devoptab::DisplayDevoptabSideBar(); - }, "Create, modify, delete network mounts (HTTP, FTP, SFTP, SMB, NFS).\n" - "Mount options only require a URL and Name be set, with other fields being optional, such as port, user, pass etc.\n\n" - "Any changes made will require restarting Sphaira to take effect."_i18n); + }, i18n::get("mount_options_info", + "Create, modify, delete network mounts (HTTP, FTP, SFTP, SMB, NFS).\n" + "Mount options only require a URL and Name be set, with other fields being optional, such as port, user, pass etc.\n\n" + "Any changes made will require restarting Sphaira to take effect.")); options->Add("Boost CPU during transfer"_i18n, App::GetApp()->m_progress_boost_mode, - "Enables boost mode during transfers which can improve transfer speed. " - "This sets the CPU to 1785mhz and lowers the GPU 76mhz"_i18n); + i18n::get("transfer_boost_info", + "Enables boost mode during transfers which can improve transfer speed. " + "This sets the CPU to 1785mhz and lowers the GPU 76mhz")); options->Add("Text scroll speed"_i18n, text_scroll_speed_items, [](s64& index_out){ App::SetTextScrollSpeed(index_out); @@ -2141,8 +2152,9 @@ void App::DisplayAdvancedOptions(bool left_side) { options->Add("Install options"_i18n, [left_side](){ App::DisplayInstallOptions(left_side); - }, "Change the install options.\n" - "You can enable installing from here."_i18n); + }, i18n::get("install_options_info", + "Change the install options.\n" + "You can enable installing from here.")); options->Add("Export options"_i18n, [left_side](){ App::DisplayDumpOptions(left_side); @@ -2194,46 +2206,54 @@ void App::DisplayInstallOptions(bool left_side) { "Skips installing tickets, not recommended."_i18n); options->Add("Skip NCA hash verify"_i18n, App::GetApp()->m_skip_nca_hash_verify, - "Enables the option to skip sha256 verification. This is a hash over the entire NCA. " - "It is used to verify that the NCA is valid / not corrupted. " - "You may have seen the option for \"checking for corrupted data\" when a corrupted game is installed. " - "That check performs various hash checks, including the hash over the NCA.\n\n" - "It is recommended to keep this disabled."_i18n); + i18n::get("skip_nca_info", + "Enables the option to skip sha256 verification. This is a hash over the entire NCA. " + "It is used to verify that the NCA is valid / not corrupted. " + "You may have seen the option for \"checking for corrupted data\" when a corrupted game is installed. " + "That check performs various hash checks, including the hash over the NCA.\n\n" + "It is recommended to keep this disabled.")); options->Add("Skip RSA header verify"_i18n, App::GetApp()->m_skip_rsa_header_fixed_key_verify, - "Enables the option to skip RSA NCA fixed key verification. " - "This is a hash over the NCA header. It is used to verify that the header has not been modified. " - "The header is signed by nintendo, thus it cannot be forged, and is reliable to detect modified NCA headers (such as NSP/XCI converts).\n\n" - "It is recommended to keep this disabled, unless you need to install nsp/xci converts."_i18n); + i18n::get("nca_verify_info", + "Enables the option to skip RSA NCA fixed key verification. " + "This is a hash over the NCA header. It is used to verify that the header has not been modified. " + "The header is signed by nintendo, thus it cannot be forged, and is reliable to detect modified NCA headers (such as NSP/XCI converts).\n\n" + "It is recommended to keep this disabled, unless you need to install nsp/xci converts.")); options->Add("Skip RSA NPDM verify"_i18n, App::GetApp()->m_skip_rsa_npdm_fixed_key_verify, - "Enables the option to skip RSA NPDM fixed key verification.\n\n" - "Currently, this option is stubbed (not implemented)."_i18n); + i18n::get("npdm_verify_info", + "Enables the option to skip RSA NPDM fixed key verification.\n\n" + "Currently, this option is stubbed (not implemented).")); options->Add("Ignore distribution bit"_i18n, App::GetApp()->m_ignore_distribution_bit, - "If set, it will ignore the distribution bit in the NCA header. " - "The distribution bit is used to signify whether a NCA is Eshop or GameCard. " - "You cannot (normally) launch install games that have the distruction bit set to GameCard.\n\n" - "It is recommended to keep this disabled."_i18n); + i18n::get("nca_distbit_info", + "If set, it will ignore the distribution bit in the NCA header. " + "The distribution bit is used to signify whether a NCA is Eshop or GameCard. " + "You cannot (normally) launch install games that have the distruction bit set to GameCard.\n\n" + "It is recommended to keep this disabled.")); options->Add("Convert to common ticket"_i18n, App::GetApp()->m_convert_to_common_ticket, - "[Requires keys] Converts personalised tickets to common (fake) tickets.\n\n" - "It is recommended to keep this enabled."_i18n); + i18n::get("ticket_convert_info", + "[Requires keys] Converts personalised tickets to common (fake) tickets.\n\n" + "It is recommended to keep this enabled.")); options->Add("Convert to standard crypto"_i18n, App::GetApp()->m_convert_to_standard_crypto, - "[Requires keys] Converts titlekey to standard crypto, also known as \"ticketless\".\n\n" - "It is recommended to keep this disabled."_i18n); + i18n::get("titlekey_crypto_info", + "[Requires keys] Converts titlekey to standard crypto, also known as \"ticketless\".\n\n" + "It is recommended to keep this disabled.")); options->Add("Lower master key"_i18n, App::GetApp()->m_lower_master_key, - "[Requires keys] Encrypts the keak (key area key) with master key 0, which allows the game to be launched on every fw. " - "Implicitly performs standard crypto.\n\n" - "Do note that just because the game can be launched on any fw (as it can be decrypted), doesn't mean it will work. It is strongly recommened to update your firmware and Atmosphere version in order to play the game, rather than enabling this option.\n\n" - "It is recommended to keep this disabled."_i18n); + i18n::get("keyarea_crypto_info", + "[Requires keys] Encrypts the keak (key area key) with master key 0, which allows the game to be launched on every fw. " + "Implicitly performs standard crypto.\n\n" + "Do note that just because the game can be launched on any fw (as it can be decrypted), doesn't mean it will work. It is strongly recommened to update your firmware and Atmosphere version in order to play the game, rather than enabling this option.\n\n" + "It is recommended to keep this disabled.")); options->Add("Lower system version"_i18n, App::GetApp()->m_lower_system_version, - "Sets the system_firmware field in the cnmt extended header to 0. " - "Note: if the master key is higher than fw version, the game still won't launch as the fw won't have the key to decrypt keak (see above).\n\n" - "It is recommended to keep this disabled."_i18n); + i18n::get("cnmt_fw_info", + "Sets the system_firmware field in the cnmt extended header to 0. " + "Note: if the master key is higher than fw version, the game still won't launch as the fw won't have the key to decrypt keak (see above).\n\n" + "It is recommended to keep this disabled.")); } void App::DisplayDumpOptions(bool left_side) { @@ -2242,30 +2262,34 @@ void App::DisplayDumpOptions(bool left_side) { ui::SidebarEntryArray::Items nsz_level_items; for (auto& e : NSZ_COMPRESS_LEVEL_OPTIONS) { - nsz_level_items.emplace_back(e.name); + nsz_level_items.emplace_back(i18n::get(e.name)); } ui::SidebarEntryArray::Items nsz_thread_items; for (auto& e : NSZ_COMPRESS_THREAD_OPTIONS) { - nsz_thread_items.emplace_back(e.name); + nsz_thread_items.emplace_back(i18n::get(e.name)); } ui::SidebarEntryArray::Items nsz_block_items; for (auto& e : NSZ_COMPRESS_BLOCK_OPTIONS) { - nsz_block_items.emplace_back(e.name); + nsz_block_items.emplace_back(i18n::get(e.name)); } options->Add( "Created nested folder"_i18n, App::GetApp()->m_dump_app_folder, - "Creates a folder using the name of the game.\n" - "For example, /name/name.xci\n" - "Disabling this would use /name.xci"_i18n + i18n::get("game_folder_info", + "Creates a folder using the name of the game.\n" + "For example, /name/name.xci\n" + "Disabling this would use /name.xci" + ) ); options->Add( "Append folder with .xci"_i18n, App::GetApp()->m_dump_append_folder_with_xci, - "XCI dumps will name the folder with the .xci extension.\n" - "For example, /name.xci/name.xci\n\n" - "Some devices only function is the xci folder is named exactly the same as the xci."_i18n + i18n::get("xci_folder_info", + "XCI dumps will name the folder with the .xci extension.\n" + "For example, /name.xci/name.xci\n\n" + "Some devices only function is the xci folder is named exactly the same as the xci." + ) ); options->Add( "Trim XCI"_i18n, App::GetApp()->m_dump_trim_xci, @@ -2284,20 +2308,24 @@ void App::DisplayDumpOptions(bool left_side) { options->Add("NSZ level"_i18n, nsz_level_items, [](s64& index_out){ App::GetApp()->m_nsz_compress_level.Set(index_out); }, App::GetApp()->m_nsz_compress_level.Get(), - "Sets the compression level used when exporting to NSZ.\n\n" - "NOTE: The switch CPU is not very fast, and setting the value too high can " - "result in exporting taking a very long time for very little gain in size.\n\n" - "It is recommended to set this value to 3."_i18n + i18n::get("compress_level_info", + "Sets the compression level used when exporting to NSZ.\n\n" + "NOTE: The switch CPU is not very fast, and setting the value too high can " + "result in exporting taking a very long time for very little gain in size.\n\n" + "It is recommended to set this value to 3." + ) ); options->Add("NSZ threads"_i18n, nsz_thread_items, [](s64& index_out){ App::GetApp()->m_nsz_compress_threads.Set(index_out); }, App::GetApp()->m_nsz_compress_threads.Get(), - "Sets the number of threads used when compression the NCA.\n\n" - "A value less than 3 allows for another thread to run freely, such as read/write threads. " - "However in my testing, a value of 3 was usually the most performant.\n" - "A value of 0 will use no threads and should only be used for testing as it is always slower.\n\n" - "It is recommended to set this value between 1-3."_i18n + i18n::get("compress_threads_info", + "Sets the number of threads used when compression the NCA.\n\n" + "A value less than 3 allows for another thread to run freely, such as read/write threads. " + "However in my testing, a value of 3 was usually the most performant.\n" + "A value of 0 will use no threads and should only be used for testing as it is always slower.\n\n" + "It is recommended to set this value between 1-3." + ) ); options->Add( @@ -2307,9 +2335,11 @@ void App::DisplayDumpOptions(bool left_side) { options->Add( "NSZ block compression"_i18n, App::GetApp()->m_nsz_compress_block, - "Enables block compression, which compresses the NCA into blocks (at the cost of compression ratio) " - "which allows for random access, allowing the NCZ to be mounted as a file system.\n\n" - "NOTE: Sphaira does not yet support mounting NCZ as a file system, but it will be added evntually."_i18n + i18n::get("block_compress_info", + "Enables block compression, which compresses the NCA into blocks (at the cost of compression ratio) " + "which allows for random access, allowing the NCZ to be mounted as a file system.\n\n" + "NOTE: Sphaira does not yet support mounting NCZ as a file system, but it will be added evntually." + ) ); auto block_size_option = options->Add("NSZ block size"_i18n, nsz_block_items, [](s64& index_out){ @@ -2338,7 +2368,7 @@ void App::DisplayFtpOptions(bool left_side) { }, "Enable FTP server to run in the background."_i18n); options->Add( - "Port", App::GetApp()->m_ftp_port.Get(), "", "", 1, 5, + "Port"_i18n, App::GetApp()->m_ftp_port.Get(), "", "", 1, 5, "Opens the FTP server on this port."_i18n, [](auto* input){ App::GetApp()->m_ftp_port.Set(input->GetNumValue()); @@ -2347,12 +2377,14 @@ void App::DisplayFtpOptions(bool left_side) { options->Add( "Anon"_i18n, App::GetApp()->m_ftp_anon, - "Allows you to login without setting a username and password.\n" - "If disabled, you must set a user name and password below!"_i18n + i18n::get("login_require_info", + "Allows you to login without setting a username and password.\n" + "If disabled, you must set a user name and password below!" + ) ); options->Add( - "User", App::GetApp()->m_ftp_user.Get(), "", "", -1, 64, + "User"_i18n, App::GetApp()->m_ftp_user.Get(), "", "", -1, 64, "Sets the username, must be set if anon is disabled."_i18n, [](auto* input){ App::GetApp()->m_ftp_user.Set(input->GetValue()); @@ -2360,7 +2392,7 @@ void App::DisplayFtpOptions(bool left_side) { ); options->Add( - "Pass", App::GetApp()->m_ftp_pass.Get(), "", "", -1, 64, + "Pass"_i18n, App::GetApp()->m_ftp_pass.Get(), "", "", -1, 64, "Sets the password, must be set if anon is disabled."_i18n, [](auto* input){ App::GetApp()->m_ftp_pass.Set(input->GetValue()); @@ -2379,30 +2411,34 @@ void App::DisplayFtpOptions(bool left_side) { options->Add( "Show bis storage"_i18n, App::GetApp()->m_ftp_show_bis_storage, - "Shows the bis folder which contains the following:\n" - "- BootPartition1Root.bin\n" - "- BootPartition2Root.bin\n" - "- UserDataRoot.bin\n" - "- BootConfigAndPackage2Part1.bin\n" - "- BootConfigAndPackage2Part2.bin\n" - "- BootConfigAndPackage2Part3.bin\n" - "- BootConfigAndPackage2Part4.bin\n" - "- BootConfigAndPackage2Part5.bin\n" - "- BootConfigAndPackage2Part6.bin\n" - "- CalibrationFile.bin\n" - "- SafeMode.bin\n" - "- User.bin\n" - "- System.bin\n" - "- SystemProperEncryption.bin"_i18n + i18n::get("bis_contents_info", + "Shows the bis folder which contains the following:\n" + "- BootPartition1Root.bin\n" + "- BootPartition2Root.bin\n" + "- UserDataRoot.bin\n" + "- BootConfigAndPackage2Part1.bin\n" + "- BootConfigAndPackage2Part2.bin\n" + "- BootConfigAndPackage2Part3.bin\n" + "- BootConfigAndPackage2Part4.bin\n" + "- BootConfigAndPackage2Part5.bin\n" + "- BootConfigAndPackage2Part6.bin\n" + "- CalibrationFile.bin\n" + "- SafeMode.bin\n" + "- User.bin\n" + "- System.bin\n" + "- SystemProperEncryption.bin" + ) ); options->Add( "Show bis file systems"_i18n, App::GetApp()->m_ftp_show_bis_fs, - "Shows the following bis file systems:\n" - "- bis_calibration_file\n" - "- bis_safe_mode\n" - "- bis_user\n" - "- bis_system"_i18n + i18n::get("bis_fs_info", + "Shows the following bis file systems:\n" + "- bis_calibration_file\n" + "- bis_safe_mode\n" + "- bis_user\n" + "- bis_system" + ) ); options->Add( @@ -2417,37 +2453,47 @@ void App::DisplayFtpOptions(bool left_side) { options->Add( "Show microSD contents"_i18n, App::GetApp()->m_ftp_show_content_sd, - "Shows the microSD contents folder.\n\n" - "NOTE: This is not the normal microSD card storage, it is instead " - "the location where NCA's are stored. The normal microSD card is always mounted."_i18n + i18n::get("microsd_contents_info", + "Shows the microSD contents folder.\n\n" + "NOTE: This is not the normal microSD card storage, it is instead " + "the location where NCA's are stored. The normal microSD card is always mounted." + ) ); options->Add( "Show games"_i18n, App::GetApp()->m_ftp_show_games, - "Shows the games folder.\n\n" - "This folder contains all of your installed games, allowing you to create " - "backups over FTP!\n\n" - "NOTE: This folder is read-only. You cannot delete games over FTP."_i18n + i18n::get("games_ftp_info", + "Shows the games folder.\n\n" + "This folder contains all of your installed games, allowing you to create " + "backups over FTP!\n\n" + "NOTE: This folder is read-only. You cannot delete games over FTP." + ) ); options->Add( "Show install"_i18n, App::GetApp()->m_ftp_show_install, - "Shows the install folder.\n\n" - "This folder is used for installing games via FTP.\n\n" - "NOTE: You must open the \"FTP Install\" menu when trying to install a game!"_i18n + i18n::get("install_ftp_info", + "Shows the install folder.\n\n" + "This folder is used for installing games via FTP.\n\n" + "NOTE: You must open the \"FTP Install\" menu when trying to install a game!" + ) ); options->Add( "Show mounts"_i18n, App::GetApp()->m_ftp_show_mounts, - "Shows the mounts folder.\n\n" - "This folder is contains all of the mounts added to Sphaira, allowing you to acces them over FTP!\n" - "For example, you can access your SMB, WebDav or other FTP mounts over FTP."_i18n + i18n::get("mounts_ftp_info", + "Shows the mounts folder.\n\n" + "This folder is contains all of the mounts added to Sphaira, allowing you to acces them over FTP!\n" + "For example, you can access your SMB, WebDav or other FTP mounts over FTP." + ) ); options->Add( "Show switch"_i18n, App::GetApp()->m_ftp_show_switch, - "Shows the shortcut for the /switch folder." - "This is the folder that contains all your homebrew (NRO's)."_i18n + i18n::get("homebrew_folder_info", + "Shows the shortcut for the /switch folder." + "This is the folder that contains all your homebrew (NRO's)." + ) ); } @@ -2473,13 +2519,15 @@ void App::DisplayMtpOptions(bool left_side) { #if 0 options->Add( "Pre-allocate file"_i18n, App::GetApp()->m_mtp_allocate_file, - "Enables pre-allocating the file size before writing.\n" - "This speeds up file writes, however, this can cause timeouts if all these conditions are met:\n" - "- using Windows\n" - "- using emuMMC\n" - "- transferring a large file (>1GB)\n\n" - "This option should be left enabled, however if you use the above and experience timeouts, " - "then try again with this option disabled."_i18n + i18n::get("prealloc_info", + "Enables pre-allocating the file size before writing.\n" + "This speeds up file writes, however, this can cause timeouts if all these conditions are met:\n" + "- using Windows\n" + "- using emuMMC\n" + "- transferring a large file (>1GB)\n\n" + "This option should be left enabled, however if you use the above and experience timeouts, " + "then try again with this option disabled." + ) ); #endif @@ -2490,9 +2538,11 @@ void App::DisplayMtpOptions(bool left_side) { options->Add( "Show microSD contents"_i18n, App::GetApp()->m_mtp_show_content_sd, - "Shows the microSD contents folder.\n\n" - "NOTE: This is not the normal microSD card storage, it is instead " - "the location where NCA's are stored. The normal microSD card is always mounted."_i18n + i18n::get("microsd_contents_info", + "Shows the microSD contents folder.\n\n" + "NOTE: This is not the normal microSD card storage, it is instead " + "the location where NCA's are stored. The normal microSD card is always mounted." + ) ); options->Add( @@ -2507,31 +2557,39 @@ void App::DisplayMtpOptions(bool left_side) { options->Add( "Show games"_i18n, App::GetApp()->m_mtp_show_games, - "Shows the games folder.\n\n" - "This folder contains all of your installed games, allowing you to create " - "backups over MTP!\n\n" - "NOTE: This folder is read-only. You cannot delete games over MTP."_i18n + i18n::get("games_mtp_info", + "Shows the games folder.\n\n" + "This folder contains all of your installed games, allowing you to create " + "backups over MTP!\n\n" + "NOTE: This folder is read-only. You cannot delete games over MTP." + ) ); options->Add( "Show install"_i18n, App::GetApp()->m_mtp_show_install, - "Shows the install folder.\n\n" - "This folder is used for installing games via MTP.\n\n" - "NOTE: You must open the \"MTP Install\" menu when trying to install a game!"_i18n + i18n::get("install_mtp_info", + "Shows the install folder.\n\n" + "This folder is used for installing games via MTP.\n\n" + "NOTE: You must open the \"MTP Install\" menu when trying to install a game!" + ) ); options->Add( "Show mounts"_i18n, App::GetApp()->m_mtp_show_mounts, - "Shows the mounts folder.\n\n" - "This folder is contains all of the mounts added to Sphaira, allowing you to acces them over MTP!\n" - "For example, you can access your SMB, WebDav and FTP mounts over MTP."_i18n + i18n::get("mounts_mtp_info", + "Shows the mounts folder.\n\n" + "This folder is contains all of the mounts added to Sphaira, allowing you to acces them over MTP!\n" + "For example, you can access your SMB, WebDav and FTP mounts over MTP." + ) ); options->Add( "Show DevNull"_i18n, App::GetApp()->m_mtp_show_speedtest, - "Shows the DevNull (Speed Test) folder.\n\n" - "This folder is used for benchmarking USB uploads. " - "This ia virtual folder, nothing is actally written to disk."_i18n + i18n::get("usb_benchmark_info", + "Shows the DevNull (Speed Test) folder.\n\n" + "This folder is used for benchmarking USB uploads. " + "This ia virtual folder, nothing is actally written to disk." + ) ); } @@ -2541,8 +2599,10 @@ void App::DisplayHddOptions(bool left_side) { options->Add("Enable"_i18n, App::GetHddEnable(), [](bool& enable){ App::SetHddEnable(enable); - }, "Enable mounting of connected USB/HDD devices. " - "Connected devices can be used in the FileBrowser, as well as a backup location when dumping games and saves."_i18n + }, i18n::get("mount_hdd_info", + "Enable mounting of connected USB/HDD devices. " + "Connected devices can be used in the FileBrowser, as well as a backup location when dumping games and saves." + ) ); options->Add("HDD write protect"_i18n, App::GetWriteProtect(), [](bool& enable){ diff --git a/sphaira/source/dumper.cpp b/sphaira/source/dumper.cpp index dfff8b8..65e7514 100644 --- a/sphaira/source/dumper.cpp +++ b/sphaira/source/dumper.cpp @@ -285,7 +285,7 @@ Result DumpToUsb(ui::ProgressBox* pbox, BaseSource* source, std::spanGetSize(path); pbox->SetImage(source->GetIcon(path)); pbox->SetTitle(source->GetName(path)); - pbox->NewTransfer("Waiting for USB connection..."); + pbox->NewTransfer("Waiting for USB connection..."_i18n); // wait until usb is ready. while (true) { diff --git a/sphaira/source/i18n.cpp b/sphaira/source/i18n.cpp index b5cb5e9..53c03b9 100644 --- a/sphaira/source/i18n.cpp +++ b/sphaira/source/i18n.cpp @@ -14,42 +14,86 @@ yyjson_val* root{}; std::unordered_map g_tr_cache{}; Mutex g_mutex{}; -std::string get_internal(std::string_view str) { +static WordOrder g_word_order = WordOrder::PhraseName; + +static WordOrder DetectWordOrder(const std::string& lang) { + // SOV Language. + if (lang == "ja" || lang == "ko") + return WordOrder::NamePhrase; + + // Default: SVO Language. + return WordOrder::PhraseName; +} + +static std::string get_internal(std::string_view str, std::string_view fallback) { SCOPED_MUTEX(&g_mutex); - const std::string kkey = {str.data(), str.length()}; + const std::string kkey{str.data(), str.length()}; if (auto it = g_tr_cache.find(kkey); it != g_tr_cache.end()) { return it->second; } // add default entry - const auto it = g_tr_cache.emplace(kkey, kkey).first; + const auto it = g_tr_cache.emplace(kkey, std::string{fallback}).first; if (!json || !root) { log_write("no json or root\n"); - return kkey; + return std::string{fallback}; } - auto key = yyjson_obj_getn(root, str.data(), str.length()); - if (!key) { + yyjson_val* node = yyjson_obj_getn(root, str.data(), str.length()); + if (!node && str != fallback) { + node = yyjson_obj_getn(root, fallback.data(), fallback.length()); + if (node) { + log_write("\tfallback-key matched: [%s]\n", std::string(fallback).c_str()); + } + } + + if (!node) { log_write("\tfailed to find key: [%s]\n", kkey.c_str()); - return kkey; + return std::string{fallback}; } - auto val = yyjson_get_str(key); - auto val_len = yyjson_get_len(key); - if (!val || !val_len) { + std::string ret; + + // key > string + if (const char* val = yyjson_get_str(node)) { + size_t len = yyjson_get_len(node); + if (len) { + ret.assign(val, len); + } + } + + // key > array of strings (multi-line) + if (ret.empty() && yyjson_is_arr(node)) { + size_t idx, max; + yyjson_val* elem; + yyjson_arr_foreach(node, idx, max, elem) { + if (idx) ret.push_back('\n'); + + if (yyjson_is_str(elem)) { + const char* s = yyjson_get_str(elem); + size_t len = yyjson_get_len(elem); + if (s && len) ret.append(s, len); + } + } + } + + if (ret.empty()) { log_write("\tfailed to get value: [%s]\n", kkey.c_str()); - return kkey; + ret = std::string{fallback}; } // update entry in cache - const std::string ret = {val, val_len}; g_tr_cache.insert_or_assign(it, kkey, ret); return ret; } +static std::string get_internal(std::string_view str) { + return get_internal(str, str); +} + } // namespace bool init(long index) { @@ -76,14 +120,15 @@ bool init(long index) { case 4: setLanguage = SetLanguage_DE; break; // "German" case 5: setLanguage = SetLanguage_IT; break; // "Italian" case 6: setLanguage = SetLanguage_ES; break; // "Spanish" - case 7: setLanguage = SetLanguage_ZHCN; break; // "Chinese" + case 7: setLanguage = SetLanguage_ZHCN; break; // "Chinese (Simplified)" case 8: setLanguage = SetLanguage_KO; break; // "Korean" case 9: setLanguage = SetLanguage_NL; break; // "Dutch" case 10: setLanguage = SetLanguage_PT; break; // "Portuguese" case 11: setLanguage = SetLanguage_RU; break; // "Russian" - case 12: lang_name = "se"; break; // "Swedish" - case 13: lang_name = "vi"; break; // "Vietnamese" - case 14: lang_name = "uk"; break; // "Ukrainian" + case 12: setLanguage = SetLanguage_ZHTW; break; // "Chinese (Traditional)" + case 13: lang_name = "se"; break; // "Swedish" + case 14: lang_name = "vi"; break; // "Vietnamese" + case 15: lang_name = "uk"; break; // "Ukrainian" } switch (setLanguage) { @@ -92,15 +137,17 @@ bool init(long index) { case SetLanguage_DE: lang_name = "de"; break; case SetLanguage_IT: lang_name = "it"; break; case SetLanguage_ES: lang_name = "es"; break; - case SetLanguage_ZHCN: lang_name = "zh"; break; + case SetLanguage_ZHCN: lang_name = "zh-CN"; break; case SetLanguage_KO: lang_name = "ko"; break; case SetLanguage_NL: lang_name = "nl"; break; case SetLanguage_PT: lang_name = "pt"; break; case SetLanguage_RU: lang_name = "ru"; break; - case SetLanguage_ZHTW: lang_name = "zh"; break; + case SetLanguage_ZHTW: lang_name = "zh-TW"; break; default: break; } + g_word_order = DetectWordOrder(lang_name); + const fs::FsPath sdmc_path = "/config/sphaira/i18n/" + lang_name + ".json"; const fs::FsPath romfs_path = "romfs:/i18n/" + lang_name + ".json"; fs::FsPath path = sdmc_path; @@ -138,6 +185,7 @@ void exit() { if (json) { yyjson_doc_free(json); json = nullptr; + root = nullptr; } g_i18n_data.clear(); } @@ -146,12 +194,40 @@ std::string get(std::string_view str) { return get_internal(str); } +std::string get(std::string_view str, std::string_view fallback) { + return get_internal(str, fallback); +} + +// Reorders sentence structure based on locale. +WordOrder GetWordOrder() { + return g_word_order; +} + +bool WordOrderLocale() { + return g_word_order == WordOrder::NamePhrase; +} + +std::string Reorder(std::string_view phrase, std::string_view name) { + std::string p = i18n::get(phrase); + std::string out; + out.reserve(phrase.length() + name.length()); + + if (g_word_order == WordOrder::NamePhrase) { + out.append(name); + out.append(p); + } else { + out.append(p); + out.append(name); + } + return out; +} + } // namespace sphaira::i18n namespace literals { std::string operator""_i18n(const char* str, size_t len) { - return sphaira::i18n::get_internal({str, len}); + return sphaira::i18n::get({str, len}); } } // namespace literals diff --git a/sphaira/source/location.cpp b/sphaira/source/location.cpp index aacf6c2..28ee22f 100644 --- a/sphaira/source/location.cpp +++ b/sphaira/source/location.cpp @@ -2,6 +2,7 @@ #include "fs.hpp" #include "app.hpp" #include "utils/devoptab.hpp" +#include "i18n.hpp" #include @@ -29,7 +30,7 @@ auto GetStdio(bool write) -> StdioEntries { } if (e.flags & FsEntryFlag::FsEntryFlag_ReadOnly) { - e.name += " (Read Only)"; + e.name += i18n::get(" (Read Only)"); } out.emplace_back(e); diff --git a/sphaira/source/title_info.cpp b/sphaira/source/title_info.cpp index c3289b5..c7bd77f 100644 --- a/sphaira/source/title_info.cpp +++ b/sphaira/source/title_info.cpp @@ -8,6 +8,7 @@ #include "yati/nx/ncm.hpp" #include "utils/thread.hpp" +#include "i18n.hpp" #include #include @@ -113,8 +114,8 @@ auto& GetNcmEntry(u8 storage_id) { void FakeNacpEntry(ThreadResultData* e) { e->status = NacpLoadStatus::Error; // fake the nacp entry - std::strcpy(e->lang.name, "Corrupted"); - std::strcpy(e->lang.author, "Corrupted"); + std::strcpy(e->lang.name, "Corrupted"_i18n.c_str()); + std::strcpy(e->lang.author, "Corrupted"_i18n.c_str()); } Result LoadControlManual(u64 id, NacpStruct& nacp, ThreadResultData* data) { diff --git a/sphaira/source/ui/menus/appstore.cpp b/sphaira/source/ui/menus/appstore.cpp index 9ffceb7..f4cccbf 100644 --- a/sphaira/source/ui/menus/appstore.cpp +++ b/sphaira/source/ui/menus/appstore.cpp @@ -354,7 +354,7 @@ auto UninstallApp(ProgressBox* pbox, const Entry& entry) -> Result { // remove directory, this will also delete manifest and info const auto dir = BuildPackageCachePath(entry); - pbox->NewTransfer("Removing "_i18n + dir.toString()); + pbox->NewTransfer(i18n::Reorder("Removing ", dir.toString())); if (R_FAILED(fs.DeleteDirectoryRecursively(dir))) { log_write("failed to delete folder: %s\n", dir.s); } else { @@ -384,7 +384,7 @@ auto InstallApp(ProgressBox* pbox, const Entry& entry) -> Result { // 1. download the zip if (!pbox->ShouldExit()) { - pbox->NewTransfer("Downloading "_i18n + entry.title); + pbox->NewTransfer(i18n::Reorder("Downloading ", entry.title)); log_write("starting download\n"); const auto url = BuildZipUrl(entry); @@ -739,7 +739,7 @@ void EntryMenu::Draw(NVGcontext* vg, Theme* theme) { text_start_y += text_inc_y; gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "updated: %s"_i18n.c_str(), m_entry.updated.c_str()); text_start_y += text_inc_y; - gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "category: %s"_i18n.c_str(), m_entry.category.c_str()); + gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "category: %s"_i18n.c_str(), i18n::get(m_entry.category).c_str()); text_start_y += text_inc_y; gfx::drawTextArgs(vg, text_start_x, text_start_y, font_size, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "extracted: %s"_i18n.c_str(), utils::formatSizeStorage(m_entry.extracted).c_str()); text_start_y += text_inc_y; @@ -806,7 +806,7 @@ void EntryMenu::UpdateOptions() { App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n); if (R_SUCCEEDED(rc)) { - App::Notify("Downloaded "_i18n + m_entry.title); + App::Notify(i18n::Reorder("Downloaded ", m_entry.title)); m_entry.status = EntryStatus::Installed; m_menu.SetDirty(); UpdateOptions(); @@ -822,7 +822,7 @@ void EntryMenu::UpdateOptions() { App::PushErrorBox(rc, "Failed to, TODO: add message here"_i18n); if (R_SUCCEEDED(rc)) { - App::Notify("Removed "_i18n + m_entry.title); + App::Notify(i18n::Reorder("Removed ", m_entry.title)); m_entry.status = EntryStatus::Get; m_menu.SetDirty(); UpdateOptions(); @@ -833,7 +833,7 @@ void EntryMenu::UpdateOptions() { const Option install_option{"Install"_i18n, install}; const Option update_option{"Update"_i18n, install}; const Option launch_option{"Launch"_i18n, launch}; - const Option remove_option{"Remove"_i18n, "Completely remove "_i18n + m_entry.title + '?', uninstall}; + const Option remove_option{"Remove"_i18n, i18n::Reorder("Completely remove ", m_entry.title) + '?', uninstall}; m_options.clear(); switch (m_entry.status) { diff --git a/sphaira/source/ui/menus/filebrowser.cpp b/sphaira/source/ui/menus/filebrowser.cpp index ead63b3..aabbebf 100644 --- a/sphaira/source/ui/menus/filebrowser.cpp +++ b/sphaira/source/ui/menus/filebrowser.cpp @@ -86,7 +86,7 @@ constexpr FsEntry FS_ENTRIES[]{ }; constexpr std::string_view AUDIO_EXTENSIONS[] = { - "mp3", "ogg", "flac", "wav", "aac" "ac3", "aif", "asf", "bfwav", + "mp3", "ogg", "flac", "wav", "aac", "ac3", "aif", "asf", "bfwav", "bfsar", "bfstm", "bwav", }; constexpr std::string_view VIDEO_EXTENSIONS[] = { @@ -315,23 +315,23 @@ ForwarderForm::ForwarderForm(const FileAssocEntry& assoc, const RomDatabaseIndex const auto icon = m_assoc.path; m_name = this->Add( - "Name", name, "", "", -1, sizeof(NacpLanguageEntry::name) - 1, + "Name"_i18n, name, "", "", -1, sizeof(NacpLanguageEntry::name) - 1, "Set the name of the application"_i18n ); m_author = this->Add( - "Author", author, "", "", -1, sizeof(NacpLanguageEntry::author) - 1, + "Author"_i18n, author, "", "", -1, sizeof(NacpLanguageEntry::author) - 1, "Set the author of the application"_i18n ); m_version = this->Add( - "Version", version, "", "", -1, sizeof(NacpStruct::display_version) - 1, + "Version"_i18n, version, "", "", -1, sizeof(NacpStruct::display_version) - 1, "Set the display version of the application"_i18n ); const std::vector filters{"nro", "png", "jpg"}; m_icon = this->Add( - "Icon", icon, filters, + "Icon"_i18n, icon, filters, "Set the path to the icon for the forwarder"_i18n ); @@ -362,7 +362,7 @@ ForwarderForm::ForwarderForm(const FileAssocEntry& assoc, const RomDatabaseIndex // try and read icon file into memory, bail if this fails. const auto rc = fs::FsStdio().read_entire_file(m_icon->GetValue(), config.icon); if (R_FAILED(rc)) { - App::PushErrorBox(rc, "Failed to load icon"); + App::PushErrorBox(rc, "Failed to load icon"_i18n); return; } } @@ -721,7 +721,7 @@ void FsView::OnClick() { } else { // special case for nro if (IsSd() && IsSamePath(entry.GetExtension(), "nro")) { - App::Push("Launch "_i18n + entry.GetName() + '?', + App::Push(i18n::Reorder("Launch ", entry.GetName()) + '?', "No"_i18n, "Launch"_i18n, 1, [this](auto op_index){ if (op_index && *op_index) { nro_launch(GetNewPathCurrent()); @@ -872,7 +872,7 @@ void FsView::InstallFiles() { App::Push(0, "Installing "_i18n, "", [this, targets](auto pbox) -> Result { for (auto& e : targets) { R_TRY(yati::InstallFromFile(pbox, m_fs.get(), GetNewPath(e))); - App::Notify("Installed "_i18n + e.GetName()); + App::Notify(i18n::Reorder("Installed ", e.GetName())); } R_SUCCEED(); @@ -1255,7 +1255,7 @@ void FsView::OnDeleteCallback() { const auto full_path = GetNewPath(selected.m_path, p.name); if (p.IsDir()) { - pbox->NewTransfer("Scanning "_i18n + full_path); + pbox->NewTransfer(i18n::Reorder("Scanning ", full_path)); R_TRY(get_collections(src_fs, full_path, p.name, collections)); } } @@ -1298,7 +1298,7 @@ void FsView::OnPasteCallback() { const auto dst_path = GetNewPath(m_path, p.name); pbox->SetTitle(p.name); - pbox->NewTransfer("Pasting "_i18n + src_path); + pbox->NewTransfer(i18n::Reorder("Pasting ", src_path)); if (p.IsDir()) { m_fs->RenameDirectory(src_path, dst_path); @@ -1333,7 +1333,7 @@ void FsView::OnPasteCallback() { const auto full_path = GetNewPath(selected.m_path, p.name); if (p.IsDir()) { - pbox->NewTransfer("Scanning "_i18n + full_path); + pbox->NewTransfer(i18n::Reorder("Scanning ", full_path)); R_TRY(get_collections(src_fs, full_path, p.name, collections)); } } @@ -1347,11 +1347,11 @@ void FsView::OnPasteCallback() { if (p.IsDir()) { pbox->SetTitle(p.name); - pbox->NewTransfer("Creating "_i18n + dst_path); + pbox->NewTransfer(i18n::Reorder("Creating ", dst_path)); m_fs->CreateDirectory(dst_path); } else { pbox->SetTitle(p.name); - pbox->NewTransfer("Copying "_i18n + src_path); + pbox->NewTransfer(i18n::Reorder("Copying ", src_path)); R_TRY(pbox->CopyFile(src_fs, m_fs.get(), src_path, dst_path, is_same_fs)); R_TRY(on_paste_file(src_path, dst_path)); } @@ -1369,7 +1369,7 @@ void FsView::OnPasteCallback() { const auto dst_path = GetNewPath(base_dst_path, p.name); pbox->SetTitle(p.name); - pbox->NewTransfer("Creating "_i18n + dst_path); + pbox->NewTransfer(i18n::Reorder("Creating ", dst_path)); m_fs->CreateDirectory(dst_path); } @@ -1381,7 +1381,7 @@ void FsView::OnPasteCallback() { const auto dst_path = GetNewPath(base_dst_path, p.name); pbox->SetTitle(p.name); - pbox->NewTransfer("Copying "_i18n + src_path); + pbox->NewTransfer(i18n::Reorder("Copying ", src_path)); R_TRY(pbox->CopyFile(src_fs, m_fs.get(), src_path, dst_path, is_same_fs)); R_TRY(on_paste_file(src_path, dst_path)); } @@ -1514,7 +1514,7 @@ Result FsView::DeleteAllCollections(ProgressBox* pbox, fs::Fs* fs, const FsDirCo const auto full_path = FsView::GetNewPath(c.path, p.name); pbox->SetTitle(p.name); - pbox->NewTransfer("Deleting "_i18n + full_path.toString()); + pbox->NewTransfer(i18n::Reorder("Deleting ", full_path.toString())); if ((mode & FsDirOpenMode_ReadDirs) && p.type == FsDirEntryType_Dir) { log_write("deleting dir: %s\n", full_path.s); R_TRY(fs->DeleteDirectory(full_path)); @@ -1545,7 +1545,7 @@ static Result DeleteAllCollectionsWithSelected(ProgressBox* pbox, fs::Fs* fs, co const auto full_path = FsView::GetNewPath(selected.m_path, p.name); pbox->SetTitle(p.name); - pbox->NewTransfer("Deleting "_i18n + full_path.toString()); + pbox->NewTransfer(i18n::Reorder("Deleting ", full_path.toString())); if ((mode & FsDirOpenMode_ReadDirs) && p.type == FsDirEntryType_Dir) { log_write("deleting dir: %s\n", full_path.s); @@ -1805,7 +1805,7 @@ void FsView::DisplayOptions() { options->Add("Extract to..."_i18n, [this](){ std::string out; - if (R_SUCCEEDED(swkbd::ShowText(out, "Extract path", "Enter the path to the folder to extract into", fs::AppendPath(m_path, ""))) && !out.empty()) { + if (R_SUCCEEDED(swkbd::ShowText(out, "Extract path", "Enter the path to the folder to extract into"_i18n.c_str, fs::AppendPath(m_path, ""))) && !out.empty()) { UnzipFiles(out); } }); @@ -1823,7 +1823,7 @@ void FsView::DisplayOptions() { options->Add("Compress to..."_i18n, [this](){ std::string out; - if (R_SUCCEEDED(swkbd::ShowText(out, "Compress path", "Enter the path to the folder to compress into", m_path)) && !out.empty()) { + if (R_SUCCEEDED(swkbd::ShowText(out, "Compress path", "Enter the path to the folder to compress into"_i18n.c_str, m_path)) && !out.empty()) { ZipFiles(out); } }); diff --git a/sphaira/source/ui/menus/game_menu.cpp b/sphaira/source/ui/menus/game_menu.cpp index 0a013da..d5a43e9 100644 --- a/sphaira/source/ui/menus/game_menu.cpp +++ b/sphaira/source/ui/menus/game_menu.cpp @@ -201,7 +201,7 @@ void FreeEntry(NVGcontext* vg, Entry& e) { void LaunchEntry(const Entry& e) { const auto rc = appletRequestLaunchApplication(e.app_id, nullptr); - Notify(rc, "Failed to launch application"); + Notify(rc, "Failed to launch application"_i18n); } Result CreateSave(u64 app_id, AccountUid uid) { @@ -372,7 +372,7 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} { LoadControlEntry(e, true); App::Push( - "Launch "_i18n + e.GetName(), + i18n::Reorder("Launch ", e.GetName()) + '?', "Back"_i18n, "Launch"_i18n, 1, [this, &e](auto op_index){ if (op_index && *op_index) { LaunchEntry(e); @@ -397,7 +397,7 @@ Menu::Menu(u32 flags) : grid::Menu{"Games"_i18n, flags} { // completely deletes the application record and all data. options->Add("Delete"_i18n, [this](){ - const auto buf = "Are you sure you want to delete "_i18n + m_entries[m_index].GetName() + "?"; + const auto buf = i18n::Reorder("Are you sure you want to delete ", m_entries[m_index].GetName()) + "?"; App::Push( buf, "Back"_i18n, "Delete"_i18n, 0, [this](auto op_index){ @@ -839,7 +839,7 @@ void DeleteMetaEntries(u64 app_id, int image, const std::string& name, const tit R_SUCCEED(); }, [](Result rc){ - App::PushErrorBox(rc, "Failed to delete meta entry"); + App::PushErrorBox(rc, "Failed to delete meta entry"_i18n); }); } diff --git a/sphaira/source/ui/menus/game_meta_menu.cpp b/sphaira/source/ui/menus/game_meta_menu.cpp index fa7b1be..a284c3a 100644 --- a/sphaira/source/ui/menus/game_meta_menu.cpp +++ b/sphaira/source/ui/menus/game_meta_menu.cpp @@ -181,23 +181,23 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { char req_vers_buf[128]; const auto ver = e.content_meta.extened.application.required_system_version; switch (e.status.meta_type) { - case NcmContentMetaType_Application: std::snprintf(req_vers_buf, sizeof(req_vers_buf), "Required System Version: %u.%u.%u", SYSVER_MAJOR(ver), SYSVER_MINOR(ver), SYSVER_MICRO(ver)); break; - case NcmContentMetaType_Patch: std::snprintf(req_vers_buf, sizeof(req_vers_buf), "Required System Version: %u.%u.%u", SYSVER_MAJOR(ver), SYSVER_MINOR(ver), SYSVER_MICRO(ver)); break; - case NcmContentMetaType_AddOnContent: std::snprintf(req_vers_buf, sizeof(req_vers_buf), "Required Application Version: v%u", ver >> 16); break; + case NcmContentMetaType_Application: std::snprintf(req_vers_buf, sizeof(req_vers_buf), "Required System Version: %u.%u.%u"_i18n.c_str(), SYSVER_MAJOR(ver), SYSVER_MINOR(ver), SYSVER_MICRO(ver)); break; + case NcmContentMetaType_Patch: std::snprintf(req_vers_buf, sizeof(req_vers_buf), "Required System Version: %u.%u.%u"_i18n.c_str(), SYSVER_MAJOR(ver), SYSVER_MINOR(ver), SYSVER_MICRO(ver)); break; + case NcmContentMetaType_AddOnContent: std::snprintf(req_vers_buf, sizeof(req_vers_buf), "Required Application Version: v%u"_i18n.c_str(), ver >> 16); break; } if (e.missing_count) { - gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Content Count: %u (%u missing)", e.content_meta.header.content_count, e.missing_count); + gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Content Count: %u (%u missing)"_i18n.c_str(), e.content_meta.header.content_count, e.missing_count); } else { - gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Content Count: %u", e.content_meta.header.content_count); + gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Content Count: %u"_i18n.c_str(), e.content_meta.header.content_count); } - gfx::drawTextArgs(vg, 50, 455, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Ticket: %s", TICKET_STR[e.ticket_type]); - gfx::drawTextArgs(vg, 50, 495, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Key Generation: %u (%s)", e.key_gen, nca::GetKeyGenStr(e.key_gen)); + gfx::drawTextArgs(vg, 50, 455, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Ticket: %s"_i18n.c_str(), i18n::get(TICKET_STR[e.ticket_type]).c_str()); + gfx::drawTextArgs(vg, 50, 495, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Key Generation: %u (%s)"_i18n.c_str(), e.key_gen, nca::GetKeyGenStr(e.key_gen)); gfx::drawTextArgs(vg, 50, 535, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", req_vers_buf); if (e.status.meta_type == NcmContentMetaType_Application || e.status.meta_type == NcmContentMetaType_Patch) { - gfx::drawTextArgs(vg, 50, 575, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Display Version: %s", e.nacp.display_version); + gfx::drawTextArgs(vg, 50, 575, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Display Version: %s"_i18n.c_str(), e.nacp.display_version); } nvgRestore(vg); @@ -224,7 +224,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { } } - gfx::drawTextArgs(vg, x + text_xoffset, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%s", ncm::GetReadableMetaTypeStr(e.status.meta_type)); + gfx::drawTextArgs(vg, x + text_xoffset, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%s", i18n::get(ncm::GetReadableMetaTypeStr(e.status.meta_type)).c_str()); gfx::drawTextArgs(vg, x + text_xoffset + 150, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "%016lX", e.status.application_id); gfx::drawTextArgs(vg, x + text_xoffset + 400, y + (h / 2.f), 20.f, NVG_ALIGN_LEFT | NVG_ALIGN_MIDDLE, theme->GetColour(text_id), "v%u (%u)", e.status.version >> 16, e.status.version); @@ -233,7 +233,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { GetNcmSizeOfMetaStatus(e); } - gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_INFO), "%s", ncm::GetReadableStorageIdStr(e.status.storageID)); + gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) + 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT_INFO), "%s", i18n::get(ncm::GetReadableStorageIdStr(e.status.storageID)).c_str()); gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f) - 3, 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_BOTTOM, theme->GetColour(ThemeEntryID_TEXT_INFO), "%s", utils::formatSizeStorage(e.size).c_str()); if (e.selected) { diff --git a/sphaira/source/ui/menus/game_nca_menu.cpp b/sphaira/source/ui/menus/game_nca_menu.cpp index c93439d..7de3d35 100644 --- a/sphaira/source/ui/menus/game_nca_menu.cpp +++ b/sphaira/source/ui/menus/game_nca_menu.cpp @@ -205,9 +205,10 @@ Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry) } } }); - }, "Performs sha256 hash over the NCA to check if it's valid.\n\n" - "NOTE: This only detects if the hash is missmatched, it does not validate if \ - the content has been modified at all."_i18n); + }, i18n::get("nca_validate_info", + "Performs sha256 hash over the NCA to check if it's valid.\n\n" + "NOTE: This only detects if the hash is missmatched, it does not validate if " + "the content has been modified at all.")); options->Add("Verify NCA fixed key"_i18n, [this](){ if (R_FAILED(nca::VerifyFixedKey(GetEntry().header))) { @@ -215,9 +216,10 @@ Menu::Menu(Entry& entry, const meta::MetaEntry& meta_entry) } else { App::Push("NCA fixed key is valid."_i18n, "OK"_i18n); } - }, "Performs RSA NCA fixed key verification. "\ - "This is a hash over the NCA header. It is used to verify that the header has not been modified. "\ - "The header is signed by nintendo, thus it cannot be forged, and is reliable to detect modified NCA headers (such as NSP/XCI converts)."_i18n); + }, i18n::get("nca_fixedkey_info", + "Performs RSA NCA fixed key verification. " + "This is a hash over the NCA header. It is used to verify that the header has not been modified. " + "The header is signed by nintendo, thus it cannot be forged, and is reliable to detect modified NCA headers (such as NSP/XCI converts).")); } }}) ); @@ -307,16 +309,16 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { gfx::drawImage(vg, 90, 130, 256, 256, m_entry.image ? m_entry.image : App::GetDefaultImage()); if (e.header.magic != NCA3_MAGIC) { - gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Failed to decrypt NCA"); + gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Failed to decrypt NCA"_i18n.c_str()); } else { nvgSave(vg); nvgIntersectScissor(vg, 50, 90, 325, 555); - gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Application Type: %s", ncm::GetReadableMetaTypeStr(m_meta_entry.status.meta_type)); - gfx::drawTextArgs(vg, 50, 455, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Content Type: %s", nca::GetContentTypeStr(e.header.content_type)); - gfx::drawTextArgs(vg, 50, 495, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Distribution Type: %s", nca::GetDistributionTypeStr(e.header.distribution_type)); - gfx::drawTextArgs(vg, 50, 535, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Program ID: %016lX", e.header.program_id); - gfx::drawTextArgs(vg, 50, 575, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Key Generation: %u (%s)", e.header.GetKeyGeneration(), nca::GetKeyGenStr(e.header.GetKeyGeneration())); - gfx::drawTextArgs(vg, 50, 615, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "SDK Version: %u.%u.%u.%u", e.header.sdk_major, e.header.sdk_minor, e.header.sdk_micro, e.header.sdk_revision); + gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Application Type: %s"_i18n.c_str(), i18n::get(ncm::GetReadableMetaTypeStr(m_meta_entry.status.meta_type)).c_str()); + gfx::drawTextArgs(vg, 50, 455, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Content Type: %s"_i18n.c_str(), nca::GetContentTypeStr(e.header.content_type)); + gfx::drawTextArgs(vg, 50, 495, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Distribution Type: %s"_i18n.c_str(), nca::GetDistributionTypeStr(e.header.distribution_type)); + gfx::drawTextArgs(vg, 50, 535, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Program ID: %016lX"_i18n.c_str(), e.header.program_id); + gfx::drawTextArgs(vg, 50, 575, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Key Generation: %u (%s)"_i18n.c_str(), e.header.GetKeyGeneration(), nca::GetKeyGenStr(e.header.GetKeyGeneration())); + gfx::drawTextArgs(vg, 50, 615, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "SDK Version: %u.%u.%u.%u"_i18n.c_str(), e.header.sdk_major, e.header.sdk_minor, e.header.sdk_micro, e.header.sdk_revision); nvgRestore(vg); } diff --git a/sphaira/source/ui/menus/gc_menu.cpp b/sphaira/source/ui/menus/gc_menu.cpp index c10554a..4011d9b 100644 --- a/sphaira/source/ui/menus/gc_menu.cpp +++ b/sphaira/source/ui/menus/gc_menu.cpp @@ -718,10 +718,10 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { nvgIntersectScissor(vg, 50, 90, 325, 555); gfx::drawTextArgs(vg, 50, 415, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", e.lang_entry.name); gfx::drawTextArgs(vg, 50, 455, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "%s", e.lang_entry.author); - gfx::drawTextArgs(vg, 50, 495, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "App-ID: 0%lX", e.app_id); - gfx::drawTextArgs(vg, 50, 535, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Key-Gen: %u (%s)", e.key_gen, nca::GetKeyGenStr(e.key_gen)); - gfx::drawTextArgs(vg, 50, 575, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Size: %.2f GB", (double)size / 0x40000000); - gfx::drawTextArgs(vg, 50, 615, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Base: %zu Patch: %zu Addon: %zu Data: %zu", e.application.size(), e.patch.size(), e.add_on.size(), e.data_patch.size()); + gfx::drawTextArgs(vg, 50, 495, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "App-ID: 0%lX"_i18n.c_str(), e.app_id); + gfx::drawTextArgs(vg, 50, 535, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Key-Gen: %u (%s)"_i18n.c_str(), e.key_gen, nca::GetKeyGenStr(e.key_gen)); + gfx::drawTextArgs(vg, 50, 575, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Size: %.2f GB"_i18n.c_str(), (double)size / 0x40000000); + gfx::drawTextArgs(vg, 50, 615, 18.f, NVG_ALIGN_LEFT | NVG_ALIGN_TOP, theme->GetColour(ThemeEntryID_TEXT), "Base: %zu Patch: %zu Addon: %zu Data: %zu"_i18n.c_str(), e.application.size(), e.patch.size(), e.add_on.size(), e.data_patch.size()); nvgRestore(vg); } @@ -1098,7 +1098,7 @@ void Menu::OnChangeIndex(s64 new_index) { m_entry_index = new_index; if (m_entries.empty()) { - this->SetSubHeading("No GameCard inserted"); + this->SetSubHeading("No GameCard inserted"_i18n); } else { const auto index = m_entries.empty() ? 0 : m_entry_index + 1; this->SetSubHeading(std::to_string(index) + " / " + std::to_string(m_entries.size())); diff --git a/sphaira/source/ui/menus/ghdl.cpp b/sphaira/source/ui/menus/ghdl.cpp index 7755696..9f3754a 100644 --- a/sphaira/source/ui/menus/ghdl.cpp +++ b/sphaira/source/ui/menus/ghdl.cpp @@ -108,7 +108,7 @@ auto DownloadApp(ProgressBox* pbox, const GhApiAsset& gh_asset, const AssetEntry // 2. download the asset if (!pbox->ShouldExit()) { - pbox->NewTransfer("Downloading "_i18n + gh_asset.name); + pbox->NewTransfer(i18n::Reorder("Downloading ", gh_asset.name)); log_write("starting download: %s\n", gh_asset.browser_download_url.c_str()); const auto result = curl::Api().ToFile( @@ -233,7 +233,7 @@ void Menu::Draw(NVGcontext* vg, Theme* theme) { nvgRestore(vg); if (!e.tag.empty()) { - gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f), 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "version: %s", e.tag.c_str()); + gfx::drawTextArgs(vg, x + w - text_xoffset, y + (h / 2.f), 16.f, NVG_ALIGN_RIGHT | NVG_ALIGN_MIDDLE, theme->GetColour(ThemeEntryID_TEXT_INFO), "version: %s"_i18n.c_str(), e.tag.c_str()); } }); } @@ -427,7 +427,7 @@ void DownloadEntries(const Entry& entry) { App::PushErrorBox(rc, "Failed to download app!"_i18n); if (R_SUCCEEDED(rc)) { - App::Notify("Downloaded "_i18n + entry.repo); + App::Notify(i18n::Reorder("Downloaded ", entry.repo)); auto post_install_message = entry.post_install_message; if (ptr && !ptr->post_install_message.empty()) { post_install_message = ptr->post_install_message; diff --git a/sphaira/source/ui/menus/homebrew.cpp b/sphaira/source/ui/menus/homebrew.cpp index eef48c3..6ae61dd 100644 --- a/sphaira/source/ui/menus/homebrew.cpp +++ b/sphaira/source/ui/menus/homebrew.cpp @@ -168,13 +168,13 @@ void Menu::SetIndex(s64 index) { if (fs::FsNativeSd().FileExists(star_path)) { SetAction(Button::R3, Action{"Unstar"_i18n, [this](){ fs::FsNativeSd().DeleteFile(GenerateStarPath(GetEntry().path)); - App::Notify("Unstarred "_i18n + GetEntry().GetName()); + App::Notify(i18n::Reorder("Unstarred ", GetEntry().GetName())); SortAndFindLastFile(); }}); } else { SetAction(Button::R3, Action{"Star"_i18n, [this](){ fs::FsNativeSd().CreateFile(GenerateStarPath(GetEntry().path)); - App::Notify("Starred "_i18n + GetEntry().GetName()); + App::Notify(i18n::Reorder("Starred ", GetEntry().GetName())); SortAndFindLastFile(); }}); } @@ -469,7 +469,7 @@ void Menu::DisplayOptions() { // for testing stuff. #if 0 - options->Add("Test", 1, 0, 2, 10, [](auto& v_out){ + options->Add("Test"_i18n, 1, 0, 2, 10, [](auto& v_out){ }); #endif @@ -494,7 +494,7 @@ void Menu::DisplayOptions() { }, "Mounts the NRO FileSystem (icon, nacp and RomFS)."_i18n); options->Add("Delete"_i18n, [this](){ - const auto buf = "Are you sure you want to delete "_i18n + GetEntry().path.toString() + "?"; + const auto buf = i18n::Reorder("Are you sure you want to delete ", GetEntry().path.toString()) + "?"; App::Push( buf, "Back"_i18n, "Delete"_i18n, 1, [this](auto op_index){ @@ -510,9 +510,10 @@ void Menu::DisplayOptions() { } }, GetEntry().image ); - }, "Perminately delete the selected homebrew.\n\n" - "Files and folders created by the homebrew will still remain. " - "Use the FileBrowser to delete them."_i18n); + }, i18n::get("hb_remove_info", + "Perminately delete the selected homebrew.\n\n" + "Files and folders created by the homebrew will still remain. " + "Use the FileBrowser to delete them.")); auto forwarder_entry = options->Add("Install Forwarder"_i18n, [this](){ InstallHomebrew(); diff --git a/sphaira/source/ui/menus/irs_menu.cpp b/sphaira/source/ui/menus/irs_menu.cpp index 4b9c153..32573bb 100644 --- a/sphaira/source/ui/menus/irs_menu.cpp +++ b/sphaira/source/ui/menus/irs_menu.cpp @@ -188,7 +188,7 @@ Menu::Menu(u32 flags) : MenuBase{"Irs"_i18n, flags} { return; } - static_assert(IRS_MAX_CAMERAS >= 9, "max camaeras has gotten smaller!"); + static_assert(IRS_MAX_CAMERAS >= 9, "max cameras has gotten smaller!"); // open all handles irsGetIrCameraHandle(&m_entries[0].m_handle, HidNpadIdType_No1); @@ -534,7 +534,7 @@ void Menu::updateColourArray() { auto Menu::GetEntryName(s64 i) -> std::string { const auto& e = m_entries[i]; - std::string text = "Pad "_i18n + (i == 8 ? "HandHeld"_i18n : std::to_string(i)); + std::string text = i18n::Reorder("Pad "_i18n, (i == 8 ? "HandHeld" : std::to_string(i))); switch (e.status) { case IrsIrCameraStatus_Available: text += " (Available)"_i18n; diff --git a/sphaira/source/ui/menus/main_menu.cpp b/sphaira/source/ui/menus/main_menu.cpp index 1911298..17200fc 100644 --- a/sphaira/source/ui/menus/main_menu.cpp +++ b/sphaira/source/ui/menus/main_menu.cpp @@ -114,7 +114,7 @@ auto InstallUpdate(ProgressBox* pbox, const std::string url, const std::string v // 1. download the zip if (!pbox->ShouldExit()) { - pbox->NewTransfer("Downloading "_i18n + version); + pbox->NewTransfer(i18n::Reorder("Downloading ", version)); log_write("starting download: %s\n", url.c_str()); const auto result = curl::Api().ToFile( @@ -317,11 +317,12 @@ MainMenu::MainMenu() { language_items.push_back("German"_i18n); language_items.push_back("Italian"_i18n); language_items.push_back("Spanish"_i18n); - language_items.push_back("Chinese"_i18n); + language_items.push_back("Chinese (Simplified)"_i18n); language_items.push_back("Korean"_i18n); language_items.push_back("Dutch"_i18n); language_items.push_back("Portuguese"_i18n); language_items.push_back("Russian"_i18n); + language_items.push_back("Chinese (Traditional)"_i18n); language_items.push_back("Swedish"_i18n); language_items.push_back("Vietnamese"_i18n); language_items.push_back("Ukrainian"_i18n); @@ -356,7 +357,7 @@ MainMenu::MainMenu() { if (R_SUCCEEDED(rc)) { m_update_state = UpdateState::None; - App::Notify("Updated to "_i18n + m_update_version); + App::Notify(i18n::Reorder("Updated to ", m_update_version)); App::Push( "Press OK to restart Sphaira"_i18n, "OK"_i18n, [](auto){ App::ExitRestart(); @@ -368,13 +369,15 @@ MainMenu::MainMenu() { } options->Add("FTP"_i18n, [](){ App::DisplayFtpOptions(); }, - "Enable / modify the FTP server settings such as port, user/pass and the folders that are shown.\n\n" - "NOTE: Changing any of the options will automatically restart the FTP server when exiting the options menu."_i18n + i18n::get("ftp_settings_info", + "Enable / modify the FTP server settings such as port, user/pass and the folders that are shown.\n\n" + "NOTE: Changing any of the options will automatically restart the FTP server when exiting the options menu.") ); options->Add("MTP"_i18n, [](){ App::DisplayMtpOptions(); }, - "Enable / modify the MTP responder settings such as the folders that are shown.\n\n" - "NOTE: Changing any of the options will automatically restart the MTP server when exiting the options menu."_i18n + i18n::get("mtp_settings_info", + "Enable / modify the MTP responder settings such as the folders that are shown.\n\n" + "NOTE: Changing any of the options will automatically restart the MTP server when exiting the options menu.") ); options->Add("HDD"_i18n, [](){ @@ -383,12 +386,14 @@ MainMenu::MainMenu() { options->Add("NXlink"_i18n, App::GetNxlinkEnable(), [](bool& enable){ App::SetNxlinkEnable(enable); - }, "Enable NXlink server to run in the background. " - "NXlink is used to send .nro's from PC to the switch\n\n" - "If you are not a developer, you can disable this option."_i18n); + }, i18n::get("nxlink_enable_info", + "Enable NXlink server to run in the background. " + "NXlink is used to send .nro's from PC to the switch\n\n" + "If you are not a developer, you can disable this option.")); - }, "Toggle FTP, MTP, HDD and NXlink\n\n" - "If Sphaira has a update available, you can download it from this menu"_i18n); + }, i18n::get("nxlink_toggle_info", + "Toggle FTP, MTP, HDD and NXlink\n\n" + "If Sphaira has a update available, you can download it from this menu")); options->Add("Theme"_i18n, [](){ App::DisplayThemeOptions(); @@ -397,14 +402,16 @@ MainMenu::MainMenu() { options->Add("Language"_i18n, language_items, [](s64& index_out){ App::SetLanguage(index_out); }, (s64)App::GetLanguage(), - "Change the language.\n\n" - "If your language isn't found, or translations are missing, please consider opening a PR at " - "github.com/ITotalJustice/sphaira"_i18n); + i18n::get("translation_info", + "Change the language.\n\n" + "If your language isn't found, or translations are missing, please consider opening a PR at " + "github.com/ITotalJustice/sphaira")); options->Add("Advanced Options"_i18n, [](){ App::DisplayAdvancedOptions(); - }, "Change the advanced options. " - "Please view the info boxes to better understand each option."_i18n); + }, i18n::get("advanced_options_info", + "Change the advanced options. " + "Please view the info boxes to better understand each option.")); }} )); diff --git a/sphaira/source/ui/menus/mtp_menu.cpp b/sphaira/source/ui/menus/mtp_menu.cpp index 71537e0..6aadd4d 100644 --- a/sphaira/source/ui/menus/mtp_menu.cpp +++ b/sphaira/source/ui/menus/mtp_menu.cpp @@ -47,7 +47,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) { usbDsGetSpeed(&speed); char buf[128]; - std::snprintf(buf, sizeof(buf), "State: %s | Speed: %s", i18n::get(GetUsbDsStateStr(state)).c_str(), i18n::get(GetUsbDsSpeedStr(speed)).c_str()); + std::snprintf(buf, sizeof(buf), "State: %s | Speed: %s"_i18n.c_str(), i18n::get(GetUsbDsStateStr(state)).c_str(), i18n::get(GetUsbDsSpeedStr(speed)).c_str()); SetSubHeading(buf); } } diff --git a/sphaira/source/ui/menus/save_menu.cpp b/sphaira/source/ui/menus/save_menu.cpp index 24ab8d2..753659c 100644 --- a/sphaira/source/ui/menus/save_menu.cpp +++ b/sphaira/source/ui/menus/save_menu.cpp @@ -404,7 +404,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) { } if (m_dirty) { - App::Notify("Updating application record list"); + App::Notify("Updating application record list"_i18n); SortAndFindLastFile(true); } @@ -718,9 +718,11 @@ void Menu::DisplayOptions() { options->Add("Restore"_i18n, [this](){ RestoreSave(); }, true, - "Restore the save for the current title.\n" - "if \"Auto backup\" is enabled, the save will first be backed up and then restored. " - "Saves that are auto backed up will have \"Auto\" in their name."_i18n + i18n::get("save_backuprestore_info", + "Restore the save for the current title.\n" + "if \"Auto backup\" is enabled, the save will first be backed up and then restored. " + "Saves that are auto backed up will have \"Auto\" in their name." + ) ); } } @@ -735,10 +737,13 @@ void Menu::DisplayOptions() { options->Add("Compress backup"_i18n, m_compress_save_backup.Get(), [this](bool& v_out){ m_compress_save_backup.Set(v_out); - }, "If enabled, backups will be compressed to a zip file.\n\n" - "NOTE: Disabling this option does not disable the zip file, it only disables compressing " - "the files stored in the zip.\n" - "Disabling will result in a much faster backup, at the cost of the file size."_i18n); + }, i18n::get("save_backup_compress_info", + "If enabled, backups will be compressed to a zip file.\n\n" + "NOTE: Disabling this option does not disable the zip file, it only disables compressing " + "the files stored in the zip.\n" + "Disabling will result in a much faster backup, at the cost of the file size." + ) + ); }); } @@ -797,7 +802,7 @@ void Menu::RestoreSave() { if (paths.empty()) { App::Push( - "No saves found in "_i18n + fs::AppendPath(mount, BuildSaveBasePath(m_entries[m_index])).toString(), + i18n::Reorder("No saves found in ", fs::AppendPath(mount, BuildSaveBasePath(m_entries[m_index])).toString()), "OK"_i18n ); return; @@ -814,7 +819,7 @@ void Menu::RestoreSave() { const auto file_path = paths[*op_index]; App::Push( - "Are you sure you want to restore "_i18n + file_name + "?", + i18n::Reorder("Are you sure you want to restore ", file_name) + "?", "Back"_i18n, "Restore"_i18n, 0, [this, file_path, location](auto op_index){ if (op_index && *op_index) { App::Push(0, "Restore"_i18n, "", [this, file_path, location](auto pbox) -> Result { @@ -871,7 +876,7 @@ auto Menu::BuildSavePath(const Entry& e, u32 flags) const -> fs::FsPath { if (flags & BackupFlag_SetName) { std::string out; while (out.empty()) { - const auto header = "Set name for "_i18n + e.GetName(); + const auto header = i18n::Reorder("Set name for ", e.GetName()); if (R_FAILED(swkbd::ShowText(out, header.c_str(), "Set backup name", name, 1, 128))) { out.clear(); } @@ -1138,7 +1143,7 @@ Result Menu::BackupSaveInternal(ProgressBox* pbox, const dump::DumpLocation& loc // if we dumped the save to ram, flush the data to file. if (!file_download) { - pbox->NewTransfer("Flushing zip to file"); + pbox->NewTransfer("Flushing zip to file"_i18n); R_TRY(writer->SetSize(mz_mem.buf.size())); R_TRY(thread::Transfer(pbox, mz_mem.buf.size(), diff --git a/sphaira/source/ui/menus/themezer.cpp b/sphaira/source/ui/menus/themezer.cpp index f33ff2c..833ba11 100644 --- a/sphaira/source/ui/menus/themezer.cpp +++ b/sphaira/source/ui/menus/themezer.cpp @@ -252,7 +252,7 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> Result { // 1. download the zip if (!pbox->ShouldExit()) { - pbox->NewTransfer("Downloading "_i18n + entry.details.name); + pbox->NewTransfer(i18n::Reorder("Downloading ", entry.details.name)); log_write("starting download\n"); const auto url = apiBuildUrlDownloadPack(entry); @@ -272,7 +272,7 @@ auto InstallTheme(ProgressBox* pbox, const PackListEntry& entry) -> Result { // 2. download the zip if (!pbox->ShouldExit()) { - pbox->NewTransfer("Downloading "_i18n + entry.details.name); + pbox->NewTransfer(i18n::Reorder("Downloading ", entry.details.name)); log_write("starting download: %s\n", download_pack.url.c_str()); const auto result = curl::Api().ToFile( @@ -383,13 +383,13 @@ Menu::Menu(u32 flags) : MenuBase{"Themezer"_i18n, flags} { const auto& entry = page.m_packList[m_index]; const auto url = apiBuildUrlDownloadPack(entry); - App::Push(entry.themes[0].preview.lazy_image.image, "Downloading "_i18n, entry.details.name, [this, &entry](auto pbox) -> Result { + App::Push(entry.themes[0].preview.lazy_image.image, i18n::Reorder("Downloading ", entry.details.name), [this, &entry](auto pbox) -> Result { return InstallTheme(pbox, entry); }, [this, &entry](Result rc){ App::PushErrorBox(rc, "Failed to download theme"_i18n); if (R_SUCCEEDED(rc)) { - App::Notify("Downloaded "_i18n + entry.details.name); + App::Notify(i18n::Reorder("Downloaded ", entry.details.name)); } }); } diff --git a/sphaira/source/ui/menus/usb_menu.cpp b/sphaira/source/ui/menus/usb_menu.cpp index af885bb..8f9f9b8 100644 --- a/sphaira/source/ui/menus/usb_menu.cpp +++ b/sphaira/source/ui/menus/usb_menu.cpp @@ -87,7 +87,7 @@ void Menu::Update(Controller* controller, TouchInfo* touch) { usbDsGetSpeed(&speed); char buf[128]; - std::snprintf(buf, sizeof(buf), "State: %s | Speed: %s", i18n::get(GetUsbDsStateStr(state)).c_str(), i18n::get(GetUsbDsSpeedStr(speed)).c_str()); + std::snprintf(buf, sizeof(buf), "State: %s | Speed: %s"_i18n.c_str(), i18n::get(GetUsbDsStateStr(state)).c_str(), i18n::get(GetUsbDsSpeedStr(speed)).c_str()); SetSubHeading(buf); } diff --git a/sphaira/source/ui/music_player.cpp b/sphaira/source/ui/music_player.cpp index ce9f252..0ded3f9 100644 --- a/sphaira/source/ui/music_player.cpp +++ b/sphaira/source/ui/music_player.cpp @@ -90,11 +90,11 @@ Menu::Menu(fs::Fs* fs, const fs::FsPath& path) { } if (m_meta.artist.empty()) { - m_meta.artist = "Artist: Unknown"; + m_meta.artist = "Artist: Unknown"_i18n; } if (m_meta.album.empty()) { - m_meta.album = "Album: Unknown"; + m_meta.album = "Album: Unknown"_i18n; } } diff --git a/sphaira/source/utils/devoptab.cpp b/sphaira/source/utils/devoptab.cpp index 3d81d6f..54d87c2 100644 --- a/sphaira/source/utils/devoptab.cpp +++ b/sphaira/source/utils/devoptab.cpp @@ -121,13 +121,13 @@ private: }; DevoptabForm::DevoptabForm(DevoptabType type, const MountConfig& config) -: FormSidebar{"Mount Creator"} +: FormSidebar{"Mount Creator"_i18n} , m_type{type} , m_config{config} { SetupButtons(false); } -DevoptabForm::DevoptabForm() : FormSidebar{"Mount Creator"} { +DevoptabForm::DevoptabForm() : FormSidebar{"Mount Creator"_i18n} { SetupButtons(true); } @@ -157,7 +157,7 @@ void DevoptabForm::SetupButtons(bool type_change) { } this->Add( - "Type", items, [this](s64& index) { + "Type"_i18n, items, [this](s64& index) { m_type = TYPE_ENTRIES[index].type; UpdateSchemeURL(); }, @@ -167,67 +167,70 @@ void DevoptabForm::SetupButtons(bool type_change) { } m_name = this->Add( - "Name", m_config.name, "", "", -1, 32, + "Name"_i18n, m_config.name, "", "", -1, 32, "Set the name of the application"_i18n ); m_url = this->Add( - "URL", m_config.url, "", "", -1, PATH_MAX, + "URL"_i18n, m_config.url, "", "", -1, PATH_MAX, "Set the URL of the application"_i18n ); m_port = this->Add( - "Port", m_config.port, "", "", 1, 5, + "Port"_i18n, m_config.port, "", "", 1, 5, "Optional: Set the port of the server. If left empty, the default port for the protocol will be used."_i18n ); #if 0 m_timeout = this->Add( - "Timeout", m_config.timeout, "Timeout in milliseconds", 1, 5, + "Timeout"_i18n, m_config.timeout, "Timeout in milliseconds", 1, 5, "Optional: Set the timeout in seconds."_i18n ); #endif m_user = this->Add( - "User", m_config.user, "", "", -1, PATH_MAX, + "User"_i18n, m_config.user, "", "", -1, PATH_MAX, "Optional: Set the username of the application"_i18n ); m_pass = this->Add( - "Pass", m_config.pass, "", "", -1, PATH_MAX, + "Pass"_i18n, m_config.pass, "", "", -1, PATH_MAX, "Optional: Set the password of the application"_i18n ); m_dump_path = this->Add( - "Dump path", m_config.dump_path, "", "", -1, PATH_MAX, + "Dump path"_i18n, m_config.dump_path, "", "", -1, PATH_MAX, "Optional: Set the dump path used when exporting games and saves."_i18n ); this->Add( - "Read only", m_config.read_only, - "Mount the filesystem as read only.\n\n" - "Setting this option also hidens the mount from being show as an export option."_i18n + "Read only"_i18n, m_config.read_only, + i18n::get("mount_readonly_info", + "Mount the filesystem as read only.\n\n" + "Setting this option also hidens the mount from being show as an export option.") ); this->Add( - "No stat file", m_config.no_stat_file, - "Enabling stops the file browser from checking the file size and timestamp of each file. " - "This improves browsing performance."_i18n + "No stat file"_i18n, m_config.no_stat_file, + i18n::get("filecheck_disable_info", + "Enabling stops the file browser from checking the file size and timestamp of each file. " + "This improves browsing performance.") ); this->Add( - "No stat dir", m_config.no_stat_dir, - "Enabling stops the file browser from checking how many files and folders are in a folder. " - "This improves browsing performance, especially for servers that has slow directory listing."_i18n + "No stat dir"_i18n, m_config.no_stat_dir, + i18n::get("dircheck_disable_info", + "Enabling stops the file browser from checking how many files and folders are in a folder. " + "This improves browsing performance, especially for servers that has slow directory listing.") ); this->Add( - "FS hidden", m_config.fs_hidden, + "FS hidden"_i18n, m_config.fs_hidden, "Hide the mount from being visible in the file browser."_i18n ); this->Add( - "Export hidden", m_config.dump_hidden, + "Export hidden"_i18n, m_config.dump_hidden, "Hide the mount from being visible as a export option for games and saves."_i18n ); @@ -236,7 +239,7 @@ void DevoptabForm::SetupButtons(bool type_change) { UpdateSchemeURL(); } - const auto callback = this->Add("Save", [this](){ + const auto callback = this->Add("Save"_i18n, [this](){ m_config.name = m_name->GetValue(); m_config.url = m_url->GetValue(); m_config.user = m_user->GetValue(); diff --git a/sphaira/source/utils/nsz_dumper.cpp b/sphaira/source/utils/nsz_dumper.cpp index 57535d6..03b4812 100644 --- a/sphaira/source/utils/nsz_dumper.cpp +++ b/sphaira/source/utils/nsz_dumper.cpp @@ -226,7 +226,7 @@ Result NszExport(ui::ProgressBox* pbox, const NcaReaderCreator& nca_creator, s64 const auto section_number = std::distance(ncz_sections.begin(), section); const auto rsize = section->size - (nca_off - section->offset); - pbox->NewTransfer("Section #" + std::to_string(section_number) + " - " + collection.name); + pbox->NewTransfer("Section #"_i18n + std::to_string(section_number) + " - " + collection.name); ZSTD_CCtx_reset(cctx, ZSTD_reset_session_only); R_TRY(thread::Transfer(pbox, rsize,