From 30b912fea4929a60a582ac9633b9146d352f7302 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 04:23:33 +0000 Subject: [PATCH 01/39] feat(libSceSystemService): parse BCP-47 locale tags ANYPS5_LANGUAGE can only name a console language number. Taking the language from the host needs the host's locale, which arrives as a BCP-47 tag such as en-US, pt-BR or zh-Hant-TW. ParseTag reads the language, script and region subtags, normalises their case, ignores variants and extensions, and rejects empty subtags, subtags over 8 characters and languages that are not 2 or 3 letters. --- .../libSceSystemService/LanguageLocale.hpp | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 core/libs/prx/libSceSystemService/LanguageLocale.hpp diff --git a/core/libs/prx/libSceSystemService/LanguageLocale.hpp b/core/libs/prx/libSceSystemService/LanguageLocale.hpp new file mode 100644 index 0000000000..94c70ff50e --- /dev/null +++ b/core/libs/prx/libSceSystemService/LanguageLocale.hpp @@ -0,0 +1,73 @@ +#ifndef CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_LANGUAGELOCALE_HPP +#define CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_LANGUAGELOCALE_HPP + +#include +#include +#include +#include + +namespace SystemServiceLocale { + +struct Tag { + std::string language; + std::string script; + std::string region; +}; + +namespace Detail { + +constexpr bool IsAlpha(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); } +constexpr bool IsDigit(char c) { return c >= '0' && c <= '9'; } +constexpr char Lower(char c) { return c >= 'A' && c <= 'Z' ? static_cast(c - 'A' + 'a') : c; } +constexpr char Upper(char c) { return c >= 'a' && c <= 'z' ? static_cast(c - 'a' + 'A') : c; } + +constexpr bool AllOf(std::string_view text, bool (*predicate)(char)) { + for (char c : text) { + if (!predicate(c)) return false; + } + return true; +} + +constexpr bool IsAlphanumeric(char c) { return IsAlpha(c) || IsDigit(c); } + +} + +inline bool ParseTag(std::string_view text, Tag& tag) { + Tag parsed; + std::size_t index = 0; + bool seenScript = false; + bool seenRegion = false; + bool tail = false; + std::size_t position = 0; + while (true) { + const std::size_t end = text.find('-', position); + const std::string_view subtag = text.substr(position, end == std::string_view::npos ? std::string_view::npos : end - position); + if (subtag.empty() || subtag.size() > 8 || !Detail::AllOf(subtag, Detail::IsAlphanumeric)) return false; + if (index == 0) { + if ((subtag.size() != 2 && subtag.size() != 3) || !Detail::AllOf(subtag, Detail::IsAlpha)) return false; + for (char c : subtag) parsed.language += Detail::Lower(c); + } else if (!tail) { + if (subtag.size() == 1) { + tail = true; + } else if (!seenScript && !seenRegion && subtag.size() == 4 && Detail::AllOf(subtag, Detail::IsAlpha)) { + seenScript = true; + parsed.script += Detail::Upper(subtag[0]); + for (std::size_t i = 1; i < subtag.size(); ++i) parsed.script += Detail::Lower(subtag[i]); + } else if (!seenRegion && ((subtag.size() == 2 && Detail::AllOf(subtag, Detail::IsAlpha)) || (subtag.size() == 3 && Detail::AllOf(subtag, Detail::IsDigit)))) { + seenRegion = true; + for (char c : subtag) parsed.region += Detail::Upper(c); + } else { + tail = true; + } + } + ++index; + if (end == std::string_view::npos) break; + position = end + 1; + } + tag = std::move(parsed); + return true; +} + +} + +#endif From 238bf96bcc01e74e2f4dcb4d5557cc77ce6ebe8b Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 04:24:07 +0000 Subject: [PATCH 02/39] test(libSceSystemService): cover the BCP-47 locale tag parser Adds a host-only test, system_service_language_locale, for ParseTag: case normalisation of the language, script and region subtags, the three-digit region of es-419, variants and extensions that are ignored, and the inputs it must reject (empty and oversized subtags, one-letter and long languages, the POSIX underscore and codeset forms) without touching the output tag. --- .../prx/libSceSystemService/CMakeLists.txt | 5 ++ .../tests/SystemServiceLanguageLocale.cpp | 59 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 core/libs/tests/SystemServiceLanguageLocale.cpp diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index 9e0190d6ad..63c13e53f2 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -90,6 +90,11 @@ if(BUILD_TESTING) set_tests_properties(guest_system_service_language PROPERTIES ENVIRONMENT_MODIFICATION "PATH=path_list_prepend:$") endif() + add_test_executable(system_service_language_locale_tests ${LIBS_INCLUDE_DIR}/tests/SystemServiceLanguageLocale.cpp) + target_include_directories(system_service_language_locale_tests PRIVATE ${LIBS_INCLUDE_DIR}) + configure_host_test_executable(system_service_language_locale_tests) + add_test(NAME system_service_language_locale COMMAND system_service_language_locale_tests) + set_tests_properties(system_service_language_locale PROPERTIES TIMEOUT 20) add_test_executable(guest_system_service_time_zone_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemServiceTimeZone.cpp) target_include_directories(guest_system_service_time_zone_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_system_service_time_zone_tests PRIVATE libSceSystemService libc) diff --git a/core/libs/tests/SystemServiceLanguageLocale.cpp b/core/libs/tests/SystemServiceLanguageLocale.cpp new file mode 100644 index 0000000000..69778b6dfe --- /dev/null +++ b/core/libs/tests/SystemServiceLanguageLocale.cpp @@ -0,0 +1,59 @@ +#include "prx/libSceSystemService/LanguageLocale.hpp" +#include +#include +#include +#include + +namespace { + +void Require(bool value, std::string_view what) { + if (value) return; + std::fprintf(stderr, "check failed: %.*s\n", static_cast(what.size()), what.data()); + std::abort(); +} + +struct ParsedTag { + std::string_view text; + std::string_view language; + std::string_view script; + std::string_view region; +}; + +void RequireBcp47Parsing() { + constexpr ParsedTag accepted[] = { + {"en", "en", "", ""}, + {"en-US", "en", "", "US"}, + {"EN-us", "en", "", "US"}, + {"pt-BR", "pt", "", "BR"}, + {"es-419", "es", "", "419"}, + {"zh-Hant-TW", "zh", "Hant", "TW"}, + {"zh-hans-cn", "zh", "Hans", "CN"}, + {"ZH-HANT", "zh", "Hant", ""}, + {"sr-Latn-RS", "sr", "Latn", "RS"}, + {"fil", "fil", "", ""}, + {"en-US-u-ca-gregory", "en", "", "US"}, + {"de-DE-1996", "de", "", "DE"}, + {"ca-ES-valencia", "ca", "", "ES"}, + {"en-x-private", "en", "", ""}, + {"en-Latn-US-x-a", "en", "Latn", "US"}, + }; + for (const auto& expected : accepted) { + SystemServiceLocale::Tag tag; + Require(SystemServiceLocale::ParseTag(expected.text, tag), expected.text); + Require(tag.language == expected.language, expected.text); + Require(tag.script == expected.script, expected.text); + Require(tag.region == expected.region, expected.text); + } + for (std::string_view rejected : {"", "-", "en-", "-en", "en--US", "e", "english", "e1", "12", "en_US", "en-US.UTF-8", "en US", "en-U$", "en-abcdefghi", "C", "x-foo"}) { + SystemServiceLocale::Tag tag{"keep", "keep", "keep"}; + const bool parsed = SystemServiceLocale::ParseTag(rejected, tag); + Require(!parsed, rejected); + Require(tag.language == "keep" && tag.script == "keep" && tag.region == "keep", rejected); + } +} + +} + +int main() { + RequireBcp47Parsing(); +} From 4d814c6a8cb4842e8cda3b55a8fa752166acb0d8 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 04:24:36 +0000 Subject: [PATCH 03/39] feat(libSceSystemService): normalise POSIX locale names to BCP-47 form POSIX hosts name their locale language[_territory][.codeset][@modifier], such as en_GB.UTF-8@euro, and use C and POSIX for no locale at all. NormalizePosixLocale drops the codeset and the modifier, turns underscores into hyphens, maps the @latin and @cyrillic modifiers to the Latn and Cyrl scripts where the name has no script, and returns an empty string for C, POSIX and an empty name so the caller sees "no locale" instead of a language called C. --- .../libSceSystemService/LanguageLocale.hpp | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/core/libs/prx/libSceSystemService/LanguageLocale.hpp b/core/libs/prx/libSceSystemService/LanguageLocale.hpp index 94c70ff50e..7b33c3c383 100644 --- a/core/libs/prx/libSceSystemService/LanguageLocale.hpp +++ b/core/libs/prx/libSceSystemService/LanguageLocale.hpp @@ -30,6 +30,14 @@ constexpr bool AllOf(std::string_view text, bool (*predicate)(char)) { constexpr bool IsAlphanumeric(char c) { return IsAlpha(c) || IsDigit(c); } +constexpr bool EqualsIgnoringCase(std::string_view left, std::string_view right) { + if (left.size() != right.size()) return false; + for (std::size_t i = 0; i < left.size(); ++i) { + if (Lower(left[i]) != Lower(right[i])) return false; + } + return true; +} + } inline bool ParseTag(std::string_view text, Tag& tag) { @@ -68,6 +76,28 @@ inline bool ParseTag(std::string_view text, Tag& tag) { return true; } +inline std::string NormalizePosixLocale(std::string_view text) { + std::string_view modifier; + if (const std::size_t at = text.find('@'); at != std::string_view::npos) { + modifier = text.substr(at + 1); + text = text.substr(0, at); + } + text = text.substr(0, text.find('.')); + std::string name; + for (char c : text) name += c == '_' ? '-' : c; + if (name.empty() || Detail::EqualsIgnoringCase(name, "C") || Detail::EqualsIgnoringCase(name, "POSIX")) return {}; + std::string_view script; + if (Detail::EqualsIgnoringCase(modifier, "latin")) script = "Latn"; + if (Detail::EqualsIgnoringCase(modifier, "cyrillic")) script = "Cyrl"; + if (!script.empty()) { + const std::size_t dash = name.find('-'); + const std::string_view second = dash == std::string::npos ? std::string_view{} : std::string_view(name).substr(dash + 1, 4); + const bool hasScript = second.size() == 4 && Detail::AllOf(second, Detail::IsAlpha) && (dash + 5 == name.size() || name[dash + 5] == '-'); + if (!hasScript) name.insert(dash == std::string::npos ? name.size() : dash, std::string("-") + std::string(script)); + } + return name; +} + } #endif From c386fbe7f7046e3c2ca32e39fa7b5b0b2d9349d7 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 04:24:55 +0000 Subject: [PATCH 04/39] test(libSceSystemService): cover POSIX locale name normalisation Checks NormalizePosixLocale on codesets, modifiers, the Latn and Cyrl script modifiers (also where the name already has a script), names that are already BCP-47, and C, POSIX and empty names, which must normalise to no locale. --- .../tests/SystemServiceLanguageLocale.cpp | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/core/libs/tests/SystemServiceLanguageLocale.cpp b/core/libs/tests/SystemServiceLanguageLocale.cpp index 69778b6dfe..95e1c8743d 100644 --- a/core/libs/tests/SystemServiceLanguageLocale.cpp +++ b/core/libs/tests/SystemServiceLanguageLocale.cpp @@ -3,6 +3,7 @@ #include #include #include +#include namespace { @@ -52,8 +53,40 @@ void RequireBcp47Parsing() { } } +void RequirePosixNormalisation() { + constexpr std::pair names[] = { + {"en_US.UTF-8", "en-US"}, + {"en_GB.UTF-8@euro", "en-GB"}, + {"de_DE.ISO-8859-15@euro", "de-DE"}, + {"pt_BR", "pt-BR"}, + {"zh_TW", "zh-TW"}, + {"zh_Hant_TW", "zh-Hant-TW"}, + {"ca_ES@valencia", "ca-ES"}, + {"sr_RS@latin", "sr-Latn-RS"}, + {"sr@latin", "sr-Latn"}, + {"sr_Latn_RS@latin", "sr-Latn-RS"}, + {"uz_UZ@Cyrillic", "uz-Cyrl-UZ"}, + {"fr", "fr"}, + {"en-US", "en-US"}, + {"EN_us.utf8", "EN-us"}, + {"C", ""}, + {"POSIX", ""}, + {"c", ""}, + {"posix", ""}, + {"C.UTF-8", ""}, + {"C.utf8@euro", ""}, + {"", ""}, + {"@latin", ""}, + {".UTF-8", ""}, + }; + for (const auto& [name, expected] : names) { + Require(SystemServiceLocale::NormalizePosixLocale(name) == expected, name); + } +} + } int main() { RequireBcp47Parsing(); + RequirePosixNormalisation(); } From f2fbb65aaa9d8710466ac3ba7b51add6673a099a Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 04:25:08 +0000 Subject: [PATCH 05/39] feat(libSceSystemService): parse a locale given as a BCP-47 tag or a POSIX name ParseLocale normalises the text as a POSIX name first, which leaves a BCP-47 tag unchanged except for underscores, and then parses the result, so the caller handles en-US and en_US.UTF-8 alike. C, POSIX and an empty text report no locale and leave the output tag as it was. --- core/libs/prx/libSceSystemService/LanguageLocale.hpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/prx/libSceSystemService/LanguageLocale.hpp b/core/libs/prx/libSceSystemService/LanguageLocale.hpp index 7b33c3c383..4d0373087c 100644 --- a/core/libs/prx/libSceSystemService/LanguageLocale.hpp +++ b/core/libs/prx/libSceSystemService/LanguageLocale.hpp @@ -98,6 +98,11 @@ inline std::string NormalizePosixLocale(std::string_view text) { return name; } +inline bool ParseLocale(std::string_view text, Tag& tag) { + const std::string normalised = NormalizePosixLocale(text); + return !normalised.empty() && ParseTag(normalised, tag); +} + } #endif From 83e0bfdfd66a657f89ecfcdbf20c92f862951dda Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 04:25:08 +0000 Subject: [PATCH 06/39] test(libSceSystemService): cover parsing a locale in either form Checks that BCP-47 and POSIX spellings of the same locale give the same language, script and region, that the @latin modifier supplies a script, and that C, POSIX, empty and malformed texts are refused without writing the tag. --- .../tests/SystemServiceLanguageLocale.cpp | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/core/libs/tests/SystemServiceLanguageLocale.cpp b/core/libs/tests/SystemServiceLanguageLocale.cpp index 95e1c8743d..c9e59d2e2a 100644 --- a/core/libs/tests/SystemServiceLanguageLocale.cpp +++ b/core/libs/tests/SystemServiceLanguageLocale.cpp @@ -84,9 +84,33 @@ void RequirePosixNormalisation() { } } +void RequireEitherForm() { + constexpr ParsedTag locales[] = { + {"en_US.UTF-8", "en", "", "US"}, + {"en-US", "en", "", "US"}, + {"zh_Hant_TW.UTF-8", "zh", "Hant", "TW"}, + {"zh-Hans-CN", "zh", "Hans", "CN"}, + {"sr_RS@latin", "sr", "Latn", "RS"}, + {"pt_BR.utf8", "pt", "", "BR"}, + {"es_419", "es", "", "419"}, + {"fr", "fr", "", ""}, + }; + for (const auto& expected : locales) { + SystemServiceLocale::Tag tag; + Require(SystemServiceLocale::ParseLocale(expected.text, tag), expected.text); + Require(tag.language == expected.language && tag.script == expected.script && tag.region == expected.region, expected.text); + } + for (std::string_view none : {"", "C", "POSIX", "C.UTF-8", "@latin", "garbage!", "english", "e", "en__US", "_US", "123"}) { + SystemServiceLocale::Tag tag{"keep", "keep", "keep"}; + Require(!SystemServiceLocale::ParseLocale(none, tag), none); + Require(tag.language == "keep" && tag.script == "keep" && tag.region == "keep", none); + } +} + } int main() { RequireBcp47Parsing(); RequirePosixNormalisation(); + RequireEitherForm(); } From 48e165218dafb96f5e131a67444dfb19b9ef4e21 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:06:28 +0000 Subject: [PATCH 07/39] feat(libSceSystemService): look up the console language of a locale in a row table ConsoleLanguageForTag picks, among the rows of the tag's language whose script and region are empty or equal to the tag's, the one that names the most (script outranks region), so a regional row wins over the language's main row and a language with no row for the tag's region falls back to its main row. The first group of rows covers the Latin-script European languages that have no regional split but a few: English (United Kingdom for GB, IE, AU and NZ, English (United States) otherwise), French and French (Canada), German, Italian, Dutch and Romanian. --- .../libSceSystemService/LanguageLocale.hpp | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) diff --git a/core/libs/prx/libSceSystemService/LanguageLocale.hpp b/core/libs/prx/libSceSystemService/LanguageLocale.hpp index 4d0373087c..6a31211fde 100644 --- a/core/libs/prx/libSceSystemService/LanguageLocale.hpp +++ b/core/libs/prx/libSceSystemService/LanguageLocale.hpp @@ -2,6 +2,7 @@ #define CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_LANGUAGELOCALE_HPP #include +#include #include #include #include @@ -14,6 +15,49 @@ struct Tag { std::string region; }; +enum ConsoleLanguage : int { + Japanese = 0, + EnglishUs = 1, + French = 2, + Spanish = 3, + German = 4, + Italian = 5, + Dutch = 6, + PortuguesePortugal = 7, + Russian = 8, + Korean = 9, + ChineseTraditional = 10, + ChineseSimplified = 11, + Finnish = 12, + Swedish = 13, + Danish = 14, + Norwegian = 15, + Polish = 16, + PortugueseBrazil = 17, + EnglishUk = 18, + Turkish = 19, + SpanishLatinAmerica = 20, + Arabic = 21, + FrenchCanada = 22, + Czech = 23, + Hungarian = 24, + Greek = 25, + Romanian = 26, + Thai = 27, + Vietnamese = 28, + Indonesian = 29, + Ukrainian = 30, +}; + +constexpr int NoConsoleLanguage = -1; + +struct Row { + std::string_view language; + std::string_view script; + std::string_view region; + int id; +}; + namespace Detail { constexpr bool IsAlpha(char c) { return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z'); } @@ -38,6 +82,25 @@ constexpr bool EqualsIgnoringCase(std::string_view left, std::string_view right) return true; } +inline constexpr Row LatinEuropeanRows[] = { + {"en", "", "", EnglishUs}, + {"en", "", "GB", EnglishUk}, + {"en", "", "IE", EnglishUk}, + {"en", "", "AU", EnglishUk}, + {"en", "", "NZ", EnglishUk}, + {"fr", "", "", French}, + {"fr", "", "CA", FrenchCanada}, + {"de", "", "", German}, + {"it", "", "", Italian}, + {"nl", "", "", Dutch}, + {"ro", "", "", Romanian}, + {"mo", "", "", Romanian}, +}; + +inline constexpr std::span RowGroups[] = { + LatinEuropeanRows, +}; + } inline bool ParseTag(std::string_view text, Tag& tag) { @@ -103,6 +166,24 @@ inline bool ParseLocale(std::string_view text, Tag& tag) { return !normalised.empty() && ParseTag(normalised, tag); } +inline int ConsoleLanguageForTag(const Tag& tag) { + int best = NoConsoleLanguage; + int bestScore = -1; + for (const std::span group : Detail::RowGroups) { + for (const Row& row : group) { + if (row.language != tag.language) continue; + if (!row.script.empty() && row.script != tag.script) continue; + if (!row.region.empty() && row.region != tag.region) continue; + const int score = (row.script.empty() ? 0 : 2) + (row.region.empty() ? 0 : 1); + if (score > bestScore) { + best = row.id; + bestScore = score; + } + } + } + return best; +} + } #endif From 6478422bcabd205573542f5d7a48511924d60283 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:06:50 +0000 Subject: [PATCH 08/39] test(libSceSystemService): cover the Latin-script European locale rows Maps English, French, German, Italian, Dutch and Romanian locales in BCP-47 and POSIX spellings, including the regional rows (en-GB, en-IE, en-AU, en-NZ, fr-CA) and the regions that fall back to the main row (en-CA, en-IN, fr-BE, fr-CH, de-AT, de-CH, nl-BE). --- .../tests/SystemServiceLanguageLocale.cpp | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/core/libs/tests/SystemServiceLanguageLocale.cpp b/core/libs/tests/SystemServiceLanguageLocale.cpp index c9e59d2e2a..b2ff159df6 100644 --- a/core/libs/tests/SystemServiceLanguageLocale.cpp +++ b/core/libs/tests/SystemServiceLanguageLocale.cpp @@ -107,10 +107,45 @@ void RequireEitherForm() { } } +struct LocaleLanguage { + std::string_view locale; + int id; +}; + +int LanguageOf(std::string_view locale) { + SystemServiceLocale::Tag tag; + if (!SystemServiceLocale::ParseLocale(locale, tag)) return SystemServiceLocale::NoConsoleLanguage; + return SystemServiceLocale::ConsoleLanguageForTag(tag); +} + +void RequireLanguages(const LocaleLanguage* first, const LocaleLanguage* last) { + for (const LocaleLanguage* entry = first; entry != last; ++entry) Require(LanguageOf(entry->locale) == entry->id, entry->locale); +} + +template +void RequireLanguages(const LocaleLanguage (&entries)[N]) { + RequireLanguages(entries, entries + N); +} + +void RequireLatinEuropeanRows() { + using namespace SystemServiceLocale; + constexpr LocaleLanguage entries[] = { + {"en", EnglishUs}, {"en-US", EnglishUs}, {"en_US.UTF-8", EnglishUs}, {"EN-us", EnglishUs}, {"en-CA", EnglishUs}, {"en-IN", EnglishUs}, + {"en-GB", EnglishUk}, {"en_GB.UTF-8@euro", EnglishUk}, {"en-IE", EnglishUk}, {"en-AU", EnglishUk}, {"en-NZ", EnglishUk}, {"en-Latn-GB", EnglishUk}, + {"fr", French}, {"fr-FR", French}, {"fr_BE.UTF-8", French}, {"fr-CH", French}, {"fr-CA", FrenchCanada}, {"fr_CA.UTF-8", FrenchCanada}, + {"de", German}, {"de-DE", German}, {"de_AT.UTF-8", German}, {"de-CH", German}, + {"it", Italian}, {"it-IT", Italian}, {"it_CH", Italian}, + {"nl", Dutch}, {"nl-NL", Dutch}, {"nl_BE.UTF-8", Dutch}, + {"ro", Romanian}, {"ro-RO", Romanian}, {"mo", Romanian}, + }; + RequireLanguages(entries); +} + } int main() { RequireBcp47Parsing(); RequirePosixNormalisation(); RequireEitherForm(); + RequireLatinEuropeanRows(); } From 348ac5df095da4a92bfa7d2b7874607d2f4d3ea8 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:06:51 +0000 Subject: [PATCH 09/39] feat(libSceSystemService): map Spanish and Portuguese locales to their regional console languages The console has Spanish (Spain) and Spanish (Latin America), and Portuguese (Portugal) and Portuguese (Brazil). A bare es is Spain, and the Latin American countries, the es-419 region and es-US are Latin America. A bare pt is Brazil, the language's larger variant, and Portugal and the African and Asian territories that follow the European standard (AO, MZ, CV, GW, ST, TL, MO) are Portugal. --- .../libSceSystemService/LanguageLocale.hpp | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/core/libs/prx/libSceSystemService/LanguageLocale.hpp b/core/libs/prx/libSceSystemService/LanguageLocale.hpp index 6a31211fde..a35985d776 100644 --- a/core/libs/prx/libSceSystemService/LanguageLocale.hpp +++ b/core/libs/prx/libSceSystemService/LanguageLocale.hpp @@ -97,8 +97,43 @@ inline constexpr Row LatinEuropeanRows[] = { {"mo", "", "", Romanian}, }; +inline constexpr Row IberianRows[] = { + {"es", "", "", Spanish}, + {"es", "", "419", SpanishLatinAmerica}, + {"es", "", "MX", SpanishLatinAmerica}, + {"es", "", "AR", SpanishLatinAmerica}, + {"es", "", "BO", SpanishLatinAmerica}, + {"es", "", "CL", SpanishLatinAmerica}, + {"es", "", "CO", SpanishLatinAmerica}, + {"es", "", "CR", SpanishLatinAmerica}, + {"es", "", "CU", SpanishLatinAmerica}, + {"es", "", "DO", SpanishLatinAmerica}, + {"es", "", "EC", SpanishLatinAmerica}, + {"es", "", "GT", SpanishLatinAmerica}, + {"es", "", "HN", SpanishLatinAmerica}, + {"es", "", "NI", SpanishLatinAmerica}, + {"es", "", "PA", SpanishLatinAmerica}, + {"es", "", "PE", SpanishLatinAmerica}, + {"es", "", "PR", SpanishLatinAmerica}, + {"es", "", "PY", SpanishLatinAmerica}, + {"es", "", "SV", SpanishLatinAmerica}, + {"es", "", "US", SpanishLatinAmerica}, + {"es", "", "UY", SpanishLatinAmerica}, + {"es", "", "VE", SpanishLatinAmerica}, + {"pt", "", "", PortugueseBrazil}, + {"pt", "", "PT", PortuguesePortugal}, + {"pt", "", "AO", PortuguesePortugal}, + {"pt", "", "MZ", PortuguesePortugal}, + {"pt", "", "CV", PortuguesePortugal}, + {"pt", "", "GW", PortuguesePortugal}, + {"pt", "", "ST", PortuguesePortugal}, + {"pt", "", "TL", PortuguesePortugal}, + {"pt", "", "MO", PortuguesePortugal}, +}; + inline constexpr std::span RowGroups[] = { LatinEuropeanRows, + IberianRows, }; } From cc521b8ceabd281efbcfbd601a0947451dcc5025 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:07:03 +0000 Subject: [PATCH 10/39] test(libSceSystemService): cover the Spanish and Portuguese locale rows Checks the Spain row, every Latin American country row, the es-419 region, the Brazilian default for a bare pt, the Portuguese territories, and spellings with case differences, POSIX codesets and an explicit script. --- core/libs/tests/SystemServiceLanguageLocale.cpp | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/core/libs/tests/SystemServiceLanguageLocale.cpp b/core/libs/tests/SystemServiceLanguageLocale.cpp index b2ff159df6..b71537656b 100644 --- a/core/libs/tests/SystemServiceLanguageLocale.cpp +++ b/core/libs/tests/SystemServiceLanguageLocale.cpp @@ -141,6 +141,22 @@ void RequireLatinEuropeanRows() { RequireLanguages(entries); } +void RequireIberianRows() { + using namespace SystemServiceLocale; + constexpr LocaleLanguage entries[] = { + {"es", Spanish}, {"es-ES", Spanish}, {"es_ES.UTF-8@euro", Spanish}, {"ES-es", Spanish}, {"es-GQ", Spanish}, {"es-Latn-ES", Spanish}, + {"es-419", SpanishLatinAmerica}, {"es_MX.UTF-8", SpanishLatinAmerica}, {"es-AR", SpanishLatinAmerica}, {"es-CO", SpanishLatinAmerica}, + {"es_CL", SpanishLatinAmerica}, {"es-PE", SpanishLatinAmerica}, {"es-VE", SpanishLatinAmerica}, {"es-US", SpanishLatinAmerica}, + {"es_UY.utf8", SpanishLatinAmerica}, {"es-PR", SpanishLatinAmerica}, {"es-CU", SpanishLatinAmerica}, {"es-DO", SpanishLatinAmerica}, + {"pt", PortugueseBrazil}, {"pt-BR", PortugueseBrazil}, {"pt_BR.UTF-8", PortugueseBrazil}, {"PT-br", PortugueseBrazil}, + {"pt-PT", PortuguesePortugal}, {"pt_PT.UTF-8", PortuguesePortugal}, {"pt-AO", PortuguesePortugal}, {"pt-MZ", PortuguesePortugal}, + {"pt-CV", PortuguesePortugal}, {"pt-TL", PortuguesePortugal}, + }; + RequireLanguages(entries); + constexpr const char* latinAmerica[] = {"MX", "AR", "BO", "CL", "CO", "CR", "CU", "DO", "EC", "GT", "HN", "NI", "PA", "PE", "PR", "PY", "SV", "US", "UY", "VE", "419"}; + for (const char* region : latinAmerica) Require(LanguageOf(std::string("es-") + region) == SpanishLatinAmerica, region); +} + } int main() { @@ -148,4 +164,5 @@ int main() { RequirePosixNormalisation(); RequireEitherForm(); RequireLatinEuropeanRows(); + RequireIberianRows(); } From b9f716124d82290924a3cc253838ba20c486c9d7 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:07:16 +0000 Subject: [PATCH 11/39] feat(libSceSystemService): map Chinese locales to Traditional or Simplified A script decides first (zh-Hant is Traditional, zh-Hans Simplified, also against a contradicting region such as zh-Hans-TW), then the region (TW, HK and MO are Traditional), and a bare zh is Simplified. Mandarin (cmn) follows the same rules and Cantonese (yue) is Traditional unless it names Hans. --- .../prx/libSceSystemService/LanguageLocale.hpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/core/libs/prx/libSceSystemService/LanguageLocale.hpp b/core/libs/prx/libSceSystemService/LanguageLocale.hpp index a35985d776..cbe8cec504 100644 --- a/core/libs/prx/libSceSystemService/LanguageLocale.hpp +++ b/core/libs/prx/libSceSystemService/LanguageLocale.hpp @@ -131,9 +131,24 @@ inline constexpr Row IberianRows[] = { {"pt", "", "MO", PortuguesePortugal}, }; +inline constexpr Row ChineseRows[] = { + {"zh", "", "", ChineseSimplified}, + {"zh", "Hans", "", ChineseSimplified}, + {"zh", "Hant", "", ChineseTraditional}, + {"zh", "", "TW", ChineseTraditional}, + {"zh", "", "HK", ChineseTraditional}, + {"zh", "", "MO", ChineseTraditional}, + {"cmn", "", "", ChineseSimplified}, + {"cmn", "Hant", "", ChineseTraditional}, + {"cmn", "", "TW", ChineseTraditional}, + {"yue", "", "", ChineseTraditional}, + {"yue", "Hans", "", ChineseSimplified}, +}; + inline constexpr std::span RowGroups[] = { LatinEuropeanRows, IberianRows, + ChineseRows, }; } From cbf394c842a807ee95bcd795bcb2086f69a59a65 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:07:20 +0000 Subject: [PATCH 12/39] test(libSceSystemService): cover the Chinese locale rows Checks Simplified and Traditional by region (CN, SG, TW, HK, MO), by script (Hans, Hant), a script that contradicts the region (zh-Hans-TW, zh-Hant-CN), an unknown script and region falling back to Simplified, and the Mandarin and Cantonese rows. --- core/libs/tests/SystemServiceLanguageLocale.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/core/libs/tests/SystemServiceLanguageLocale.cpp b/core/libs/tests/SystemServiceLanguageLocale.cpp index b71537656b..823b195434 100644 --- a/core/libs/tests/SystemServiceLanguageLocale.cpp +++ b/core/libs/tests/SystemServiceLanguageLocale.cpp @@ -157,6 +157,20 @@ void RequireIberianRows() { for (const char* region : latinAmerica) Require(LanguageOf(std::string("es-") + region) == SpanishLatinAmerica, region); } +void RequireChineseRows() { + using namespace SystemServiceLocale; + constexpr LocaleLanguage entries[] = { + {"zh", ChineseSimplified}, {"zh-CN", ChineseSimplified}, {"zh_CN.UTF-8", ChineseSimplified}, {"zh-SG", ChineseSimplified}, {"zh_SG.utf8", ChineseSimplified}, + {"zh-Hans", ChineseSimplified}, {"zh-Hans-CN", ChineseSimplified}, {"zh-hans-sg", ChineseSimplified}, {"zh-Latn", ChineseSimplified}, {"zh-MY", ChineseSimplified}, + {"zh-Hant", ChineseTraditional}, {"zh-Hant-TW", ChineseTraditional}, {"zh_Hant_TW.UTF-8", ChineseTraditional}, {"zh-TW", ChineseTraditional}, {"zh_TW.UTF-8", ChineseTraditional}, + {"zh-HK", ChineseTraditional}, {"zh_HK.UTF-8", ChineseTraditional}, {"zh-MO", ChineseTraditional}, {"zh-Hant-HK", ChineseTraditional}, {"zh-Hant-MO", ChineseTraditional}, + {"zh-Hans-TW", ChineseSimplified}, {"zh-Hans-HK", ChineseSimplified}, {"zh-Hant-CN", ChineseTraditional}, + {"cmn", ChineseSimplified}, {"cmn-Hans-CN", ChineseSimplified}, {"cmn-Hant", ChineseTraditional}, {"cmn-TW", ChineseTraditional}, + {"yue", ChineseTraditional}, {"yue-HK", ChineseTraditional}, {"yue-Hans-CN", ChineseSimplified}, + }; + RequireLanguages(entries); +} + } int main() { @@ -165,4 +179,5 @@ int main() { RequireEitherForm(); RequireLatinEuropeanRows(); RequireIberianRows(); + RequireChineseRows(); } From 6db6ef40d4c603a60e9f2611cfed2ade5834e38b Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:07:34 +0000 Subject: [PATCH 13/39] feat(libSceSystemService): map Japanese and Korean locales Japanese and Korean each have one console language, so every region of ja and ko maps to it. --- core/libs/prx/libSceSystemService/LanguageLocale.hpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/core/libs/prx/libSceSystemService/LanguageLocale.hpp b/core/libs/prx/libSceSystemService/LanguageLocale.hpp index cbe8cec504..73f28df6a5 100644 --- a/core/libs/prx/libSceSystemService/LanguageLocale.hpp +++ b/core/libs/prx/libSceSystemService/LanguageLocale.hpp @@ -145,10 +145,16 @@ inline constexpr Row ChineseRows[] = { {"yue", "Hans", "", ChineseSimplified}, }; +inline constexpr Row JapaneseKoreanRows[] = { + {"ja", "", "", Japanese}, + {"ko", "", "", Korean}, +}; + inline constexpr std::span RowGroups[] = { LatinEuropeanRows, IberianRows, ChineseRows, + JapaneseKoreanRows, }; } From f365e87c521b2dfd263214a3cba041ceb6106323 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:07:37 +0000 Subject: [PATCH 14/39] test(libSceSystemService): cover the Japanese and Korean locale rows Checks ja and ko with and without a region, with explicit scripts and with POSIX codesets, including the legacy eucJP codeset. --- core/libs/tests/SystemServiceLanguageLocale.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/tests/SystemServiceLanguageLocale.cpp b/core/libs/tests/SystemServiceLanguageLocale.cpp index 823b195434..b5cb6d4513 100644 --- a/core/libs/tests/SystemServiceLanguageLocale.cpp +++ b/core/libs/tests/SystemServiceLanguageLocale.cpp @@ -171,6 +171,15 @@ void RequireChineseRows() { RequireLanguages(entries); } +void RequireJapaneseKoreanRows() { + using namespace SystemServiceLocale; + constexpr LocaleLanguage entries[] = { + {"ja", Japanese}, {"ja-JP", Japanese}, {"ja_JP.UTF-8", Japanese}, {"JA-jp", Japanese}, {"ja-Jpan-JP", Japanese}, {"ja_JP.eucJP", Japanese}, + {"ko", Korean}, {"ko-KR", Korean}, {"ko_KR.UTF-8", Korean}, {"ko-KP", Korean}, {"ko-Kore-KR", Korean}, + }; + RequireLanguages(entries); +} + } int main() { @@ -180,4 +189,5 @@ int main() { RequireLatinEuropeanRows(); RequireIberianRows(); RequireChineseRows(); + RequireJapaneseKoreanRows(); } From e439a2afe18ca401751c8065c1fb9e4ac0e07c4e Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:07:37 +0000 Subject: [PATCH 15/39] feat(libSceSystemService): map Slavic and Nordic locales Russian, Ukrainian, Polish, Czech, Finnish, Swedish and Danish map to their console language. Norwegian is the console's one Norwegian language, so no, nb (Bokmal) and nn (Nynorsk) all map to it. Lithuanian, Latvian, Estonian and the other Slavic languages the console has no entry for stay unrecognised. --- .../prx/libSceSystemService/LanguageLocale.hpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/libs/prx/libSceSystemService/LanguageLocale.hpp b/core/libs/prx/libSceSystemService/LanguageLocale.hpp index 73f28df6a5..8c823126b6 100644 --- a/core/libs/prx/libSceSystemService/LanguageLocale.hpp +++ b/core/libs/prx/libSceSystemService/LanguageLocale.hpp @@ -150,11 +150,25 @@ inline constexpr Row JapaneseKoreanRows[] = { {"ko", "", "", Korean}, }; +inline constexpr Row SlavicNordicRows[] = { + {"ru", "", "", Russian}, + {"uk", "", "", Ukrainian}, + {"pl", "", "", Polish}, + {"cs", "", "", Czech}, + {"fi", "", "", Finnish}, + {"sv", "", "", Swedish}, + {"da", "", "", Danish}, + {"no", "", "", Norwegian}, + {"nb", "", "", Norwegian}, + {"nn", "", "", Norwegian}, +}; + inline constexpr std::span RowGroups[] = { LatinEuropeanRows, IberianRows, ChineseRows, JapaneseKoreanRows, + SlavicNordicRows, }; } From f1530f373453476cf34feaa4ac64ed9a64376cf2 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:07:41 +0000 Subject: [PATCH 16/39] test(libSceSystemService): cover the Slavic and Nordic locale rows Checks every language of the group, the three Norwegian codes, and that the Baltic and other Slavic languages the console has no entry for (lt, lv, et, sk, sl, bg, sr, hr, be, is) are not mapped to a neighbour. --- .../tests/SystemServiceLanguageLocale.cpp | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/core/libs/tests/SystemServiceLanguageLocale.cpp b/core/libs/tests/SystemServiceLanguageLocale.cpp index b5cb6d4513..f3dc94131a 100644 --- a/core/libs/tests/SystemServiceLanguageLocale.cpp +++ b/core/libs/tests/SystemServiceLanguageLocale.cpp @@ -180,6 +180,24 @@ void RequireJapaneseKoreanRows() { RequireLanguages(entries); } +void RequireSlavicNordicRows() { + using namespace SystemServiceLocale; + constexpr LocaleLanguage entries[] = { + {"ru", Russian}, {"ru-RU", Russian}, {"ru_RU.UTF-8", Russian}, {"ru-UA", Russian}, {"ru-BY", Russian}, + {"uk", Ukrainian}, {"uk-UA", Ukrainian}, {"uk_UA.UTF-8", Ukrainian}, + {"pl", Polish}, {"pl-PL", Polish}, {"pl_PL.UTF-8", Polish}, + {"cs", Czech}, {"cs-CZ", Czech}, {"cs_CZ.UTF-8", Czech}, + {"fi", Finnish}, {"fi-FI", Finnish}, {"fi_FI.UTF-8@euro", Finnish}, + {"sv", Swedish}, {"sv-SE", Swedish}, {"sv_FI.UTF-8", Swedish}, + {"da", Danish}, {"da-DK", Danish}, {"da_DK.UTF-8", Danish}, + {"no", Norwegian}, {"no-NO", Norwegian}, {"nb", Norwegian}, {"nb-NO", Norwegian}, {"nb_NO.UTF-8", Norwegian}, {"nn", Norwegian}, {"nn-NO", Norwegian}, {"nn_NO.UTF-8", Norwegian}, + }; + RequireLanguages(entries); + for (std::string_view unmapped : {"lt-LT", "lv-LV", "et-EE", "sk-SK", "sl-SI", "bg-BG", "sr-RS", "sr_RS@latin", "hr-HR", "be-BY", "is-IS"}) { + Require(LanguageOf(unmapped) == NoConsoleLanguage, unmapped); + } +} + } int main() { @@ -190,4 +208,5 @@ int main() { RequireIberianRows(); RequireChineseRows(); RequireJapaneseKoreanRows(); + RequireSlavicNordicRows(); } From 7538dd65df438707a012d842036a8eb057505e9e Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:07:50 +0000 Subject: [PATCH 17/39] feat(libSceSystemService): map Arabic, Thai, Vietnamese, Indonesian, Turkish, Greek and Hungarian locales Each has a single console language, so every region maps to it. The legacy code in for Indonesian, still used by some POSIX and Java hosts, is accepted as well as id. --- core/libs/prx/libSceSystemService/LanguageLocale.hpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/libs/prx/libSceSystemService/LanguageLocale.hpp b/core/libs/prx/libSceSystemService/LanguageLocale.hpp index 8c823126b6..9ca55c1a29 100644 --- a/core/libs/prx/libSceSystemService/LanguageLocale.hpp +++ b/core/libs/prx/libSceSystemService/LanguageLocale.hpp @@ -163,12 +163,24 @@ inline constexpr Row SlavicNordicRows[] = { {"nn", "", "", Norwegian}, }; +inline constexpr Row OtherRows[] = { + {"ar", "", "", Arabic}, + {"th", "", "", Thai}, + {"vi", "", "", Vietnamese}, + {"id", "", "", Indonesian}, + {"in", "", "", Indonesian}, + {"tr", "", "", Turkish}, + {"el", "", "", Greek}, + {"hu", "", "", Hungarian}, +}; + inline constexpr std::span RowGroups[] = { LatinEuropeanRows, IberianRows, ChineseRows, JapaneseKoreanRows, SlavicNordicRows, + OtherRows, }; } From 678cfa6f7174f848fbd6f21f5528a444ea5d4ecc Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:07:54 +0000 Subject: [PATCH 18/39] test(libSceSystemService): cover the Arabic, Thai, Vietnamese, Indonesian, Turkish, Greek and Hungarian rows Checks each language with several regions and POSIX spellings, the legacy in code, and ar-001 (the world region). --- core/libs/tests/SystemServiceLanguageLocale.cpp | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/core/libs/tests/SystemServiceLanguageLocale.cpp b/core/libs/tests/SystemServiceLanguageLocale.cpp index f3dc94131a..23aafbed33 100644 --- a/core/libs/tests/SystemServiceLanguageLocale.cpp +++ b/core/libs/tests/SystemServiceLanguageLocale.cpp @@ -198,6 +198,20 @@ void RequireSlavicNordicRows() { } } +void RequireOtherRows() { + using namespace SystemServiceLocale; + constexpr LocaleLanguage entries[] = { + {"ar", Arabic}, {"ar-AE", Arabic}, {"ar-SA", Arabic}, {"ar_EG.UTF-8", Arabic}, {"ar-MA", Arabic}, {"ar-001", Arabic}, + {"th", Thai}, {"th-TH", Thai}, {"th_TH.UTF-8", Thai}, + {"vi", Vietnamese}, {"vi-VN", Vietnamese}, {"vi_VN.utf8", Vietnamese}, + {"id", Indonesian}, {"id-ID", Indonesian}, {"id_ID.UTF-8", Indonesian}, {"in", Indonesian}, {"in_ID", Indonesian}, + {"tr", Turkish}, {"tr-TR", Turkish}, {"tr_TR.UTF-8", Turkish}, {"tr-CY", Turkish}, + {"el", Greek}, {"el-GR", Greek}, {"el_GR.UTF-8", Greek}, {"el-CY", Greek}, + {"hu", Hungarian}, {"hu-HU", Hungarian}, {"hu_HU.UTF-8", Hungarian}, + }; + RequireLanguages(entries); +} + } int main() { @@ -209,4 +223,5 @@ int main() { RequireChineseRows(); RequireJapaneseKoreanRows(); RequireSlavicNordicRows(); + RequireOtherRows(); } From 26f2879f05ddbc655622e47df474fe6fcd2cf232 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:08:09 +0000 Subject: [PATCH 19/39] feat(libSceSystemService): fall back to English (United States) for a locale with no console language MatchLocale returns the console language of a locale and whether the locale was recognised. An empty, C, POSIX, malformed or unlisted locale is not recognised and gives English (United States), the language the console answers when ANYPS5_LANGUAGE is unset, so resolving a host locale cannot fail. A recognised language with no row for its region already takes the language's main row. --- core/libs/prx/libSceSystemService/LanguageLocale.hpp | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/core/libs/prx/libSceSystemService/LanguageLocale.hpp b/core/libs/prx/libSceSystemService/LanguageLocale.hpp index 9ca55c1a29..db27c10b3c 100644 --- a/core/libs/prx/libSceSystemService/LanguageLocale.hpp +++ b/core/libs/prx/libSceSystemService/LanguageLocale.hpp @@ -266,6 +266,18 @@ inline int ConsoleLanguageForTag(const Tag& tag) { return best; } +struct Match { + int id; + bool recognised; +}; + +inline Match MatchLocale(std::string_view locale) { + Tag tag; + const int id = ParseLocale(locale, tag) ? ConsoleLanguageForTag(tag) : NoConsoleLanguage; + if (id == NoConsoleLanguage) return {EnglishUs, false}; + return {id, true}; +} + } #endif From 79692c44efa0ad3151156456790ff69194ca73f2 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:08:32 +0000 Subject: [PATCH 20/39] test(libSceSystemService): cover the locale fallback rules Checks that every console language is reached by a tag of its own, that unrecognised locales (empty, C, POSIX, malformed, languages the console has no entry for such as lt, he, hi, ca and sr) give English (United States) and are reported as unrecognised, and that a recognised language with an unknown region or script takes its main row. --- .../tests/SystemServiceLanguageLocale.cpp | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/core/libs/tests/SystemServiceLanguageLocale.cpp b/core/libs/tests/SystemServiceLanguageLocale.cpp index 23aafbed33..720d78a86d 100644 --- a/core/libs/tests/SystemServiceLanguageLocale.cpp +++ b/core/libs/tests/SystemServiceLanguageLocale.cpp @@ -212,6 +212,27 @@ void RequireOtherRows() { RequireLanguages(entries); } +void RequireFallbackRules() { + using namespace SystemServiceLocale; + constexpr const char* perLanguage[31] = { + "ja-JP", "en-US", "fr-FR", "es-ES", "de-DE", "it-IT", "nl-NL", "pt-PT", "ru-RU", "ko-KR", "zh-Hant", "zh-Hans", "fi-FI", "sv-SE", "da-DK", "no-NO", + "pl-PL", "pt-BR", "en-GB", "tr-TR", "es-419", "ar-AE", "fr-CA", "cs-CZ", "hu-HU", "el-GR", "ro-RO", "th-TH", "vi-VN", "id-ID", "uk-UA"}; + for (int id = 0; id <= 30; ++id) { + const Match match = MatchLocale(perLanguage[id]); + Require(match.recognised && match.id == id, perLanguage[id]); + } + for (std::string_view unrecognised : {"", "C", "POSIX", "C.UTF-8", "c", "garbage", "xx", "xx-YY", "x-foo", "en_", "_US", "lt-LT", "sw-KE", "he-IL", "hi-IN", "sr-Latn-RS", "ca-ES", "eu", "gl-ES", "qaa", "en--US", "12345", "\t"}) { + const Match match = MatchLocale(unrecognised); + Require(!match.recognised && match.id == EnglishUs, unrecognised); + } + Require(MatchLocale("en-US").recognised && MatchLocale("en-ZZ").recognised && MatchLocale("en-ZZ").id == EnglishUs, "en-ZZ"); + Require(MatchLocale("fr-ZZ").recognised && MatchLocale("fr-ZZ").id == French, "fr-ZZ"); + Require(MatchLocale("zh-ZZ").recognised && MatchLocale("zh-ZZ").id == ChineseSimplified, "zh-ZZ"); + Require(MatchLocale("pt-ZZ").recognised && MatchLocale("pt-ZZ").id == PortugueseBrazil, "pt-ZZ"); + Require(MatchLocale("es-ZZ").recognised && MatchLocale("es-ZZ").id == Spanish, "es-ZZ"); + Require(MatchLocale("de-Cyrl-RU").recognised && MatchLocale("de-Cyrl-RU").id == German, "de-Cyrl-RU"); +} + } int main() { @@ -224,4 +245,5 @@ int main() { RequireJapaneseKoreanRows(); RequireSlavicNordicRows(); RequireOtherRows(); + RequireFallbackRules(); } From c3fa50327ce2a34fcffbb7c830d7a5bcc6da053d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:08:34 +0000 Subject: [PATCH 21/39] feat(libSceSystemService): read the host locales of a POSIX system PosixHostLocales takes an injectable environment reader and returns the locales in order of preference: LC_ALL, then LC_MESSAGES, then LANG, the first one that is set and not empty, as the C library chooses the locale of the messages category. When that locale is a real one (not C, POSIX or unset) the entries of LANGUAGE, the GNU preference list, come before it, as gettext ignores LANGUAGE under the C locale. --- .../libSceSystemService/LanguageLocale.hpp | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/core/libs/prx/libSceSystemService/LanguageLocale.hpp b/core/libs/prx/libSceSystemService/LanguageLocale.hpp index db27c10b3c..bc2263658d 100644 --- a/core/libs/prx/libSceSystemService/LanguageLocale.hpp +++ b/core/libs/prx/libSceSystemService/LanguageLocale.hpp @@ -2,10 +2,13 @@ #define CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_LANGUAGELOCALE_HPP #include +#include +#include #include #include #include #include +#include namespace SystemServiceLocale { @@ -278,6 +281,32 @@ inline Match MatchLocale(std::string_view locale) { return {id, true}; } +using Environment = std::function; + +inline std::vector PosixHostLocales(const Environment& environment) { + const auto read = [&environment](const char* name) { + const char* value = environment(name); + return std::string_view(value != nullptr ? value : ""); + }; + std::string_view effective; + for (const char* name : {"LC_ALL", "LC_MESSAGES", "LANG"}) { + effective = read(name); + if (!effective.empty()) break; + } + std::vector locales; + if (!NormalizePosixLocale(effective).empty()) { + std::string_view preferred = read("LANGUAGE"); + while (!preferred.empty()) { + const std::size_t colon = preferred.find(':'); + const std::string_view entry = preferred.substr(0, colon); + if (!entry.empty()) locales.emplace_back(entry); + preferred = colon == std::string_view::npos ? std::string_view{} : preferred.substr(colon + 1); + } + } + if (!effective.empty()) locales.emplace_back(effective); + return locales; +} + } #endif From 56947540a6e31b4c0aa72a07a339cf3d0d8035b9 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:09:04 +0000 Subject: [PATCH 22/39] test(libSceSystemService): cover the POSIX host locale source with injected environments Checks the LC_ALL, LC_MESSAGES, LANG precedence with empty and unset variables, that other categories are not read, and the LANGUAGE rules: its entries come first under a real locale, empty entries are skipped, and it is ignored under C, POSIX, C.UTF-8 and when nothing else is set. --- .../tests/SystemServiceLanguageLocale.cpp | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/core/libs/tests/SystemServiceLanguageLocale.cpp b/core/libs/tests/SystemServiceLanguageLocale.cpp index 720d78a86d..e70943d0ed 100644 --- a/core/libs/tests/SystemServiceLanguageLocale.cpp +++ b/core/libs/tests/SystemServiceLanguageLocale.cpp @@ -1,9 +1,11 @@ #include "prx/libSceSystemService/LanguageLocale.hpp" #include #include +#include #include #include #include +#include namespace { @@ -233,6 +235,33 @@ void RequireFallbackRules() { Require(MatchLocale("de-Cyrl-RU").recognised && MatchLocale("de-Cyrl-RU").id == German, "de-Cyrl-RU"); } +std::vector Posix(const std::map& variables) { + return SystemServiceLocale::PosixHostLocales([&variables](const char* name) -> const char* { + const auto found = variables.find(name); + return found == variables.end() ? nullptr : found->second.c_str(); + }); +} + +void RequirePosixSource() { + using Locales = std::vector; + Require(Posix({}).empty(), "nothing set"); + Require(Posix({{"LANG", ""}, {"LC_ALL", ""}}).empty(), "all empty"); + Require(Posix({{"LANG", "de_DE.UTF-8"}}) == Locales{"de_DE.UTF-8"}, "LANG alone"); + Require(Posix({{"LC_MESSAGES", "fr_FR.UTF-8"}, {"LANG", "de_DE.UTF-8"}}) == Locales{"fr_FR.UTF-8"}, "LC_MESSAGES over LANG"); + Require(Posix({{"LC_ALL", "it_IT.UTF-8"}, {"LC_MESSAGES", "fr_FR.UTF-8"}, {"LANG", "de_DE.UTF-8"}}) == Locales{"it_IT.UTF-8"}, "LC_ALL over the rest"); + Require(Posix({{"LC_ALL", ""}, {"LC_MESSAGES", ""}, {"LANG", "nl_NL"}}) == Locales{"nl_NL"}, "empty values are skipped"); + Require(Posix({{"LC_ALL", ""}, {"LC_MESSAGES", "es_ES"}, {"LANG", "nl_NL"}}) == Locales{"es_ES"}, "empty LC_ALL is skipped"); + Require(Posix({{"LC_CTYPE", "ja_JP.UTF-8"}, {"LC_TIME", "ko_KR"}}).empty(), "other categories are not read"); + Require(Posix({{"LANG", "de_DE.UTF-8"}, {"LANGUAGE", "es:fr"}}) == Locales{"es", "fr", "de_DE.UTF-8"}, "LANGUAGE precedes a real locale"); + Require(Posix({{"LANG", "de_DE.UTF-8"}, {"LANGUAGE", ":pt_BR::en:"}}) == Locales{"pt_BR", "en", "de_DE.UTF-8"}, "empty LANGUAGE entries are skipped"); + Require(Posix({{"LANG", "de_DE.UTF-8"}, {"LANGUAGE", ""}}) == Locales{"de_DE.UTF-8"}, "empty LANGUAGE"); + Require(Posix({{"LANG", "C"}, {"LANGUAGE", "es"}}) == Locales{"C"}, "LANGUAGE is ignored under C"); + Require(Posix({{"LC_ALL", "POSIX"}, {"LANG", "de_DE"}, {"LANGUAGE", "es"}}) == Locales{"POSIX"}, "LANGUAGE is ignored under POSIX"); + Require(Posix({{"LANG", "C.UTF-8"}, {"LANGUAGE", "es"}}) == Locales{"C.UTF-8"}, "LANGUAGE is ignored under C.UTF-8"); + Require(Posix({{"LANGUAGE", "es"}}).empty(), "LANGUAGE alone is ignored"); + Require(Posix({{"LC_ALL", "C"}, {"LANG", "de_DE"}}) == Locales{"C"}, "LC_ALL=C overrides LANG"); +} + } int main() { @@ -246,4 +275,5 @@ int main() { RequireSlavicNordicRows(); RequireOtherRows(); RequireFallbackRules(); + RequirePosixSource(); } From 403ca716988e5d18d107a4a1afd45472244318a2 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:09:06 +0000 Subject: [PATCH 23/39] feat(libSceSystemService): read the host locale of a Windows system WindowsHostLocales returns the user default locale name from GetUserDefaultLocaleName, a BCP-47 tag such as en-US, zh-Hans-CN or pt-BR. It reads the regional-format locale of the user, not the list of preferred display languages, because that is one documented call with a fixed buffer. A name with a non-ASCII character is treated as no locale. Only built on Windows. --- .../libSceSystemService/LanguageLocale.hpp | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/core/libs/prx/libSceSystemService/LanguageLocale.hpp b/core/libs/prx/libSceSystemService/LanguageLocale.hpp index bc2263658d..8b72af3b26 100644 --- a/core/libs/prx/libSceSystemService/LanguageLocale.hpp +++ b/core/libs/prx/libSceSystemService/LanguageLocale.hpp @@ -10,6 +10,16 @@ #include #include +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#endif + namespace SystemServiceLocale { struct Tag { @@ -307,6 +317,20 @@ inline std::vector PosixHostLocales(const Environment& environment) return locales; } +#if defined(_WIN32) +inline std::vector WindowsHostLocales() { + wchar_t name[LOCALE_NAME_MAX_LENGTH]; + const int length = GetUserDefaultLocaleName(name, LOCALE_NAME_MAX_LENGTH); + if (length <= 1) return {}; + std::string locale; + for (int i = 0; i < length - 1; ++i) { + if (name[i] <= 0 || name[i] > 127) return {}; + locale += static_cast(name[i]); + } + return {locale}; +} +#endif + } #endif From ab43fe1e8cd523c528c63f52e6d8b93b6c7a813b Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:09:09 +0000 Subject: [PATCH 24/39] feat(libSceSystemService): read the host locales of a macOS system MacHostLocales returns the user's preferred languages in order from CFLocaleCopyPreferredLanguages, BCP-47 tags such as en-US, zh-Hans-CN or pt-BR, which the process environment of a macOS desktop session does not carry. Only built on macOS. --- .../prx/libSceSystemService/LanguageLocale.hpp | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/core/libs/prx/libSceSystemService/LanguageLocale.hpp b/core/libs/prx/libSceSystemService/LanguageLocale.hpp index 8b72af3b26..b24646dcc7 100644 --- a/core/libs/prx/libSceSystemService/LanguageLocale.hpp +++ b/core/libs/prx/libSceSystemService/LanguageLocale.hpp @@ -18,6 +18,8 @@ #define NOMINMAX #endif #include +#elif defined(__APPLE__) +#include #endif namespace SystemServiceLocale { @@ -331,6 +333,22 @@ inline std::vector WindowsHostLocales() { } #endif +#if defined(__APPLE__) +inline std::vector MacHostLocales() { + std::vector locales; + CFArrayRef languages = CFLocaleCopyPreferredLanguages(); + if (languages == nullptr) return locales; + const CFIndex count = CFArrayGetCount(languages); + for (CFIndex i = 0; i < count; ++i) { + const auto name = static_cast(CFArrayGetValueAtIndex(languages, i)); + char buffer[128]; + if (CFStringGetCString(name, buffer, sizeof buffer, kCFStringEncodingUTF8)) locales.emplace_back(buffer); + } + CFRelease(languages); + return locales; +} +#endif + } #endif From 0c4be5ba0e03348ebf0a9b2ef21ac2663cdc849d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:09:11 +0000 Subject: [PATCH 25/39] feat(libSceSystemService): choose the host locale source of the platform HostLocales calls the Windows source on Windows, the macOS source on macOS and the POSIX environment source elsewhere. --- core/libs/prx/libSceSystemService/LanguageLocale.hpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/prx/libSceSystemService/LanguageLocale.hpp b/core/libs/prx/libSceSystemService/LanguageLocale.hpp index b24646dcc7..d403b12014 100644 --- a/core/libs/prx/libSceSystemService/LanguageLocale.hpp +++ b/core/libs/prx/libSceSystemService/LanguageLocale.hpp @@ -349,6 +349,16 @@ inline std::vector MacHostLocales() { } #endif +inline std::vector HostLocales() { +#if defined(_WIN32) + return WindowsHostLocales(); +#elif defined(__APPLE__) + return MacHostLocales(); +#else + return PosixHostLocales([](const char* name) { return std::getenv(name); }); +#endif +} + } #endif From 0b45532833edcb76f6089ec0688554c86575ce5d Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:09:53 +0000 Subject: [PATCH 26/39] build(libSceSystemService): link CoreFoundation where the macOS host locale source is used The macOS locale source calls CFLocaleCopyPreferredLanguages. The header is inline, so every target that resolves the language needs the framework: an interface target carries it on macOS and nothing elsewhere, and the system service library, the trophy library and the locale test link it. --- core/libs/prx/libSceNpTrophy2/CMakeLists.txt | 2 +- core/libs/prx/libSceSystemService/CMakeLists.txt | 8 +++++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/core/libs/prx/libSceNpTrophy2/CMakeLists.txt b/core/libs/prx/libSceNpTrophy2/CMakeLists.txt index 7f9d3efe3a..7092245bcd 100644 --- a/core/libs/prx/libSceNpTrophy2/CMakeLists.txt +++ b/core/libs/prx/libSceNpTrophy2/CMakeLists.txt @@ -13,7 +13,7 @@ add_library(libSceNpTrophy2 SHARED EXCLUDE_FROM_ALL target_include_directories(libSceNpTrophy2 PRIVATE ${LIBS_INCLUDE_DIR} ) -target_link_libraries(libSceNpTrophy2 PRIVATE libc) +target_link_libraries(libSceNpTrophy2 PRIVATE libc system_service_host_locale) configure_windows_unwind(libSceNpTrophy2) if(BUILD_TESTING) diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index 63c13e53f2..b0be55b5d6 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -1,6 +1,11 @@ +add_library(system_service_host_locale INTERFACE) +if(APPLE) + target_link_libraries(system_service_host_locale INTERFACE "-framework CoreFoundation") +endif() + add_library(libSceSystemService SHARED EXCLUDE_FROM_ALL Export.cpp PendingExports.cpp LoadExec.cpp) target_include_directories(libSceSystemService PRIVATE ${LIBS_INCLUDE_DIR}) -target_link_libraries(libSceSystemService PRIVATE libSceAgcDriver libSceUsbStorage) +target_link_libraries(libSceSystemService PRIVATE libSceAgcDriver libSceUsbStorage system_service_host_locale) configure_windows_unwind(libSceSystemService) if(BUILD_TESTING) @@ -92,6 +97,7 @@ if(BUILD_TESTING) endif() add_test_executable(system_service_language_locale_tests ${LIBS_INCLUDE_DIR}/tests/SystemServiceLanguageLocale.cpp) target_include_directories(system_service_language_locale_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(system_service_language_locale_tests PRIVATE system_service_host_locale) configure_host_test_executable(system_service_language_locale_tests) add_test(NAME system_service_language_locale COMMAND system_service_language_locale_tests) set_tests_properties(system_service_language_locale PROPERTIES TIMEOUT 20) From 59d0159e3be902068419cfbba125a30afcef6a24 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:09:57 +0000 Subject: [PATCH 27/39] feat(libSceSystemService): resolve the host language from an injectable locale source ResolveHostLanguage takes the host locales in order of preference from a source and the console language of the first one that is recognised. When none is (or the source returns none), it reports one notice line that names the detected locale and the English (United States) fallback and returns it, so auto never fails. Source and notice are injectable so tests do not depend on the host. --- .../prx/libSceSystemService/LanguageLocale.hpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/core/libs/prx/libSceSystemService/LanguageLocale.hpp b/core/libs/prx/libSceSystemService/LanguageLocale.hpp index d403b12014..286573b07a 100644 --- a/core/libs/prx/libSceSystemService/LanguageLocale.hpp +++ b/core/libs/prx/libSceSystemService/LanguageLocale.hpp @@ -359,6 +359,20 @@ inline std::vector HostLocales() { #endif } +using HostLocaleSource = std::function()>; +using HostLocaleNotice = std::function; + +inline int ResolveHostLanguage(const HostLocaleSource& source, const HostLocaleNotice& notice) { + const std::vector locales = source(); + for (const std::string& locale : locales) { + const Match match = MatchLocale(locale); + if (match.recognised) return match.id; + } + const std::string detected = locales.empty() ? "the host reports no locale" : "the host locale \"" + locales.front() + "\" has no console language"; + notice("ANYPS5_LANGUAGE=auto: " + detected + ", using English (United States), language number " + std::to_string(EnglishUs)); + return EnglishUs; +} + } #endif From deff826f079fede0b56c331aa4e126e6796431af Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:10:23 +0000 Subject: [PATCH 28/39] test(libSceSystemService): cover resolving the host language from an injected source Checks the first recognised locale wins over later and earlier unrecognised ones, malformed entries are skipped, and an unrecognised or missing locale returns English (United States) with exactly one notice that names the first detected locale or says the host reports none. --- .../tests/SystemServiceLanguageLocale.cpp | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/core/libs/tests/SystemServiceLanguageLocale.cpp b/core/libs/tests/SystemServiceLanguageLocale.cpp index e70943d0ed..257556e9cd 100644 --- a/core/libs/tests/SystemServiceLanguageLocale.cpp +++ b/core/libs/tests/SystemServiceLanguageLocale.cpp @@ -262,6 +262,33 @@ void RequirePosixSource() { Require(Posix({{"LC_ALL", "C"}, {"LANG", "de_DE"}}) == Locales{"C"}, "LC_ALL=C overrides LANG"); } +int Resolve(const std::vector& locales, std::vector& notices) { + return SystemServiceLocale::ResolveHostLanguage([&locales] { return locales; }, [¬ices](const std::string& line) { notices.push_back(line); }); +} + +void RequireHostResolution() { + using namespace SystemServiceLocale; + std::vector notices; + Require(Resolve({"de_DE.UTF-8"}, notices) == German && notices.empty(), "one recognised locale"); + Require(Resolve({"pt_BR"}, notices) == PortugueseBrazil && notices.empty(), "pt_BR"); + Require(Resolve({"lt", "uk", "fr"}, notices) == Ukrainian && notices.empty(), "first recognised entry wins"); + Require(Resolve({"es", "fr", "de_DE.UTF-8"}, notices) == Spanish && notices.empty(), "LANGUAGE order"); + Require(Resolve({"en-GB", "fr"}, notices) == EnglishUk && notices.empty(), "first entry before later ones"); + Require(Resolve({"zh-Hant-TW"}, notices) == ChineseTraditional && notices.empty(), "zh-Hant-TW"); + Require(Resolve({"lt_LT.UTF-8"}, notices) == EnglishUs && notices.size() == 1, "unlisted language"); + Require(notices[0] == "ANYPS5_LANGUAGE=auto: the host locale \"lt_LT.UTF-8\" has no console language, using English (United States), language number 1", notices[0]); + notices.clear(); + Require(Resolve({"C"}, notices) == EnglishUs && notices.size() == 1, "C"); + Require(notices[0].find("\"C\"") != std::string::npos && notices[0].find("English (United States)") != std::string::npos, notices[0]); + notices.clear(); + Require(Resolve({"lt", "lv"}, notices) == EnglishUs && notices.size() == 1 && notices[0].find("\"lt\"") != std::string::npos, "first unrecognised entry is named"); + notices.clear(); + Require(Resolve({}, notices) == EnglishUs && notices.size() == 1, "no locale"); + Require(notices[0] == "ANYPS5_LANGUAGE=auto: the host reports no locale, using English (United States), language number 1", notices[0]); + notices.clear(); + Require(Resolve({"garbage!", "ja_JP"}, notices) == Japanese && notices.empty(), "malformed entry is skipped"); +} + } int main() { @@ -276,4 +303,5 @@ int main() { RequireOtherRows(); RequireFallbackRules(); RequirePosixSource(); + RequireHostResolution(); } From 0706e413940dec1cb5852557c185c11ebf4ac7dd Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:10:24 +0000 Subject: [PATCH 29/39] feat(libSceSystemService): take the console language from the host with ANYPS5_LANGUAGE=auto auto, in any letter case, resolves the host locale once per process and answers that console language for every later query of parameters 1 and 400 and for every other consumer of SystemServiceConsoleLanguage (the trophy texts, and the keyboard type the user service derives from parameter 1). An unset or empty variable and every integer behave as before and are read at each query; any other text is still refused. --- core/libs/prx/libSceSystemService/SystemService.hpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/prx/libSceSystemService/SystemService.hpp b/core/libs/prx/libSceSystemService/SystemService.hpp index dc9d45e4d9..7ca1a787dd 100644 --- a/core/libs/prx/libSceSystemService/SystemService.hpp +++ b/core/libs/prx/libSceSystemService/SystemService.hpp @@ -4,12 +4,15 @@ #include #include #include +#include #include #include #include #include #include +#include "prx/libSceSystemService/LanguageLocale.hpp" + constexpr int SYSTEM_SERVICE_OK = 0; constexpr int SYSTEM_SERVICE_ERROR_PARAMETER = -2136932349; constexpr int SYSTEM_SERVICE_ERROR_NO_EVENT = -2136932348; @@ -41,10 +44,17 @@ constexpr int SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN = 30; constexpr int SYSTEM_SERVICE_PARAM_DATE_FORMAT_DDMMYYYY = 1; constexpr int SYSTEM_SERVICE_PARAM_TIME_FORMAT_24HOUR = 1; +inline int SystemServiceHostLanguage() { + static const int language = SystemServiceLocale::ResolveHostLanguage( + SystemServiceLocale::HostLocales, [](const std::string& line) { std::fprintf(stderr, "%s\n", line.c_str()); }); + return language; +} + inline int SystemServiceConsoleLanguage(std::string_view caller) { const char* configured = std::getenv("ANYPS5_LANGUAGE"); if (configured == nullptr || configured[0] == '\0') return SYSTEM_SERVICE_PARAM_LANG_ENGLISH_US; const std::string_view text(configured); + if (SystemServiceLocale::Detail::EqualsIgnoringCase(text, "auto")) return SystemServiceHostLanguage(); int language = -1; const auto [end, error] = std::from_chars(text.data(), text.data() + text.size(), language); if (error != std::errc{} || end != text.data() + text.size() || language < SYSTEM_SERVICE_PARAM_LANG_JAPANESE || language > SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN) { From d01ee0435f0a82e9e2bbde16db1e7b4ef4ecb732 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:10:31 +0000 Subject: [PATCH 30/39] feat(libSceSystemService): name auto in the invalid ANYPS5_LANGUAGE error A value that is neither a console language number from 0 to 30 nor auto still stops the game at its first language query with an error that names the variable, which now lists auto as the other accepted value. --- core/libs/prx/libSceSystemService/SystemService.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/libs/prx/libSceSystemService/SystemService.hpp b/core/libs/prx/libSceSystemService/SystemService.hpp index 7ca1a787dd..af106ea7dc 100644 --- a/core/libs/prx/libSceSystemService/SystemService.hpp +++ b/core/libs/prx/libSceSystemService/SystemService.hpp @@ -60,7 +60,7 @@ inline int SystemServiceConsoleLanguage(std::string_view caller) { if (error != std::errc{} || end != text.data() + text.size() || language < SYSTEM_SERVICE_PARAM_LANG_JAPANESE || language > SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN) { throw std::runtime_error(std::string(caller) + ": ANYPS5_LANGUAGE must be a console language number from " + std::to_string(SYSTEM_SERVICE_PARAM_LANG_JAPANESE) + " to " + std::to_string(SYSTEM_SERVICE_PARAM_LANG_UKRAINIAN) + - ", not \"" + std::string(text) + "\""); + " or auto, not \"" + std::string(text) + "\""); } return language; } From 048ac7703d52b0d159b9589932a67dab4a5f41ad Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:11:05 +0000 Subject: [PATCH 31/39] test(libSceSystemService): cover ANYPS5_LANGUAGE=auto through sceSystemServiceParamGetInt Runs thirteen host environments (LANG, LC_MESSAGES and LC_ALL precedence, LANGUAGE, C, nothing set, an unlisted language) and checks that parameters 1 and 400 and SystemServiceConsoleLanguage answer the expected console language, that the answer does not change when the host locale changes afterwards, that integers, the empty value and an unset variable still behave as before after auto, that exactly the expected number of notices is printed, and that near misses of auto and invalid values are refused with an error that names the variable and lists auto. Registered on Linux, where the environment is the locale source. --- .../prx/libSceSystemService/CMakeLists.txt | 26 +++++ .../tests/GuestSystemServiceLanguageAuto.cpp | 95 +++++++++++++++++++ 2 files changed, 121 insertions(+) create mode 100644 core/libs/tests/GuestSystemServiceLanguageAuto.cpp diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index b0be55b5d6..caa959a3e8 100644 --- a/core/libs/prx/libSceSystemService/CMakeLists.txt +++ b/core/libs/prx/libSceSystemService/CMakeLists.txt @@ -101,6 +101,32 @@ if(BUILD_TESTING) configure_host_test_executable(system_service_language_locale_tests) add_test(NAME system_service_language_locale COMMAND system_service_language_locale_tests) set_tests_properties(system_service_language_locale PROPERTIES TIMEOUT 20) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + add_test_executable(guest_system_service_language_auto_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemServiceLanguageAuto.cpp) + target_include_directories(guest_system_service_language_auto_tests PRIVATE ${LIBS_INCLUDE_DIR}) + target_link_libraries(guest_system_service_language_auto_tests PRIVATE libSceSystemService libc system_service_host_locale) + configure_windows_unwind(guest_system_service_language_auto_tests) + set(languageAutoIndex 0) + foreach(languageAutoCase IN ITEMS + "4,-,-,de_DE.UTF-8,-,auto,0" + "17,-,-,pt_BR.UTF-8,-,AUTO,0" + "10,-,-,zh_TW.UTF-8,-,Auto,0" + "11,-,-,zh_CN.UTF-8,-,auto,0" + "20,es_MX.UTF-8,fr_FR.UTF-8,de_DE.UTF-8,-,auto,0" + "2,-,fr_FR.UTF-8,de_DE.UTF-8,-,auto,0" + "3,-,-,en_US.UTF-8,es:fr,auto,0" + "18,-,-,en_GB.UTF-8@euro,-,auto,0" + "0,-,-,ja_JP.UTF-8,-,auto,0" + "1,-,-,C,es,auto,1" + "1,-,-,-,-,auto,1" + "1,-,-,lt_LT.UTF-8,-,auto,1" + "30,-,-,lt_LT.UTF-8,lt:uk:fr,auto,0") + string(REPLACE "," ";" languageAutoArguments "${languageAutoCase}") + add_test(NAME guest_system_service_language_auto_${languageAutoIndex} COMMAND guest_system_service_language_auto_tests ${languageAutoArguments}) + set_tests_properties(guest_system_service_language_auto_${languageAutoIndex} PROPERTIES TIMEOUT 20) + math(EXPR languageAutoIndex "${languageAutoIndex} + 1") + endforeach() + endif() add_test_executable(guest_system_service_time_zone_tests ${LIBS_INCLUDE_DIR}/tests/GuestSystemServiceTimeZone.cpp) target_include_directories(guest_system_service_time_zone_tests PRIVATE ${LIBS_INCLUDE_DIR}) target_link_libraries(guest_system_service_time_zone_tests PRIVATE libSceSystemService libc) diff --git a/core/libs/tests/GuestSystemServiceLanguageAuto.cpp b/core/libs/tests/GuestSystemServiceLanguageAuto.cpp new file mode 100644 index 0000000000..0889d916ec --- /dev/null +++ b/core/libs/tests/GuestSystemServiceLanguageAuto.cpp @@ -0,0 +1,95 @@ +#include "prx/libSceSystemService/include/SystemService.hpp" +#include "SceTypes.hpp" +#include "prx/libSceSystemService/SystemService.hpp" +#include +#include +#include +#include +#include +#include + +namespace { + +void Require(bool value, const char* what) { + if (value) return; + std::fprintf(stderr, "check failed: %s\n", what); + std::abort(); +} + +void SetVariable(const char* name, std::string_view value) { + if (value == "-") Require(::unsetenv(name) == 0, name); + else Require(::setenv(name, std::string(value).c_str(), 1) == 0, name); +} + +int Query(int paramId) { + int value = -1; + Require(sceSystemServiceParamGetInt(paramId, &value) == SYSTEM_SERVICE_OK, "query"); + return value; +} + +bool Rejected(const char* value, std::string& message) { + SetVariable("ANYPS5_LANGUAGE", value); + int language = -1; + try { + static_cast(sceSystemServiceParamGetInt(SYSTEM_SERVICE_PARAM_ID_LANG, &language)); + } catch (const std::runtime_error& error) { + message = error.what(); + return language == -1; + } + return false; +} + +std::string ReadAll(std::FILE* file) { + std::string text; + std::rewind(file); + for (int c = std::fgetc(file); c != EOF; c = std::fgetc(file)) text += static_cast(c); + return text; +} + +int CountNotices(const std::string& text) { + int count = 0; + for (std::size_t at = text.find("ANYPS5_LANGUAGE=auto:"); at != std::string::npos; at = text.find("ANYPS5_LANGUAGE=auto:", at + 1)) ++count; + return count; +} + +} + +int main(int argc, char** argv) { + Require(argc == 8, "arguments: expected LC_ALL LC_MESSAGES LANG LANGUAGE spelling notices"); + const int expected = std::atoi(argv[1]); + const int expectedNotices = std::atoi(argv[7]); + std::FILE* captured = std::tmpfile(); + Require(captured != nullptr && ::dup2(::fileno(captured), 2) == 2, "capture stderr"); + SetVariable("LC_ALL", argv[2]); + SetVariable("LC_MESSAGES", argv[3]); + SetVariable("LANG", argv[4]); + SetVariable("LANGUAGE", argv[5]); + SetVariable("ANYPS5_LANGUAGE", argv[6]); + Require(Query(SYSTEM_SERVICE_PARAM_ID_LANG) == expected, "parameter 1"); + Require(Query(SYSTEM_SERVICE_PARAM_ID_400) == expected, "parameter 400"); + Require(SystemServiceConsoleLanguage("test") == expected, "SystemServiceConsoleLanguage"); + SetVariable("LC_ALL", "ja_JP.UTF-8"); + SetVariable("LC_MESSAGES", "ja_JP.UTF-8"); + SetVariable("LANG", "ja_JP.UTF-8"); + SetVariable("LANGUAGE", "ja"); + Require(Query(SYSTEM_SERVICE_PARAM_ID_LANG) == expected, "the host locale is resolved once"); + Require(SystemServiceConsoleLanguage("test") == expected, "the host locale is resolved once for every caller"); + Require(CountNotices(ReadAll(captured)) == expectedNotices, "number of notices"); + SetVariable("ANYPS5_LANGUAGE", "17"); + Require(Query(SYSTEM_SERVICE_PARAM_ID_LANG) == 17, "an integer after auto"); + SetVariable("ANYPS5_LANGUAGE", "0"); + Require(Query(SYSTEM_SERVICE_PARAM_ID_400) == 0, "integer 0 after auto"); + SetVariable("ANYPS5_LANGUAGE", argv[6]); + Require(Query(SYSTEM_SERVICE_PARAM_ID_LANG) == expected, "auto again gives the same answer"); + SetVariable("ANYPS5_LANGUAGE", ""); + Require(Query(SYSTEM_SERVICE_PARAM_ID_LANG) == SYSTEM_SERVICE_PARAM_LANG_ENGLISH_US, "empty after auto"); + SetVariable("ANYPS5_LANGUAGE", "-"); + Require(Query(SYSTEM_SERVICE_PARAM_ID_LANG) == SYSTEM_SERVICE_PARAM_LANG_ENGLISH_US, "unset after auto"); + for (const char* value : {"auto ", " auto", "autoo", "aut", "a", "au to", "auto\n", "automatic", "-auto", "+auto", "31", "-1", "21a", "0x15", "arabic"}) { + std::string message; + Require(Rejected(value, message), value); + Require(message.find("ANYPS5_LANGUAGE") != std::string::npos, "the error names the variable"); + Require(message.find("0 to 30 or auto") != std::string::npos, "the error lists auto"); + } + Require(CountNotices(ReadAll(captured)) == expectedNotices, "no notice after the first resolution"); +} From 5bd0a09ab74dcb4e1969ca5f91710c43db3a2110 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:11:37 +0000 Subject: [PATCH 32/39] test(libSceUserService): the keyboard type follows ANYPS5_LANGUAGE=auto With auto and a German host locale the initial user reports the keyboard type of German, and changing the host locale afterwards does not change it: the user service sees the language the system service resolved once. --- core/libs/tests/GuestUserService.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index 9b8ce9d612..31290f317d 100644 --- a/core/libs/tests/GuestUserService.cpp +++ b/core/libs/tests/GuestUserService.cpp @@ -95,6 +95,16 @@ int main() { Require(sceUserServiceGetKeyboardType(123, &untouched) == USER_SERVICE_ERROR_UNKNOWN_USER && untouched == 0x5A5A); Require(sceUserServiceGetJapaneseInputType(USER_SERVICE_INITIAL_USER_ID, &untouched) == USER_SERVICE_OK && untouched == 0); } +#if defined(__linux__) + for (const char* name : {"LC_MESSAGES", "LANG", "LANGUAGE"}) Require(::unsetenv(name) == 0); + Require(::setenv("LC_ALL", "de_DE.UTF-8", 1) == 0); + SetLanguage("auto"); + std::int32_t german = 0x5A5A; + Require(sceUserServiceGetKeyboardType(USER_SERVICE_INITIAL_USER_ID, &german) == USER_SERVICE_OK && german == keyboardTypeByLanguage[4]); + Require(::setenv("LC_ALL", "ja_JP.UTF-8", 1) == 0); + german = 0x5A5A; + Require(sceUserServiceGetKeyboardType(USER_SERVICE_INITIAL_USER_ID, &german) == USER_SERVICE_OK && german == keyboardTypeByLanguage[4]); +#endif SetLanguage(nullptr); Require(color == 0); From 6a080f1a6d1bfce8531de5bb3c40911c62e70568 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:11:37 +0000 Subject: [PATCH 33/39] test(libSceConvertKeycode): the IME keyboard type follows ANYPS5_LANGUAGE=auto With auto and a German host locale sceConvertKeycodeGetImeKeyboardType gives the IME type of German, and a later change of the host locale does not change it. --- core/libs/tests/GuestConvertKeycode.cpp | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/libs/tests/GuestConvertKeycode.cpp b/core/libs/tests/GuestConvertKeycode.cpp index d2d3cb463f..842bcf0eed 100644 --- a/core/libs/tests/GuestConvertKeycode.cpp +++ b/core/libs/tests/GuestConvertKeycode.cpp @@ -166,6 +166,16 @@ int main() { } SetLanguage("30"); Require(sceConvertKeycodeGetImeKeyboardType(0x10000000, &type) == 0 && type == 4); +#if defined(__linux__) + for (const char* name : {"LC_MESSAGES", "LANG", "LANGUAGE"}) Require(::unsetenv(name) == 0); + Require(::setenv("LC_ALL", "de_DE.UTF-8", 1) == 0); + SetLanguage("auto"); + type = 0xFFFFFFFFu; + Require(sceConvertKeycodeGetImeKeyboardType(0x10000000, &type) == 0 && type == imeTypeByLanguage[4]); + Require(::setenv("LC_ALL", "ja_JP.UTF-8", 1) == 0); + type = 0xFFFFFFFFu; + Require(sceConvertKeycodeGetImeKeyboardType(0x10000000, &type) == 0 && type == imeTypeByLanguage[4]); +#endif SetLanguage(nullptr); Require(ImeKeyboardType::IsPs4TitleId("CUSA00000")); From b0eb4c292d1a083cd780aa9c6482f5cfde5a08dd Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:11:37 +0000 Subject: [PATCH 34/39] test(libSceNpTrophy2): every console language tag maps back to its language The trophy library keeps its own table from console language to locale tag. Each of its 31 tags must be recognised by the host-locale mapping and give the same console language, so the two tables cannot drift apart. --- core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp b/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp index 0e7b0e4364..122c2d7195 100644 --- a/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp +++ b/core/libs/prx/libSceNpTrophy2/tests/TrophyPackage.cpp @@ -1,6 +1,7 @@ #include "prx/libSceNpTrophy2/include/TitleTrophies.hpp" #include "prx/libSceNpTrophy2/include/TrophyPackage.hpp" #include "prx/libSceNpTrophy2/include/TrophyStore.hpp" +#include "prx/libSceSystemService/LanguageLocale.hpp" #include #include @@ -272,6 +273,10 @@ void testPackage() { "a tropconf.json of more than 1048576 values was read"); check(contains(failure([] { NpTrophy2::ParseTrophyPackage("not a package", "en-US"); }), "fewer than the 64-byte header"), "a package that is not a container was read"); check(NpTrophy2::LanguageTag(1) == "en-US" && NpTrophy2::LanguageTag(0) == "ja-JP" && NpTrophy2::LanguageTag(2) == "fr-FR" && NpTrophy2::LanguageTag(29) == "id-ID", "language tags are wrong"); + for (int language = 0; language <= 30; ++language) { + const auto match = SystemServiceLocale::MatchLocale(NpTrophy2::LanguageTag(language)); + check(match.recognised && match.id == language, "the trophy tag of a console language does not map back to it"); + } bool thrown = false; try { NpTrophy2::LanguageTag(31); From dc122e54dcd30fe6b9f0c2904d7afda0b15d5e97 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:11:48 +0000 Subject: [PATCH 35/39] docs(user): describe ANYPS5_LANGUAGE=auto in the usage guide States where the host language comes from on each system, which regional variants are kept, the fallback to English (United States) with its one stderr line, and that the host is read once per process. --- docs/user/USAGE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index 00ce1f877b..a6bf0522e7 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -301,7 +301,7 @@ Games that open the console's system font sets (`sceFontOpenFontSet`) need font ### Console language -Games ask for the console's language with `sceSystemServiceParamGetInt` and show their text in it when they have it. It is English (United States) unless `ANYPS5_LANGUAGE` is set to another console language number: 0 Japanese, 1 English (United States), 2 French, 3 Spanish, 4 German, 5 Italian, 6 Dutch, 7 Portuguese (Portugal), 8 Russian, 9 Korean, 10 Chinese (Traditional), 11 Chinese (Simplified), 12 Finnish, 13 Swedish, 14 Danish, 15 Norwegian, 16 Polish, 17 Portuguese (Brazil), 18 English (United Kingdom), 19 Turkish, 20 Spanish (Latin America), 21 Arabic, 22 French (Canada), 23 Czech, 24 Hungarian, 25 Greek, 26 Romanian, 27 Thai, 28 Vietnamese, 29 Indonesian, 30 Ukrainian. For example, `ANYPS5_LANGUAGE=21` selects Arabic. An empty value is the same as an unset one; any other value stops the game at its first language query with an error that names the variable. +Games ask for the console's language with `sceSystemServiceParamGetInt` and show their text in it when they have it. It is English (United States) unless `ANYPS5_LANGUAGE` is set to another console language number: 0 Japanese, 1 English (United States), 2 French, 3 Spanish, 4 German, 5 Italian, 6 Dutch, 7 Portuguese (Portugal), 8 Russian, 9 Korean, 10 Chinese (Traditional), 11 Chinese (Simplified), 12 Finnish, 13 Swedish, 14 Danish, 15 Norwegian, 16 Polish, 17 Portuguese (Brazil), 18 English (United Kingdom), 19 Turkish, 20 Spanish (Latin America), 21 Arabic, 22 French (Canada), 23 Czech, 24 Hungarian, 25 Greek, 26 Romanian, 27 Thai, 28 Vietnamese, 29 Indonesian, 30 Ukrainian. For example, `ANYPS5_LANGUAGE=21` selects Arabic. `ANYPS5_LANGUAGE=auto` (in any letter case) takes the language from the host operating system instead: the first of `LC_ALL`, `LC_MESSAGES` and `LANG` that is set and not empty on Linux, with the entries of `LANGUAGE` first when that locale is not `C` or `POSIX`, the user's locale name on Windows and the preferred language list on macOS. The regional variants the console has are kept (`en-GB` is English (United Kingdom), `fr-CA` French (Canada), `es-MX` Spanish (Latin America), `pt-PT` Portuguese (Portugal), `zh-TW`, `zh-HK` and `zh-Hant` Chinese (Traditional), `zh-CN` and `zh-Hans` Chinese (Simplified)), and a language without an entry for the region uses its main one. A host language the console has no entry for, and the `C` and `POSIX` locales, give English (United States) and print one line on stderr that names the locale. The host is read once per process, and every query then answers the same language. An empty value is the same as an unset one; any other value stops the game at its first language query with an error that names the variable. ### Trophies From 88c8565286542e26e8c9093cb301e5fed0daf748 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:12:23 +0000 Subject: [PATCH 36/39] docs(dev): list the auto value in the environment reference ANYPS5_LANGUAGE accepts auto besides the console language numbers; env_reference --check passes. --- docs/dev/ENVIRONMENT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index d3d2b90d19..88db31e284 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -551,7 +551,7 @@ The code reads environment names through `getenv`, `IsEnvironmentSet`, `kibSetti | Name | Kind | Source | Default | Description | |------|------|--------|---------|-------------| -| `ANYPS5_LANGUAGE` | integer | `core/libs/prx/libSceSystemService/SystemService.hpp` | 1 | console language number from 0 to 30 returned by sceSystemServiceParamGetInt, defaulting to English US; any other value throws | +| `ANYPS5_LANGUAGE` | integer | `core/libs/prx/libSceSystemService/SystemService.hpp` | 1 | console language number from 0 to 30 returned by sceSystemServiceParamGetInt, defaulting to English US; `auto` (any letter case) maps the host's locale to a console language once per process and falls back to English US with one stderr line; any other value throws | ### libSceUsbStorage From 18491e29199ffc7e8e94a3691a2b1050d97317eb Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:12:38 +0000 Subject: [PATCH 37/39] docs(debt): record that the auto language table is an unmeasured host-side convenience The mapping from a host locale to a console language is a choice of the project, not something measured on a console; what a title does with the resulting number is unmeasured as well. --- docs/dev/debt/libSceSystemService.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index baf22a6271..8dc3ba0e55 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -28,6 +28,7 @@ - [sceSystemServiceLaunchWebBrowser](../../../core/libs/prx/libSceSystemService/Export.cpp): the native export takes one URI argument. Null input, the 6015-byte length limit and supported scheme checks are recovered. The host still returns kernel `EOPNOTSUPP` for valid input; a missing browser service's native error is unresolved. - [sceSystemServiceParamGetString](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name. As in a payload process on a PS5 (firmware 13.60), the name is written NUL-padded to the whole buffer, and a buffer under 65 bytes, a null buffer or any other parameter returns `SYSTEM_SERVICE_ERROR_PARAMETER` without writing. Parameters 0 to 40, 100 to 110, 1000 to 1010, -1 and `INT_MAX` were tried there; the others are assumed to fail the same way. PPSA26344 at `0xA87935` passes parameter 6 with a 65-byte buffer - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Parameter 400 returns the same number as parameter 1: PPSA01325 asks for 400 first, asks for 1 when that fails, and uses either answer as an index into the same 31 languages; in the PS5 10.01 and 12.60 `libSceSystemService` modules parameter 400 reads the same registry value as parameter 1, without parameter 1's mapping of newer language numbers by SDK version, and returns 0x80a10003 when an imported check that is not identified returns 1; the 4.03 module has no parameter 400. Neither the mapping nor that error is modelled, and the parameter's name is not known. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format and time format stay fixed +- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - `ANYPS5_LANGUAGE=auto` takes the console language from the host locale with the table in [LanguageLocale.hpp](../../../core/libs/prx/libSceSystemService/LanguageLocale.hpp). The table is a host-side convenience: nothing was measured on a console about which language a person of a given host locale would choose, and what a title does with the number is unmeasured too. The language numbers and names are those of the entry above ([OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) for 0 to 29). - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - parameters 1 to 5, 100 to 111, 200 to 213, 300 and 400 answer (see the integer system parameters entry); every other parameter, and a null value pointer, returns 0x80A10003 without writing the value. Inside a retail title on a PS5 (firmware 13.40, measured 2026-10-19 through ptrace remote calls in the game process) parameters 0, 6, 7 (game parental level), 8 to 20, 401, 999, 1000 (enter button assignment) and 1001 to 1008 returned 0x80A10003, as did a null value pointer; 1 to 5 and 400 answered, and 100 answered the value 0. A sweep of parameters 0 to 2048 in the same way on 2026-10-23 also found 101 to 111, 200 to 213 and 300 answering, each writing 4 bytes; every other parameter, and a null value pointer for 100 to 111, returned 0x80A10003 and left the value as it was. One title (PPSA13356, call at 0xAF350) asks for parameter 1 and does not read the result. - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the time zone (parameter 4) and summer time (parameter 5) follow the host's local time zone at the moment of the call. Titles compute the local time as UTC plus the time zone in minutes plus 60 minutes when summer time is 1 ([Apollo](https://github.com/bucanero/apollo-ps4/blob/252a9d5a187c463994263009851a179f4f910c49/source/exec_cmd.c#L70-L92), [GoldHEN's frame logger](https://github.com/GoldHEN/GoldHEN_Plugins_Repository/blob/6c3a06f53d9caebadfb0aeed9bb94119012010a7/plugin_src/frame_logger/source/main.c#L60-L82), and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/413d2e3518fb435f40971d238fe0ecae7ee551da/src/SharpProspero/Platform/SystemParameters.cs#L75-L79) calls them the offset from UTC in minutes and whether summer time is in effect), so the time zone is the host's current offset east of UTC less the summer time shift while summer time is in effect: 60 minutes on Linux, and on Windows the C runtime's own summer time bias (`_get_dstbias`), which is also what shifted its local time. For a host zone whose summer time is not one hour, Linux keeps the sum right and the standard offset wrong, and Windows the opposite; how the console reports such a zone is unknown. libkernel's `sceKernelConvertUtcToLocaltime`, `sceKernelConvertLocaltimeToUtc` and `sceKernelGettimezone` still use UTC - [sceSystemServiceInitializePlayerDialogParam](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - a null param throws: what the console does with it is not measured. The console run in [prosper](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/tests/data/console_oracle/nethttp.golden.tsv#L46) shows only the 0x40 bytes written (size 0x40, user ID -1 at 0xC, zeros), so the field names and the mode and target account fields follow [SharpProspero's struct](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L637-L655), and the `void` return [its declaration](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L419-L426); the console left 0x800693020 in `rax`, not an SCE error code. Closes when: the null case is measured on a PS5 From 86270241ca96c6098073ad3c24f3746f5c41e263 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:12:39 +0000 Subject: [PATCH 38/39] docs(debt): record the unmeasured regional choices of ANYPS5_LANGUAGE=auto Bare pt and es, the English variants, bare zh and the Latin American and Portuguese-speaking regions are decisions, and languages the console has no entry for fall back to English (United States) instead of a neighbour. --- docs/dev/debt/libSceSystemService.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 8dc3ba0e55..147471153e 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -29,6 +29,7 @@ - [sceSystemServiceParamGetString](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the system name (parameter 6) is the fixed `PS5`, not the console's name. As in a payload process on a PS5 (firmware 13.60), the name is written NUL-padded to the whole buffer, and a buffer under 65 bytes, a null buffer or any other parameter returns `SYSTEM_SERVICE_ERROR_PARAMETER` without writing. Parameters 0 to 40, 100 to 110, 1000 to 1010, -1 and `INT_MAX` were tried there; the others are assumed to fail the same way. PPSA26344 at `0xA87935` passes parameter 6 with a 65-byte buffer - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Parameter 400 returns the same number as parameter 1: PPSA01325 asks for 400 first, asks for 1 when that fails, and uses either answer as an index into the same 31 languages; in the PS5 10.01 and 12.60 `libSceSystemService` modules parameter 400 reads the same registry value as parameter 1, without parameter 1's mapping of newer language numbers by SDK version, and returns 0x80a10003 when an imported check that is not identified returns 1; the 4.03 module has no parameter 400. Neither the mapping nor that error is modelled, and the parameter's name is not known. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format and time format stay fixed - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - `ANYPS5_LANGUAGE=auto` takes the console language from the host locale with the table in [LanguageLocale.hpp](../../../core/libs/prx/libSceSystemService/LanguageLocale.hpp). The table is a host-side convenience: nothing was measured on a console about which language a person of a given host locale would choose, and what a title does with the number is unmeasured too. The language numbers and names are those of the entry above ([OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) for 0 to 29). +- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - `auto` chooses where the console has no exact variant, and none of these choices is measured: a bare `pt` is Portuguese (Brazil) and a bare `es` Spanish (Spain); `en-GB`, `en-IE`, `en-AU` and `en-NZ` are English (United Kingdom) and every other English is English (United States); a bare `zh` is Chinese (Simplified) and `zh-TW`, `zh-HK`, `zh-MO` and the Hant script Traditional; `es-US` and the Latin American countries are Spanish (Latin America); the Portuguese-speaking territories outside Brazil are Portuguese (Portugal). A language the console has no entry for (Baltic and the other Slavic languages, Hebrew, Hindi, Catalan, Basque and Galician) gives English (United States) and is not mapped to a neighbouring language. - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - parameters 1 to 5, 100 to 111, 200 to 213, 300 and 400 answer (see the integer system parameters entry); every other parameter, and a null value pointer, returns 0x80A10003 without writing the value. Inside a retail title on a PS5 (firmware 13.40, measured 2026-10-19 through ptrace remote calls in the game process) parameters 0, 6, 7 (game parental level), 8 to 20, 401, 999, 1000 (enter button assignment) and 1001 to 1008 returned 0x80A10003, as did a null value pointer; 1 to 5 and 400 answered, and 100 answered the value 0. A sweep of parameters 0 to 2048 in the same way on 2026-10-23 also found 101 to 111, 200 to 213 and 300 answering, each writing 4 bytes; every other parameter, and a null value pointer for 100 to 111, returned 0x80A10003 and left the value as it was. One title (PPSA13356, call at 0xAF350) asks for parameter 1 and does not read the result. - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the time zone (parameter 4) and summer time (parameter 5) follow the host's local time zone at the moment of the call. Titles compute the local time as UTC plus the time zone in minutes plus 60 minutes when summer time is 1 ([Apollo](https://github.com/bucanero/apollo-ps4/blob/252a9d5a187c463994263009851a179f4f910c49/source/exec_cmd.c#L70-L92), [GoldHEN's frame logger](https://github.com/GoldHEN/GoldHEN_Plugins_Repository/blob/6c3a06f53d9caebadfb0aeed9bb94119012010a7/plugin_src/frame_logger/source/main.c#L60-L82), and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/413d2e3518fb435f40971d238fe0ecae7ee551da/src/SharpProspero/Platform/SystemParameters.cs#L75-L79) calls them the offset from UTC in minutes and whether summer time is in effect), so the time zone is the host's current offset east of UTC less the summer time shift while summer time is in effect: 60 minutes on Linux, and on Windows the C runtime's own summer time bias (`_get_dstbias`), which is also what shifted its local time. For a host zone whose summer time is not one hour, Linux keeps the sum right and the standard offset wrong, and Windows the opposite; how the console reports such a zone is unknown. libkernel's `sceKernelConvertUtcToLocaltime`, `sceKernelConvertLocaltimeToUtc` and `sceKernelGettimezone` still use UTC - [sceSystemServiceInitializePlayerDialogParam](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - a null param throws: what the console does with it is not measured. The console run in [prosper](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/tests/data/console_oracle/nethttp.golden.tsv#L46) shows only the 0x40 bytes written (size 0x40, user ID -1 at 0xC, zeros), so the field names and the mode and target account fields follow [SharpProspero's struct](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L637-L655), and the `void` return [its declaration](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L419-L426); the console left 0x800693020 in `rax`, not an SCE error code. Closes when: the null case is measured on a PS5 From 84fd495ba9cc260af93dbd78b0ee1650b4240c76 Mon Sep 17 00:00:00 2001 From: MarcZX10 <67475268+MarcZX10@users.noreply.github.com> Date: Thu, 29 Oct 2026 07:12:39 +0000 Subject: [PATCH 39/39] docs(debt): record the host language sources of ANYPS5_LANGUAGE=auto as run on Linux only Only the Linux environment source has run; the Windows and macOS sources are not built or run, and a second copy of the inline resolver in another DLL can print its own fallback line. --- docs/dev/debt/libSceSystemService.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index 147471153e..9229b02af5 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -30,6 +30,7 @@ - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - console language numbers 0 to 29 follow the PS4 SDK enum in [OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) (21 Arabic). 30 (Ukrainian) is not in it and follows PPSA99505 ([Jelly5](https://github.com/02dnot/Jelly5/blob/552b866d89b2a58b27f7f7b0484dc743ce235526/app/src/app/i18n.cpp#L29-L60)) and PPSA99002 ([ProsperoLight](https://github.com/blackbearreloaded/ProsperoLight/blob/b708bbde3457bef1073a9ab6959957cb691e8e2f/include/i18n.hpp#L252-L309)), whose numbers agree with the enum. Not confirmed on a console, and neither title starts on `main` yet. Parameter 400 returns the same number as parameter 1: PPSA01325 asks for 400 first, asks for 1 when that fails, and uses either answer as an index into the same 31 languages; in the PS5 10.01 and 12.60 `libSceSystemService` modules parameter 400 reads the same registry value as parameter 1, without parameter 1's mapping of newer language numbers by SDK version, and returns 0x80a10003 when an imported check that is not identified returns 1; the 4.03 module has no parameter 400. Neither the mapping nor that error is modelled, and the parameter's name is not known. Without `ANYPS5_LANGUAGE` the language is English (United States); the date format and time format stay fixed - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - `ANYPS5_LANGUAGE=auto` takes the console language from the host locale with the table in [LanguageLocale.hpp](../../../core/libs/prx/libSceSystemService/LanguageLocale.hpp). The table is a host-side convenience: nothing was measured on a console about which language a person of a given host locale would choose, and what a title does with the number is unmeasured too. The language numbers and names are those of the entry above ([OpenOrbis](https://github.com/OpenOrbis/OpenOrbis-PS4-Toolchain/blob/06fcf7fd108fa080035cdd3259ed22eb07e0508c/include/orbis/_types/sys_service.h#L54-L83) for 0 to 29). - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - `auto` chooses where the console has no exact variant, and none of these choices is measured: a bare `pt` is Portuguese (Brazil) and a bare `es` Spanish (Spain); `en-GB`, `en-IE`, `en-AU` and `en-NZ` are English (United Kingdom) and every other English is English (United States); a bare `zh` is Chinese (Simplified) and `zh-TW`, `zh-HK`, `zh-MO` and the Hant script Traditional; `es-US` and the Latin American countries are Spanish (Latin America); the Portuguese-speaking territories outside Brazil are Portuguese (Portugal). A language the console has no entry for (Baltic and the other Slavic languages, Hebrew, Hindi, Catalan, Basque and Galician) gives English (United States) and is not mapped to a neighbouring language. +- [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - `auto` reads the host language through `LC_ALL`, `LC_MESSAGES`, `LANG` and `LANGUAGE` on Linux, `GetUserDefaultLocaleName` on Windows (the user's regional-format locale, not the list of display languages) and `CFLocaleCopyPreferredLanguages` on macOS. Only the Linux source has run, with injected and real environments and no non-English host; the Windows and macOS sources are not built or run. The resolver is inline, so a library that carries its own copy (a separate DLL on Windows) resolves the host again and can print its own fallback line; its answer is the same. - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - parameters 1 to 5, 100 to 111, 200 to 213, 300 and 400 answer (see the integer system parameters entry); every other parameter, and a null value pointer, returns 0x80A10003 without writing the value. Inside a retail title on a PS5 (firmware 13.40, measured 2026-10-19 through ptrace remote calls in the game process) parameters 0, 6, 7 (game parental level), 8 to 20, 401, 999, 1000 (enter button assignment) and 1001 to 1008 returned 0x80A10003, as did a null value pointer; 1 to 5 and 400 answered, and 100 answered the value 0. A sweep of parameters 0 to 2048 in the same way on 2026-10-23 also found 101 to 111, 200 to 213 and 300 answering, each writing 4 bytes; every other parameter, and a null value pointer for 100 to 111, returned 0x80A10003 and left the value as it was. One title (PPSA13356, call at 0xAF350) asks for parameter 1 and does not read the result. - [sceSystemServiceParamGetInt](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - the time zone (parameter 4) and summer time (parameter 5) follow the host's local time zone at the moment of the call. Titles compute the local time as UTC plus the time zone in minutes plus 60 minutes when summer time is 1 ([Apollo](https://github.com/bucanero/apollo-ps4/blob/252a9d5a187c463994263009851a179f4f910c49/source/exec_cmd.c#L70-L92), [GoldHEN's frame logger](https://github.com/GoldHEN/GoldHEN_Plugins_Repository/blob/6c3a06f53d9caebadfb0aeed9bb94119012010a7/plugin_src/frame_logger/source/main.c#L60-L82), and [SharpProspero](https://github.com/SvenGDK/SharpProspero/blob/413d2e3518fb435f40971d238fe0ecae7ee551da/src/SharpProspero/Platform/SystemParameters.cs#L75-L79) calls them the offset from UTC in minutes and whether summer time is in effect), so the time zone is the host's current offset east of UTC less the summer time shift while summer time is in effect: 60 minutes on Linux, and on Windows the C runtime's own summer time bias (`_get_dstbias`), which is also what shifted its local time. For a host zone whose summer time is not one hour, Linux keeps the sum right and the standard offset wrong, and Windows the opposite; how the console reports such a zone is unknown. libkernel's `sceKernelConvertUtcToLocaltime`, `sceKernelConvertLocaltimeToUtc` and `sceKernelGettimezone` still use UTC - [sceSystemServiceInitializePlayerDialogParam](../../../core/libs/prx/libSceSystemService/Export.cpp) (libSceSystemService) - a null param throws: what the console does with it is not measured. The console run in [prosper](https://github.com/mattias800/prosper/blob/40d95db32efe1ad00106a5b9a84e018fffa9ecff/prosper/tests/data/console_oracle/nethttp.golden.tsv#L46) shows only the 0x40 bytes written (size 0x40, user ID -1 at 0xC, zeros), so the field names and the mode and target account fields follow [SharpProspero's struct](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L637-L655), and the `void` return [its declaration](https://github.com/SvenGDK/SharpProspero/blob/5dfdb4b0b166e83dc9ae275001afb695f12c7174/src/SharpProspero/Interop/SystemService/SystemService.cs#L419-L426); the console left 0x800693020 in `rax`, not an SCE error code. Closes when: the null case is measured on a PS5