Merge branch 'default-locale-improve'

Improvements for handling default locale under macOS and Unix.

See #23119, #23147, #23226.

Closes #23114.
This commit is contained in:
Vadim Zeitlin 2023-02-08 17:33:19 +01:00
commit c4b71b3694
10 changed files with 258 additions and 79 deletions

View file

@ -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
-----------------------------------------------------

View file

@ -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.

View file

@ -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().
*/

View file

@ -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.

View file

@ -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();

View file

@ -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

View file

@ -52,13 +52,6 @@
#include <errno.h>
#if defined(__DARWIN__)
#include "wx/osx/core/cfref.h"
#include <CoreFoundation/CFLocale.h>
#include "wx/osx/core/cfstring.h"
#include <xlocale.h>
#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<CFLocaleRef> 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 */ )
{

View file

@ -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 */

View file

@ -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<wxString> 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<wxString> 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;
}

View file

@ -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