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/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); diff --git a/core/libs/prx/libSceSystemService/CMakeLists.txt b/core/libs/prx/libSceSystemService/CMakeLists.txt index 2c9a9034b8..dd55e9a025 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) @@ -101,6 +106,38 @@ 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}) + 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) + 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/prx/libSceSystemService/LanguageLocale.hpp b/core/libs/prx/libSceSystemService/LanguageLocale.hpp new file mode 100644 index 0000000000..286573b07a --- /dev/null +++ b/core/libs/prx/libSceSystemService/LanguageLocale.hpp @@ -0,0 +1,378 @@ +#ifndef CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_LANGUAGELOCALE_HPP +#define CORE_LIBS_PRX_LIBSCESYSTEMSERVICE_LANGUAGELOCALE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +#if defined(_WIN32) +#ifndef WIN32_LEAN_AND_MEAN +#define WIN32_LEAN_AND_MEAN +#endif +#ifndef NOMINMAX +#define NOMINMAX +#endif +#include +#elif defined(__APPLE__) +#include +#endif + +namespace SystemServiceLocale { + +struct Tag { + std::string language; + std::string script; + 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'); } +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); } + +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 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 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 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 Row JapaneseKoreanRows[] = { + {"ja", "", "", Japanese}, + {"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 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, +}; + +} + +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; +} + +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; +} + +inline bool ParseLocale(std::string_view text, Tag& tag) { + const std::string normalised = NormalizePosixLocale(text); + 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; +} + +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}; +} + +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; +} + +#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 + +#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 + +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 +} + +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 diff --git a/core/libs/prx/libSceSystemService/SystemService.hpp b/core/libs/prx/libSceSystemService/SystemService.hpp index dc9d45e4d9..af106ea7dc 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,16 +44,23 @@ 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) { 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; } diff --git a/core/libs/tests/GuestConvertKeycode.cpp b/core/libs/tests/GuestConvertKeycode.cpp index 70ba11927f..6e29956c43 100644 --- a/core/libs/tests/GuestConvertKeycode.cpp +++ b/core/libs/tests/GuestConvertKeycode.cpp @@ -195,6 +195,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")); 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"); +} diff --git a/core/libs/tests/GuestUserService.cpp b/core/libs/tests/GuestUserService.cpp index 39de9a2f70..16e6cb2bbc 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); diff --git a/core/libs/tests/SystemServiceLanguageLocale.cpp b/core/libs/tests/SystemServiceLanguageLocale.cpp new file mode 100644 index 0000000000..257556e9cd --- /dev/null +++ b/core/libs/tests/SystemServiceLanguageLocale.cpp @@ -0,0 +1,307 @@ +#include "prx/libSceSystemService/LanguageLocale.hpp" +#include +#include +#include +#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); + } +} + +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); + } +} + +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); + } +} + +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); +} + +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); +} + +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); +} + +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); +} + +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); + } +} + +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); +} + +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"); +} + +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 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() { + RequireBcp47Parsing(); + RequirePosixNormalisation(); + RequireEitherForm(); + RequireLatinEuropeanRows(); + RequireIberianRows(); + RequireChineseRows(); + RequireJapaneseKoreanRows(); + RequireSlavicNordicRows(); + RequireOtherRows(); + RequireFallbackRules(); + RequirePosixSource(); + RequireHostResolution(); +} diff --git a/docs/dev/ENVIRONMENT.md b/docs/dev/ENVIRONMENT.md index 01c0ab6afd..6148495f6b 100644 --- a/docs/dev/ENVIRONMENT.md +++ b/docs/dev/ENVIRONMENT.md @@ -587,7 +587,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 diff --git a/docs/dev/debt/libSceSystemService.md b/docs/dev/debt/libSceSystemService.md index e88fdc2800..98e0f4a8c9 100644 --- a/docs/dev/debt/libSceSystemService.md +++ b/docs/dev/debt/libSceSystemService.md @@ -26,6 +26,9 @@ - [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) - `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 diff --git a/docs/user/USAGE.md b/docs/user/USAGE.md index e7ba0424bc..8153ce079b 100644 --- a/docs/user/USAGE.md +++ b/docs/user/USAGE.md @@ -364,7 +364,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. ### Save data free space