From ab95a29697ac00c3a68655f5a3051e2e3f10817b Mon Sep 17 00:00:00 2001 From: Andrew James Date: Sat, 1 Jan 2022 19:31:48 +1100 Subject: [PATCH] Get user preferred languages on apple devices (#3875) Co-authored-by: Bubio --- CMakeLists.txt | 6 +++ Source/platform/locale.cpp | 79 ++++++++++++++++++++++++++++++-------- Source/platform/locale.hpp | 4 ++ 3 files changed, 73 insertions(+), 16 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b31e4a05..c5401b811 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -275,6 +275,12 @@ if(APPLE) COMPONENT Runtime) endif() + find_library(COREFOUNDATION_LIBRARY CoreFoundation) + if(COREFOUNDATION_LIBRARY) + target_link_libraries(libdevilutionx PUBLIC "${COREFOUNDATION_LIBRARY}") + target_compile_definitions(libdevilutionx PRIVATE USE_COREFOUNDATION) + endif() + set(MACOSX_BUNDLE_LONG_VERSION_STRING "Version ${PROJECT_VERSION}") set(CPACK On) endif() diff --git a/Source/platform/locale.cpp b/Source/platform/locale.cpp index 7179b8c5d..1eb98b68f 100644 --- a/Source/platform/locale.cpp +++ b/Source/platform/locale.cpp @@ -15,13 +15,42 @@ #include #include // clang-format on - -#include "utils/stdcompat/algorithm.hpp" +#elif defined(__APPLE__) and defined(USE_COREFOUNDATION) +#include #else #include #endif +#include "utils/stdcompat/algorithm.hpp" +#include "utils/stdcompat/string_view.hpp" + namespace devilution { +namespace { + +std::string IetfToPosix(string_view langCode) +{ + /* + * Handle special case for simplified/traditional Chinese. IETF/BCP-47 specifies that only the script should be + * used to discriminate languages when the region doesn't add additional value. For chinese scripts zh-Hans is + * preferred over zh-Hans-CN (but platforms may include the region identifier anyway). POSIX locales don't use + * script in the same way so we need to convert these back to lang_region. + */ + if (langCode.substr(0, 7) == "zh-Hans") { + return "zh_CN"; + } + if (langCode.substr(0, 7) == "zh-Hant") { + return "zh_TW"; + } + + std::string posixLangCode { langCode }; + + // if a region is included in the locale do a simple transformation to the expected POSIX style. + std::replace(posixLangCode.begin(), posixLangCode.end(), '-', '_'); + + return posixLangCode; +} + +} // namespace std::vector GetLocales() { @@ -77,27 +106,26 @@ std::vector GetLocales() #elif defined(_WIN32) #if WINVER >= 0x0600 auto wideCharToUtf8 = [](PWSTR wideString) { - char utf8Buffer[12] {}; - // We only handle 5 character locales (lang2-region2), so don't bother reading past that. This does leave the - // resulting string unterminated but the buffer was zero initialised anyway. - WideCharToMultiByte(CP_UTF8, 0, wideString, 5, utf8Buffer, sizeof(utf8Buffer), nullptr, nullptr); - - // GetUserDefaultLocaleName could return an ISO 639-2/T string (three letter language code) or even an - // arbitrary custom locale, however we only handle 639-1 (two letter language code) locale names when checking - // the fallback language. - // if a region is included in the locale do a simple transformation to the expected POSIX style. - std::replace(utf8Buffer, utf8Buffer + sizeof(utf8Buffer), '-', '_'); - return std::move(std::string(utf8Buffer)); + // WideCharToMultiByte potentially leaves the buffer unterminated, default initialise here as a workaround + char utf8Buffer[16] {}; + + // Fetching up to 10 characters to allow for script tags + WideCharToMultiByte(CP_UTF8, 0, wideString, 10, utf8Buffer, sizeof(utf8Buffer), nullptr, nullptr); + + // Windows NLS functions return IETF/BCP-47 locale codes (or potentially arbitrary custom locales) but devX + // uses posix format codes, return the expected format + return std::move(IetfToPosix(utf8Buffer)); }; + WCHAR localeBuffer[LOCALE_NAME_MAX_LENGTH]; - if (GetUserDefaultLocaleName(localeBuffer, LOCALE_NAME_MAX_LENGTH) != 0) { + if (GetUserDefaultLocaleName(localeBuffer, sizeof(localeBuffer)) != 0) { // Found a user default locale convert from WIN32's default of UTF16 to UTF8 and add to the list locales.push_back(wideCharToUtf8(localeBuffer)); } ULONG numberOfLanguages; - ULONG bufferSize = LOCALE_NAME_MAX_LENGTH; + ULONG bufferSize = sizeof(localeBuffer); GetUserPreferredUILanguages(MUI_LANGUAGE_NAME, &numberOfLanguages, localeBuffer, &bufferSize); PWSTR bufferOffset = localeBuffer; @@ -123,8 +151,27 @@ std::vector GetLocales() locales.push_back(std::move(locale)); } #endif +#elif defined(__APPLE__) and defined(USE_COREFOUNDATION) + // Get the user's language list (in order of preference) + CFArrayRef languages = CFLocaleCopyPreferredLanguages(); + CFIndex numLanguages = CFArrayGetCount(languages); + + for (CFIndex i = 0; i < numLanguages; i++) { + auto language = static_cast(CFArrayGetValueAtIndex(languages, i)); + + char buffer[16]; + + if (CFStringGetCString(language, buffer, sizeof(buffer), kCFStringEncodingUTF8)) { + // Convert to the posix format expected by callers + locales.push_back(IetfToPosix(buffer)); + } + } + + CFRelease(languages); #else - locales.emplace_back(std::locale("").name().substr(0, 5)); + std::string locale = std::locale("").name(); + // strip off any encoding specifier, devX uses UTF8. + locales.emplace_back(locale.substr(0, locale.find('.'))); #endif return locales; } diff --git a/Source/platform/locale.hpp b/Source/platform/locale.hpp index ed744451c..7ee797928 100644 --- a/Source/platform/locale.hpp +++ b/Source/platform/locale.hpp @@ -5,6 +5,10 @@ namespace devilution { +/** + * @brief Returns a list of preferred languages based on user/system configuration. + * @return 0 or more POSIX locale codes (language code with optional region identifier) + */ std::vector GetLocales(); } // namespace devilution