// SPDX-License-Identifier: MIT // // ES-DE Frontend // LocalizationUtil.cpp // // Localization functions. // Provides support for translations using gettext/libintl. // #include "utils/LocalizationUtil.h" #include "Log.h" #include "Settings.h" #include "resources/ResourceManager.h" #include "utils/StringUtil.h" #include <SDL2/SDL_locale.h> #include <algorithm> #include <iostream> #if defined(_WIN64) #include <Windows.h> #endif namespace Utils { namespace Localization { // clang-format off // When adding a new locale, then make sure to also update ThemeData::sSupportedLanguages. const std::vector<std::pair<std::string, std::string>> sSupportedLocales {{{"en"}, {"US"}}, {{"en"}, {"GB"}}, {{"ca"}, {"ES"}}, {{"de"}, {"DE"}}, {{"es"}, {"ES"}}, {{"fr"}, {"FR"}}, {{"it"}, {"IT"}}, {{"nl"}, {"NL"}}, {{"pl"}, {"PL"}}, {{"pt"}, {"BR"}}, {{"ro"}, {"RO"}}, {{"ru"}, {"RU"}}, {{"sv"}, {"SE"}}, {{"ja"}, {"JP"}}, {{"ko"}, {"KR"}}, {{"zh"}, {"CN"}}}; // clang-format on std::string sCurrentLocale {"en_US"}; float sMenuTitleScaleFactor {1.0f}; const char* pgettextBuiltin(const char* msgctxt, const char* msgid) { // This is an unbelievable hack but it's actually done pretty much the same way in // the gettext.h header where a macro is used to wrap around the libintl functionality. // Why this function is simply not part of libintl itself is anyone's guess, as that // would be the logical thing to do. std::string lookup; lookup.append(msgctxt).append("\004").append(msgid); const char* translation {gettext(lookup.c_str())}; if (translation == lookup.c_str()) return msgid; else return translation; } const char* npgettextBuiltin(const char* msgctxt, const char* msgid1, const char* msgid2, unsigned long int n) { std::string lookup; lookup.append(msgctxt).append("\004").append(msgid1); const char* translation {ngettext(lookup.c_str(), msgid2, n)}; if (translation == lookup.c_str()) return msgid1; else return translation; } std::pair<std::string, std::string> getLocale() { #if defined(_WIN64) std::wstring localeNameWide(LOCALE_NAME_MAX_LENGTH, '\0'); if (GetUserDefaultLocaleName(&localeNameWide[0], LOCALE_NAME_MAX_LENGTH) == 0) return std::make_pair("en", "US"); std::string localeName {Utils::String::wideStringToString(localeNameWide)}; localeName.erase(localeName.find('\0')); // This should never happen, but who knows with Windows. if (localeName.empty()) return std::make_pair("en", "US"); std::vector<std::string> localeVector; // Of course Windows doesn't follow standards and names locales with dashes // instead of underscores, such as "sv-SE" instead of "sv_SE". But who knows // if this is consistent, so we check for underscores as an extra precaution. if (localeName.find("_") != std::string::npos) localeVector = Utils::String::delimitedStringToVector(localeName, "_"); else localeVector = Utils::String::delimitedStringToVector(localeName, "-"); if (localeVector.size() == 1) return std::make_pair(localeVector[0], ""); else return std::make_pair(localeVector[0], localeVector[1]); #else // SDL_GetPreferredLocales() does not seem to always return accurate results // on Windows but for all other operating systems we use it. SDL_Locale* preferredLocales {SDL_GetPreferredLocales()}; if (preferredLocales == nullptr) return std::make_pair("en", "US"); std::string language {preferredLocales->language}; std::string country; if (preferredLocales->country != nullptr) country = preferredLocales->country; SDL_free(preferredLocales); return std::make_pair(language, country); #endif } void setLocale() { // Only detect locale once (on application startup). if (Settings::getInstance()->getString("DetectedLocale") == "") { const std::pair<std::string, std::string> detectedLocale {getLocale()}; if (detectedLocale.second == "") Settings::getInstance()->setString("DetectedLocale", detectedLocale.first); else { Settings::getInstance()->setString( "DetectedLocale", detectedLocale.first + "_" + detectedLocale.second); } } sMenuTitleScaleFactor = 1.0f; sCurrentLocale = "en_US"; std::string languageSetting {Settings::getInstance()->getString("ApplicationLanguage")}; std::vector<std::string> localeVector; std::pair<std::string, std::string> localePair; if (languageSetting == "automatic") { localeVector = Utils::String::delimitedStringToVector( Settings::getInstance()->getString("DetectedLocale"), "_"); } else { localeVector = Utils::String::delimitedStringToVector(languageSetting, "_"); } if (localeVector.size() == 1) localePair = std::make_pair(localeVector[0], ""); else localePair = std::make_pair(localeVector[0], localeVector[1]); std::string locale; std::string localePairCombined; if (localePair.second == "") localePairCombined = localePair.first; else localePairCombined = localePair.first + "_" + localePair.second; if (std::find(sSupportedLocales.cbegin(), sSupportedLocales.cend(), localePair) != sSupportedLocales.cend()) { locale = localePairCombined; LOG(LogInfo) << "Application language set to \"" << locale << "\""; } else { for (auto& localeEntry : sSupportedLocales) { if (localeEntry.first == localePair.first) { LOG(LogInfo) << "No support for language \"" << localePairCombined << "\", falling back to closest match \"" << localeEntry.first + "_" + localeEntry.second << "\""; locale = localeEntry.first + "_" + localeEntry.second; break; } } } if (locale == "") { LOG(LogInfo) << "No support for language \"" << localePairCombined << "\", falling back to default \"en_US\""; locale = "en_US"; } // Language-specific menu title scale factor. if (localePair.first == "ca") sMenuTitleScaleFactor = 0.92f; else if (localePair.first == "de") sMenuTitleScaleFactor = 0.92f; else if (localePair.first == "es") sMenuTitleScaleFactor = 0.90f; else if (localePair.first == "fr") sMenuTitleScaleFactor = 0.90f; else if (localePair.first == "it") sMenuTitleScaleFactor = 0.94f; else if (localePair.first == "nl") sMenuTitleScaleFactor = 0.94f; else if (localePair.first == "pl") sMenuTitleScaleFactor = 0.94f; else if (localePair.first == "pt") sMenuTitleScaleFactor = 0.90f; else if (localePair.first == "ro") sMenuTitleScaleFactor = 0.94f; else if (localePair.first == "ru") sMenuTitleScaleFactor = 0.94f; else if (localePair.first == "sv") sMenuTitleScaleFactor = 0.87f; else if (localePair.first == "ja") sMenuTitleScaleFactor = 0.94f; else if (localePair.first == "ko") sMenuTitleScaleFactor = 0.96f; else if (localePair.first == "zh") sMenuTitleScaleFactor = 0.94f; std::string localePath; localePath.append("/") .append(locale) .append("/LC_MESSAGES/") .append(locale) .append(".mo"); // If the message catalog file is not found then an emergency shutdown will be // initiated by ResourceManager. std::string objectPath { ResourceManager::getInstance().getResourcePath(":/locale" + localePath)}; // This makes it possible to override the message catalog with a file in the // application data directory. if (objectPath.length() > localePath.length()) objectPath = objectPath.substr(0, objectPath.length() - localePath.length()); #if defined(_WIN64) _configthreadlocale(_DISABLE_PER_THREAD_LOCALE); const LCID localeID {LocaleNameToLCID(Utils::String::stringToWideString(locale).c_str(), LOCALE_ALLOW_NEUTRAL_NAMES)}; SetThreadLocale(localeID); #else setenv("LANGUAGE", locale.c_str(), 1); setenv("LANG", locale.c_str(), 1); // For some bizarre reason we need to first set the locale to en_US.UTF-8 before // we set it to the requested locale as some specific locales like pt_BR and zh_CN // otherwise won't work consistently. This must be some kind of library or OS bug as // it only happens on regular Linux, and not on macOS, Windows, Android or FreeBSD. setlocale(LC_MESSAGES, std::string {"en_US.UTF-8"}.c_str()); setlocale(LC_MESSAGES, std::string {locale + ".UTF-8"}.c_str()); #endif textdomain(locale.c_str()); bindtextdomain(locale.c_str(), objectPath.c_str()); bind_textdomain_codeset(locale.c_str(), "UTF-8"); sCurrentLocale = locale; } } // namespace Localization } // namespace Utils