diff --git a/docs/changes.txt b/docs/changes.txt index fab00ba096..fdbc5cb67f 100644 --- a/docs/changes.txt +++ b/docs/changes.txt @@ -63,6 +63,12 @@ Changes in behaviour not resulting in compilation errors mode under MSW, use the new AreAppsDark() or IsSystemDark() to check if the other applications or the system are using dark mode. +- wxUILocale::IsSupported() now returns false for unavailable locales under + Unix systems without trying to fall back on another locale using the same + language in a different region, e.g. it doesn't use fr_FR if fr_BE is not + available. If any locale using the given language is acceptable, the region + must be left empty, e.g. just "fr" would use any available "fr_XX". + Changes in behaviour which may result in build errors ----------------------------------------------------- diff --git a/include/wx/uilocale.h b/include/wx/uilocale.h index 039c6c2411..6176803008 100644 --- a/include/wx/uilocale.h +++ b/include/wx/uilocale.h @@ -158,12 +158,18 @@ public: // its dtor is not virtual. ~wxUILocale(); + // Return the locale ID representing the default system locale, which would + // be set is UseDefault() is called. + static wxLocaleIdent GetSystemLocaleId(); + // Try to get user's (or OS's) preferred language setting. // Return wxLANGUAGE_UNKNOWN if the language-guessing algorithm failed + // Prefer using GetSystemLocaleId() above. static int GetSystemLanguage(); // Try to get user's (or OS's) default locale setting. // Return wxLANGUAGE_UNKNOWN if the locale-guessing algorithm failed + // Prefer using GetSystemLocaleId() above. static int GetSystemLocale(); // Try to retrieve a list of user's (or OS's) preferred UI languages. diff --git a/interface/wx/intl.h b/interface/wx/intl.h index 53bc3284ab..5114d00516 100644 --- a/interface/wx/intl.h +++ b/interface/wx/intl.h @@ -526,13 +526,15 @@ public: /** Tries to detect the user's default locale setting. - Returns the ::wxLanguage value or @c wxLANGUAGE_UNKNOWN if the language-guessing - algorithm failed. + @note This function is somewhat misleading, as it uses the default + system locale to determine its return value, and not just the system + language. It is preserved for backwards compatibility, but to actually + get the language, and not locale, used by the system by default, call + wxUILocale::GetSystemLanguage() instead. - @note This function works with @em locales and returns the user's default - locale. This may be, and usually is, the same as their preferred UI - language, but it's not the same thing. Use wxTranslation to obtain - @em language information. + Returns the ::wxLanguage value or @c wxLANGUAGE_UNKNOWN if the locale + is not recognized, as can notably happen when combining any language + with a region where this language is not typically spoken. @see wxTranslations::GetBestTranslation(). */ diff --git a/interface/wx/uilocale.h b/interface/wx/uilocale.h index d61e11d928..67dc2364e7 100644 --- a/interface/wx/uilocale.h +++ b/interface/wx/uilocale.h @@ -289,7 +289,7 @@ public: by the operating system (for example, Windows 7 and below), the user's default @em locale will be used. - @see wxTranslations::GetBestTranslation(). + @see wxTranslations::GetBestTranslation(), GetSystemLocaleId(). */ static int GetSystemLanguage(); @@ -297,7 +297,8 @@ public: Tries to detect the user's default locale setting. Returns the ::wxLanguage value or @c wxLANGUAGE_UNKNOWN if the locale-guessing - algorithm failed. + algorithm failed or if the locale can't be described using solely a + language constant. Consider using GetSystemLocaleId() in this case. @note This function works with @em locales and returns the user's default locale. This may be, and usually is, the same as their preferred UI @@ -308,7 +309,19 @@ public: @see wxTranslations::GetBestTranslation(). */ - static int GetSystemLocale();}; + static int GetSystemLocale(); + + /** + Return the description of the default system locale. + + This function can always represent the system locale, even when using + a language and region pair that doesn't correspond to any of the + predefined ::wxLanguage constants, such as e.g. "fr-DE", which means + French language used with German locale settings. + + @since 3.3.0 + */ +}; /** Return the format to use for formatting user-visible dates. diff --git a/src/common/intl.cpp b/src/common/intl.cpp index 9e6c43f5c5..c32d708173 100644 --- a/src/common/intl.cpp +++ b/src/common/intl.cpp @@ -653,8 +653,14 @@ const wxLanguageInfo* wxLocale::GetLanguageInfo(int lang) { // We need to explicitly handle the case "lang == wxLANGUAGE_DEFAULT" here, // because wxUILocale::GetLanguageInfo() determines the system language - // based on the the preferred UI language while wxLocale uses the default + // based on the preferred UI language while wxLocale uses the default // user locale for that purpose. + // + // Note that even though wxUILocale::GetLanguageInfo() seems to do the same + // thing as we do here, it actually does _not_ because we're calling our + // GetSystemLanguage() which maps to wxUILocale::GetSystemLocale() and not + // the function with the same name in that class. This is incredibly + // confusing but necessary for backwards compatibility. if (lang == wxLANGUAGE_DEFAULT) lang = GetSystemLanguage(); return wxUILocale::GetLanguageInfo(lang); @@ -726,11 +732,36 @@ bool wxLocale::IsAvailable(int lang) const wxLanguageInfo *info = wxLocale::GetLanguageInfo(lang); if ( !info ) { - // The language is unknown (this normally only happens when we're - // passed wxLANGUAGE_DEFAULT), so we can't support it. - wxASSERT_MSG( lang == wxLANGUAGE_DEFAULT, - wxS("No info for a valid language?") ); - return false; + // This must be wxLANGUAGE_DEFAULT as otherwise we should have found + // the matching entry. + wxCHECK_MSG( lang == wxLANGUAGE_DEFAULT, false, + wxS("No info for a valid language?") ); + + // For this one, we need to check whether using it later is going to + // actually work, i.e. if the CRT supports it. + const char* const origLocale = wxSetlocale(LC_ALL, nullptr); + if ( !origLocale ) + { + // This is not supposed to happen, we should always be able to + // query the current locale, but don't crash if it does. + return false; + } + + // Make a copy of the string because wxSetlocale() call below may + // change the buffer to which it points. + const wxString origLocaleStr = wxString::FromUTF8(origLocale); + + if ( !wxSetlocale(LC_ALL, "") ) + { + // Locale wasn't changed, so nothing else to do. + return false; + } + + // We support this locale, but restore the original one before + // returning. + wxSetlocale(LC_ALL, origLocaleStr.utf8_str()); + + return true; } wxString localeTag = info->GetCanonicalWithRegion(); diff --git a/src/common/uilocale.cpp b/src/common/uilocale.cpp index 69846c2274..cc50184bc5 100644 --- a/src/common/uilocale.cpp +++ b/src/common/uilocale.cpp @@ -632,6 +632,13 @@ wxUILocale::~wxUILocale() } +/* static */ +wxLocaleIdent wxUILocale::GetSystemLocaleId() +{ + wxUILocale defaultLocale(wxUILocaleImpl::CreateUserDefault()); + return defaultLocale.GetLocaleId(); +} + /*static*/ int wxUILocale::GetSystemLanguage() { @@ -677,12 +684,26 @@ int wxUILocale::GetSystemLanguage() /*static*/ int wxUILocale::GetSystemLocale() { - // Create default wxUILocale - wxUILocale defaultLocale(wxUILocaleImpl::CreateUserDefault()); + const wxLocaleIdent locId = GetSystemLocaleId(); - // Find corresponding wxLanguageInfo - const wxLanguageInfo* defaultLanguage = wxUILocale::FindLanguageInfo(defaultLocale.GetLocaleId()); - return defaultLanguage ? defaultLanguage->Language : wxLANGUAGE_UNKNOWN; + // Find wxLanguageInfo corresponding to the default locale. + const wxLanguageInfo* defaultLanguage = wxUILocale::FindLanguageInfo(locId); + + // Check if it really corresponds to this locale: we could find it via the + // fallback on the language, which is something that it generally makes + // sense for FindLanguageInfo() to do, but in this case we really need the + // locale. + if ( defaultLanguage ) + { + // We have to handle the "C" locale specially as its name is different + // from the "en-US" tag found for it, but we do still want to return + // English for it. + const wxString tag = locId.GetTag(wxLOCALE_TAGTYPE_BCP47); + if ( tag == defaultLanguage->LocaleTag || IsDefaultCLocale(tag) ) + return defaultLanguage->Language; + } + + return wxLANGUAGE_UNKNOWN; } /* static */ @@ -813,9 +834,12 @@ const wxLanguageInfo* wxUILocale::FindLanguageInfo(const wxLocaleIdent& locId) CreateLanguagesDB(); const wxLanguageInfo* infoRet = nullptr; + + wxString lang = locId.GetLanguage(); wxString localeTag = locId.GetTag(wxLOCALE_TAGTYPE_BCP47); - if (IsDefaultCLocale(locId.GetLanguage())) + if (IsDefaultCLocale(lang)) { + lang = wxS("en"); localeTag = "en-US"; } @@ -832,7 +856,7 @@ const wxLanguageInfo* wxUILocale::FindLanguageInfo(const wxLocaleIdent& locId) break; } - if (wxStricmp(localeTag, info->LocaleTag.BeforeFirst(wxS('-'))) == 0) + if (wxStricmp(lang, info->LocaleTag.BeforeFirst(wxS('-'))) == 0) { // a match -- but maybe we'll find an exact one later, so continue // looking diff --git a/src/common/wxcrt.cpp b/src/common/wxcrt.cpp index ab8ae524a7..bf00eecc86 100644 --- a/src/common/wxcrt.cpp +++ b/src/common/wxcrt.cpp @@ -52,13 +52,6 @@ #include -#if defined(__DARWIN__) - #include "wx/osx/core/cfref.h" - #include - #include "wx/osx/core/cfstring.h" - #include -#endif - wxDECL_FOR_STRICT_MINGW32(int, vswprintf, (wchar_t*, const wchar_t*, __VALIST)) wxDECL_FOR_STRICT_MINGW32(int, _putws, (const wchar_t*)) wxDECL_FOR_STRICT_MINGW32(void, _wperror, (const wchar_t*)) @@ -125,27 +118,7 @@ WXDLLIMPEXP_BASE size_t wxWC2MB(char *buf, const wchar_t *pwz, size_t n) char* wxSetlocale(int category, const char *locale) { -#ifdef __WXMAC__ - char *rv = nullptr ; - if ( locale != nullptr && locale[0] == 0 ) - { - // the attempt to use newlocale(LC_ALL_MASK, "", nullptr); - // here in order to deduce the language along the environment vars rules - // lead to strange crashes later... - - // we have to emulate the behaviour under OS X - wxCFRef userLocaleRef(CFLocaleCopyCurrent()); - wxCFStringRef str(wxCFRetain((CFStringRef)CFLocaleGetValue(userLocaleRef, kCFLocaleLanguageCode))); - wxString langFull = str.AsString()+"_"; - str.reset(wxCFRetain((CFStringRef)CFLocaleGetValue(userLocaleRef, kCFLocaleCountryCode))); - langFull += str.AsString(); - rv = setlocale(category, langFull.c_str()); - } - else - rv = setlocale(category, locale); -#else char *rv = setlocale(category, locale); -#endif if ( locale != nullptr /* setting locale, not querying */ && rv /* call was successful */ ) { diff --git a/src/osx/core/uilocale.mm b/src/osx/core/uilocale.mm index 9c15b13f94..ee0eb07bc8 100644 --- a/src/osx/core/uilocale.mm +++ b/src/osx/core/uilocale.mm @@ -131,7 +131,13 @@ wxUILocaleImplCF::Use() wxString wxUILocaleImplCF::GetName() const { - return wxCFStringRef::AsString([m_nsloc localeIdentifier]); + wxString name = wxCFStringRef::AsString([m_nsloc localeIdentifier]); + + // Check for the special case of the "empty" system locale, see CreateStdC() + if ( name.empty() ) + name = "C"; + + return name; } wxLocaleIdent @@ -193,7 +199,12 @@ wxUILocaleImplCF::GetLayoutDirection() const /* static */ wxUILocaleImpl* wxUILocaleImpl::CreateStdC() { - return wxUILocaleImplCF::Create(wxLocaleIdent().Language("C")); + // This is an "empty" locale, but it seems to correspond rather well to the + // "C" locale under POSIX systems and using localeWithLocaleIdentifier:@"C" + // wouldn't be much better as we'd still need a hack for it in GetName() + // because the locale names are always converted to lower case, while we + // really want to return "C" rather than "c" as the name of this one. + return new wxUILocaleImplCF([NSLocale systemLocale]); } /* static */ diff --git a/src/unix/uilocale.cpp b/src/unix/uilocale.cpp index 11d084c474..c51c4f2f19 100644 --- a/src/unix/uilocale.cpp +++ b/src/unix/uilocale.cpp @@ -47,17 +47,15 @@ inline bool wxGetNonEmptyEnvVar(const wxString& name, wxString* value) return wxGetEnv(name, value) && !value->empty(); } -// Get locale information from the appropriate environment variable: the output +// Get locale information from the specified environment variable: the output // variables are filled with the locale part (xx_XX) and the modifier is filled // with the optional part following "@". // // Return false if there is no locale information in the environment variables // or if it is just "C" or "POSIX". -bool GetLocaleFromEnvironment(wxString& langFull, wxString& modifier) +bool GetLocaleFromEnvVar(const char* var, wxString& langFull, wxString& modifier) { - if (!wxGetNonEmptyEnvVar(wxS("LC_ALL"), &langFull) && - !wxGetNonEmptyEnvVar(wxS("LC_MESSAGES"), &langFull) && - !wxGetNonEmptyEnvVar(wxS("LANG"), &langFull)) + if ( !wxGetNonEmptyEnvVar(var, &langFull) ) { return false; } @@ -272,11 +270,11 @@ locale_t TryCreateLocaleWithUTF8(wxLocaleIdent& locId) locale_t TryCreateMatchingLocale(wxLocaleIdent& locId) { locale_t loc = TryCreateLocaleWithUTF8(locId); - if ( !loc ) + if ( !loc && locId.GetRegion().empty() ) { - // Try to find a variant of this locale available on this system: first - // of all, using just the language, without the territory, typically - // does _not_ work under Linux, so try adding one if we don't have it. + // Try to find a variant of this locale available on this system: as + // using just the language, without the territory, typically does _not_ + // work under Linux, we try adding one if we don't have it. const wxString lang = locId.GetLanguage(); const wxLanguageInfos& infos = wxGetLanguageInfos(); @@ -475,7 +473,8 @@ wxUILocaleImplUnix::InitLocaleNameAndCodeset() const // This must be the default locale. wxString locName, modifier; - if ( !GetLocaleFromEnvironment(locName, modifier) ) + if ( !GetLocaleFromEnvVar("LC_ALL", locName, modifier) && + !GetLocaleFromEnvVar("LANG", locName, modifier) ) { // This is the default locale if nothing is specified. locName = "en_US"; @@ -730,7 +729,21 @@ wxUILocaleImpl* wxUILocaleImpl::CreateStdC() /* static */ wxUILocaleImpl* wxUILocaleImpl::CreateUserDefault() { +#ifdef HAVE_LOCALE_T + // Setting default locale can fail under Unix if LANG or LC_ALL are set to + // an unsupported value, so check for this here to let the caller know if + // we can't do it. + wxLocaleIdent locDef; + locale_t loc = TryCreateLocaleWithUTF8(locDef); + if ( !loc ) + return nullptr; + + return new wxUILocaleImplUnix(wxLocaleIdent(), loc); +#else // !HAVE_LOCALE_T + // We could temporarily change the locale here to check if it's supported, + // but for now don't bother and assume it is. return new wxUILocaleImplUnix(wxLocaleIdent()); +#endif // HAVE_LOCALE_T/!HAVE_LOCALE_T } /* static */ @@ -760,7 +773,37 @@ wxVector wxUILocaleImpl::GetPreferredUILanguages() // When the preferred UI language is determined, the LANGUAGE environment // variable is the primary source of preference. // http://www.gnu.org/software/gettext/manual/html_node/Locale-Environment-Variables.html - // + + // Since the first item in LANGUAGE is supposed to be equal to LANG resp LC_ALL, + // determine the default language based on the locale related environment variables + // as the first entry in the list of preferred languages. + wxString langFull; + wxString modifier; + + // Check LC_ALL first, as it's supposed to override everything else, then + // for LC_MESSAGES because this is the variable defining the translations + // language and so must correspond to the language the user wants to use + // and, otherwise, fall back on LANG which is the normal way to specify + // both the locale and the language. + if ( GetLocaleFromEnvVar("LC_ALL", langFull, modifier) || + GetLocaleFromEnvVar("LC_MESSAGES", langFull, modifier) || + GetLocaleFromEnvVar("LANG", langFull, modifier) ) + { + if (!modifier.empty()) + { + // Locale name with modifier + if (const wxLanguageInfo* li = wxUILocale::FindLanguageInfo(langFull + modifier)) + { + preferred.push_back(li->CanonicalName); + } + } + // Locale name without modifier + if (const wxLanguageInfo* li = wxUILocale::FindLanguageInfo(langFull)) + { + preferred.push_back(li->CanonicalName); + } + } + // The LANGUAGE variable may contain a colon separated list of language // codes in the order of preference. // http://www.gnu.org/software/gettext/manual/html_node/The-LANGUAGE-variable.html @@ -780,26 +823,19 @@ wxVector wxUILocaleImpl::GetPreferredUILanguages() return preferred; wxLogTrace(TRACE_I18N, " - LANGUAGE was set, but it didn't contain any languages recognized by the system"); } + else + { + wxLogTrace(TRACE_I18N, " - LANGUAGE was not set or empty, check LC_ALL, LC_MESSAGES, and LANG"); + } - wxLogTrace(TRACE_I18N, " - LANGUAGE was not set or empty, check LC_ALL, LC_MESSAGES, and LANG"); - - // first get the string identifying the language from the environment - wxString langFull, - modifier; - if (!GetLocaleFromEnvironment(langFull, modifier)) + if (preferred.empty()) { // no language specified, treat it as English langFull = "en_US"; + preferred.push_back(langFull); + wxLogTrace(TRACE_I18N, " - LC_ALL, LC_MESSAGES, and LANG were not set or empty, use English"); } - if (!modifier.empty()) - { - // Locale name with modifier - preferred.push_back(langFull + modifier); - } - // Locale name without modifier - preferred.push_back(langFull); - return preferred; } diff --git a/tests/intl/intltest.cpp b/tests/intl/intltest.cpp index 9810199cfc..0391a90b1b 100644 --- a/tests/intl/intltest.cpp +++ b/tests/intl/intltest.cpp @@ -237,12 +237,13 @@ void IntlTestCase::IsAvailable() TEST_CASE("wxLocale::Default", "[locale]") { - INFO("System language: " << wxLocale::GetSystemLanguage()); - CHECK( wxLocale::IsAvailable(wxLANGUAGE_DEFAULT) ); + const int langDef = wxUILocale::GetSystemLanguage(); + INFO("System language: " << wxUILocale::GetLanguageName(langDef)); + CHECK( wxLocale::IsAvailable(langDef) ); wxLocale loc; - REQUIRE( loc.Init(wxLANGUAGE_DEFAULT, wxLOCALE_DONT_LOAD_DEFAULT) ); + REQUIRE( loc.Init(langDef, wxLOCALE_DONT_LOAD_DEFAULT) ); } // Under MSW and macOS all the locales used below should be supported, but @@ -412,6 +413,11 @@ TEST_CASE("wxUILocale::FindLanguageInfo", "[uilocale]") CheckFindLanguage("English_United States.utf8", "en_US"); // Test tag that includes an explicit script CheckFindLanguage("sr-Latn-RS", "sr_RS@latin"); + + // Test mixed locales: we should still detect the language correctly, even + // if we don't recognize the full locale. + CheckFindLanguage("en_FR", "en"); + CheckFindLanguage("fr_DE", "fr"); } // Test which can be used to check if the given locale tag is supported. @@ -430,4 +436,75 @@ TEST_CASE("wxUILocale::FromTag", "[.]") WARN("Locale \"" << tag << "\" supported: " << loc.IsSupported() ); } +namespace +{ + +const wxString GetLangName(int lang) +{ + switch ( lang ) + { + case wxLANGUAGE_DEFAULT: + return "DEFAULT"; + + case wxLANGUAGE_UNKNOWN: + return "UNKNOWN"; + + default: + return wxUILocale::GetLanguageName(lang); + } +} + +wxString GetLocaleDesc(const char* when) +{ + const wxUILocale& curloc = wxUILocale::GetCurrent(); + const wxLocaleIdent locid = curloc.GetLocaleId(); + + // Make the output slightly more readable. + wxString decsep = curloc.GetInfo(wxLOCALE_DECIMAL_POINT); + if ( decsep == "." ) + decsep = "point"; + else if ( decsep == "," ) + decsep = "comma"; + else + decsep = wxString::Format("UNKNOWN (%s)", decsep); + + return wxString::Format("%s\ncurrent locale:\t%s (decimal separator: %s)", + when, + locid.IsEmpty() ? wxString("NONE") : locid.GetTag(), + decsep); +} + +} // anonymous namespace + +// Test to show information about the system locale and the effects of various +// ways to change the current locale. +TEST_CASE("wxUILocale::ShowSystem", "[.]") +{ + WARN("System locale identifier:\t" + << wxUILocale::GetSystemLocaleId().GetTag() << "\n" + "System locale as language:\t" + << GetLangName(wxUILocale::GetSystemLocale()) << "\n" + "System language identifier:\t" + << GetLangName(wxUILocale::GetSystemLanguage())); + + WARN(GetLocaleDesc("Before calling any locale functions")); + + wxLocale locDef; + CHECK( locDef.Init(wxLANGUAGE_DEFAULT, wxLOCALE_DONT_LOAD_DEFAULT) ); + WARN(GetLocaleDesc("After wxLocale::Init(wxLANGUAGE_DEFAULT)")); + + CHECK( wxUILocale::UseDefault() ); + WARN(GetLocaleDesc("After wxUILocale::UseDefault()")); + + wxString preferredLangsStr; + const auto preferredLangs = wxUILocale::GetPreferredUILanguages(); + for (const auto& lang: preferredLangs) + { + if ( !preferredLangsStr.empty() ) + preferredLangsStr += ", "; + preferredLangsStr += lang; + } + WARN("Preferred UI languages:\n" << preferredLangsStr); +} + #endif // wxUSE_INTL