Files
wxWidgets/src/common/uilocale.cpp
T
utelle 8ab635b451 Improve wxLocale backwards compatibility and enhance wxUILocale
Restore the old behaviour of wxLocale, which is supposed to use the
default locale and not the preferred language, which may not be the same
(see #22281).

Also apply the following fixes and improvements to wxUILocale:

- Add new GetSystemLocale() method.
- Change the MSW implementation to use the default locale instead of the
  preferred UI language for Windows versions below Windows 10.
- Change the Unix implementation to respect LANGUAGE environment
  variable and use it for determining the preferred UI languages.
- Use wxUILocale in wxTranslations to determine the preferred UI
  languages.
- Use wxUILocale during initialization of internat sample.

Closes #22281.

Closes #22318.
2022-04-17 18:04:21 +02:00

838 lines
23 KiB
C++

///////////////////////////////////////////////////////////////////////////////
// Name: src/common/uilocale.cpp
// Purpose: wxUILocale implementation
// Author: Vadim Zeitlin
// Created: 2021-07-31
// Copyright: (c) 2021 Vadim Zeitlin <vadim@wxwidgets.org>
// Licence: wxWindows licence
///////////////////////////////////////////////////////////////////////////////
// ============================================================================
// declarations
// ============================================================================
// ----------------------------------------------------------------------------
// headers
// ----------------------------------------------------------------------------
// for compilers that support precompilation, includes "wx.h".
#include "wx/wxprec.h"
#if wxUSE_INTL
#include "wx/uilocale.h"
#include "wx/arrstr.h"
#include "wx/intl.h"
#ifndef __WINDOWS__
#include "wx/language.h"
#endif
#include "wx/private/uilocale.h"
// ----------------------------------------------------------------------------
// helper functions
// ----------------------------------------------------------------------------
namespace
{
const char* validCharsAlpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
const char* validCharsAlnum = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
const char* validCharsModExt = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-";
// Handle special case "ca-ES-valencia"
// Sync attributes modifier and extension
inline void CheckLanguageVariant(wxLocaleIdent& locId)
{
if (locId.GetModifier().IsSameAs("valencia"))
{
locId.Extension(locId.GetModifier());
}
else if (locId.GetExtension().IsSameAs("valencia") && locId.GetModifier().empty())
{
locId.Modifier(locId.GetExtension());
}
}
} // anonymous namespace
// ----------------------------------------------------------------------------
// global variables
// ----------------------------------------------------------------------------
// This static global variable doesn't need to be protected from concurrent
// access as it's only supposed to be used from the UI thread.
/* static */
wxUILocale wxUILocale::ms_current;
// ============================================================================
// implementation
// ============================================================================
// ----------------------------------------------------------------------------
// wxLocaleIdent
// ----------------------------------------------------------------------------
/* static */
wxLocaleIdent wxLocaleIdent::FromTag(const wxString& tag)
{
// This method accepts tags in various formats: BCP47, Windows, POSIX, and macOS.
//
// See section 2.01 of https://www.rfc-editor.org/rfc/bcp/bcp47.txt for the
// full syntax. Here we fully support just the subset we're interested in:
//
// - Normal language tags (not private use or grandfathered ones).
// - Script and region.
//
// Additionally platform-specific tags are supported:
// - Extensions (without validity checks) (Windows only).
// - Charset and modifier (POSIX only)
//
// Only language, script, and region are supported across all platforms.
// The script tag is mapped to the modifier for POSIX platforms.
// The script tag takes precedence, if a modifier is also specified.
//
// The following tag syntax is accepted:
// BCP47: language[-script][-region][-extension]
// Windows: language[-script][-region][-extension][_sortorder]
// POSIX: language[_region][.charset][@modifier]
// macOS: language[-script][_region]
// MSVC CRT: language[_region][.codepage]
wxLocaleIdent locId;
// 1. Handle platform-dependent cases
// 1a. Check for modifier in POSIX tag
wxString tagRest;
wxString tagMain = tag.BeforeFirst('@', &tagRest);
if (!tagRest.empty())
{
// POSIX modifier found
wxString script = wxUILocale::GetScriptNameFromAlias(tagRest);
if (!script.empty())
locId.Script(script);
else
locId.Modifier(tagRest);
}
// 1b. Check for charset in POSIX tag
tagMain = tagMain.BeforeFirst('.', &tagRest);
if (!tagRest.empty())
{
// POSIX charset found
locId.Charset(tagRest);
}
// 1c. Check for Windows CRT language and region names
{
// The tag is potentially a Windows CRT language/region name,
// if language and region part both have a length greater 3
// (that is, they are not given as ISO codes)
wxString tagTemp = tagMain.BeforeFirst('_', &tagRest);
if (tagTemp.length() > 3 && (tagRest.empty() || tagRest.length() > 3))
{
const wxLanguageInfo* const info = wxUILocale::FindLanguageInfo(tagMain);
if (info)
{
tagMain = info->LocaleTag;
}
}
}
// 1d. Check for sort order in Windows tag
//
// Make sure we don't extract the region identifier erroneously as a sortorder identifier
{
wxString tagTemp = tagMain.BeforeFirst('_', &tagRest);
if (tagRest.length() > 4 && locId.m_modifier.empty() && locId.m_charset.empty())
{
// Windows sortorder found
locId.SortOrder(tagRest);
tagMain = tagTemp;
}
}
// 2. Handle remaining tag identifier as being BCP47-like
// Now that special POSIX attributes have been handled
// POSIX specific delimiters must no longer be present
// Replace '_' separators by '-' to simplify further processing
tagMain.Replace("_", "-");
const wxArrayString& parts = wxSplit(tagMain, '-', '\0');
wxArrayString::const_iterator it = parts.begin();
if ( it == parts.end() )
return wxLocaleIdent();
// We have at least the language, so we'll return a valid object.
locId.m_language = (*it).Lower();
// Also store the full string.
locId.m_tag = tag;
if ( ++it == parts.end() )
return locId;
// Advance to the next component we know about.
switch ( locId.m_language.length() )
{
case 2:
case 3:
// Looks like an ISO 639 code.
break;
default:
// It may be private use or grandfathered tag or just invalid
// syntax, but in any case we can't parse it further.
return locId;
}
// Skip extlangs that are 3 letters long, in contrast to 3 digit region
// codes.
while ( it->length() == 3 && !isdigit((*it)[0]) )
{
if ( ++it == parts.end() )
return locId;
}
switch ( it->length() )
{
case 2:
case 3:
// Either an ISO 3166-1 or UN M.49 region code.
locId.m_region = (*it).Upper();
break;
case 4:
// Must be an ISO 15924 script.
locId.m_script = (*it).Left(1).Upper() + (*it).Mid(2).Lower();
break;
default:
// This looks to be completely invalid.
return wxLocaleIdent();
}
// Check whether we have got the region above.
// If not, we must have got the script. So, check if we have the region, too.
if (++it == parts.end())
{
CheckLanguageVariant(locId);
return locId;
}
if (locId.m_region.empty())
{
switch (it->length())
{
case 2:
case 3:
locId.m_region = (*it).Upper(); ++it;
break;
}
}
// If there is still anything to parse (variants, extensions, private use),
// we assign it to the extension.
if (it != parts.end())
{
wxString custom = *it;
while (++it != parts.end())
{
custom << "-" << *it;
}
locId.m_extension = custom;
}
// We also handle the only language variant known at the time of writing:
// valencia (ca-ES-valencia resp ca_ES@valencia).
CheckLanguageVariant(locId);
return locId;
}
wxLocaleIdent& wxLocaleIdent::Language(const wxString& language)
{
if (language.IsSameAs("C", false) || language.IsSameAs("POSIX", false))
{
m_language = language.Upper();
}
else if ((language.length() == 2 || language.length() == 3) &&
language.find_first_not_of(validCharsAlpha) == wxString::npos)
{
m_language = language.Lower();
}
else
{
m_language.clear();
}
return *this;
}
wxLocaleIdent& wxLocaleIdent::Region(const wxString& region)
{
if ((region.length() == 2 || region.length() == 3) &&
region.find_first_not_of(validCharsAlnum) == wxString::npos)
{
m_region = region.Upper();
}
else
{
m_region.clear();
}
return *this;
}
wxLocaleIdent& wxLocaleIdent::Script(const wxString& script)
{
if (script.length() == 4 &&
script.find_first_not_of(validCharsAlpha) == wxString::npos)
{
// Capitalize first character
m_script = script.Left(1).Upper() + script.Mid(2).Lower();
}
else if (!script.empty())
{
m_script = wxUILocale::GetScriptNameFromAlias(script.Lower());
}
else
{
m_script.clear();
}
return *this;
}
wxLocaleIdent& wxLocaleIdent::Charset(const wxString& charset)
{
if (charset.find_first_not_of(validCharsModExt) == wxString::npos)
{
m_charset = charset;
}
else
{
m_charset.clear();
}
return *this;
}
wxLocaleIdent& wxLocaleIdent::Modifier(const wxString& modifier)
{
if (modifier.find_first_not_of(validCharsModExt) == wxString::npos)
{
m_modifier = modifier;
}
else
{
m_modifier.clear();
}
return *this;
}
wxLocaleIdent& wxLocaleIdent::Extension(const wxString& extension)
{
// Windows extensions follow the BCP 47 syntax
if (extension.find_first_not_of(validCharsModExt) == wxString::npos)
{
m_extension = extension;
}
return *this;
}
wxLocaleIdent& wxLocaleIdent::SortOrder(const wxString& sortorder)
{
// Windows sortorder identifiers all seem to have a length of 6 characters.
// To distinguish sortorder from script and region identifiers require length > 4.
if (sortorder.length() > 4 &&
sortorder.find_first_not_of(validCharsAlpha) == wxString::npos)
{
m_sortorder = sortorder;
}
return *this;
}
wxString wxLocaleIdent::GetTag(wxLocaleTagType tagType) const
{
if (tagType == wxLOCALE_TAGTYPE_DEFAULT && !m_tag.empty() )
return m_tag;
wxString tag = m_language;
switch (tagType)
{
case wxLOCALE_TAGTYPE_BCP47:
if (!m_script.empty())
tag << '-' << m_script;
if (!m_region.empty())
tag << '-' << m_region;
if (!m_extension.empty())
tag << '-' << m_extension;
break;
case wxLOCALE_TAGTYPE_MACOS:
if (!m_script.empty())
tag << '-' << m_script;
if (!m_region.empty())
tag << '_' << m_region;
break;
case wxLOCALE_TAGTYPE_POSIX:
if (!m_region.empty())
tag << '_' << m_region;
if (!m_charset.empty())
tag << '.' << m_charset;
if (!m_script.empty())
tag << '@' << wxUILocale::GetScriptAliasFromName(m_script);
else if (!m_modifier.empty())
tag << '@' << m_modifier;
break;
case wxLOCALE_TAGTYPE_WINDOWS:
if (!m_script.empty())
tag << '-' << m_script;
if (!m_region.empty())
tag << '-' << m_region;
if (!m_extension.empty())
tag << '-' << m_extension;
if (!m_sortorder.empty())
tag << '-' << m_sortorder;
break;
case wxLOCALE_TAGTYPE_SYSTEM:
default:
tag = GetName();
break;
}
return tag;
}
// ----------------------------------------------------------------------------
// wxUILocale
// ----------------------------------------------------------------------------
#ifndef __WINDOWS__
/* static */
wxUILocaleImpl* wxUILocaleImpl::CreateForLanguage(const wxLanguageInfo& info)
{
wxLocaleIdent locId;
// Strings in our language database are of the form "lang[_region[@mod]]".
wxString rest;
locId.Language(info.CanonicalName.BeforeFirst('_', &rest));
if ( !rest.empty() )
{
wxString mod;
locId.Region(rest.BeforeFirst('@', &mod));
if ( !mod.empty() )
locId.Modifier(mod);
}
wxUILocaleImpl* impl = CreateForLocale(locId);
if ( !impl &&
(info.Language == wxLANGUAGE_ENGLISH ||
info.Language == wxLANGUAGE_ENGLISH_US) )
{
// For compatibility, never fail creating locale for neutral or US
// English, even if it's unavailable on the current system somehow.
impl = CreateStdC();
}
return impl;
}
#endif // !__WINDOWS__
/* static */
bool wxUILocale::UseDefault()
{
// We don't attempt to optimize this function by checking whether
// ms_current is already set to the user default locale, as we're
// supposed to be called just once during the program lifetime anyhow.
wxUILocaleImpl* const impl = wxUILocaleImpl::CreateUserDefault();
if ( !impl )
return false;
impl->Use();
ms_current = wxUILocale(impl);
return true;
}
/* static */
bool wxUILocale::UseLocaleName(const wxString& localeName)
{
wxUILocaleImpl* impl = NULL;
if (localeName.IsSameAs("C", false) || localeName.IsSameAs("POSIX", false))
{
impl = wxUILocaleImpl::CreateStdC();
}
else
{
wxLocaleIdent localeId = wxLocaleIdent::FromTag(localeName);
impl = wxUILocaleImpl::CreateForLocale(localeId);
if (!impl)
{
// Creating the locale may have failed due to lacking support for wxUILocaleImplName
// Try to locate the locale in our language database
const wxLanguageInfo* const info = wxUILocale::FindLanguageInfo(localeId);
if (info)
{
// Language found in language database
// Try to create a locale based on the language
impl = wxUILocaleImpl::CreateForLanguage(*info);
}
}
}
if (!impl)
return false;
impl->Use();
ms_current = wxUILocale(impl);
return true;
}
/* static */
const wxUILocale& wxUILocale::GetCurrent()
{
// We initialize it on demand.
if ( !ms_current.m_impl )
{
ms_current = wxUILocale(wxUILocaleImpl::CreateStdC());
}
return ms_current;
}
wxUILocale::wxUILocale(const wxLocaleIdent& localeId)
{
if ( localeId.IsEmpty() )
{
wxFAIL_MSG( "Locale identifier must be initialized" );
m_impl = NULL;
return;
}
m_impl = wxUILocaleImpl::CreateForLocale(localeId);
}
wxUILocale::wxUILocale(const wxUILocale& loc)
{
m_impl = loc.m_impl;
if ( m_impl )
m_impl->IncRef();
}
wxUILocale& wxUILocale::operator=(const wxUILocale& loc)
{
if ( m_impl )
m_impl->DecRef();
m_impl = loc.m_impl;
if ( m_impl )
m_impl->IncRef();
return *this;
}
bool wxUILocale::IsSupported() const
{
return m_impl != NULL;
}
wxString wxUILocale::GetName() const
{
if ( !m_impl )
return wxString();
return m_impl->GetName();
}
wxLocaleIdent wxUILocale::GetLocaleId() const
{
if (!m_impl)
return wxLocaleIdent();
return m_impl->GetLocaleId();
}
wxString wxUILocale::GetInfo(wxLocaleInfo index, wxLocaleCategory cat) const
{
if ( !m_impl )
return wxGetStdCLocaleInfo(index, cat);
return m_impl->GetInfo(index, cat);
}
wxString wxUILocale::GetLocalizedName(wxLocaleName name, wxLocaleForm form) const
{
if (!m_impl)
return wxString();
return m_impl->GetLocalizedName(name, form);
}
wxLayoutDirection wxUILocale::GetLayoutDirection() const
{
if (!m_impl)
return wxLayout_Default;
wxLayoutDirection dir = m_impl->GetLayoutDirection();
if (dir == wxLayout_Default)
{
wxLocaleIdent localeId = m_impl->GetLocaleId();
if (!localeId.IsEmpty())
{
const wxLanguageInfo* const info = wxUILocale::FindLanguageInfo(localeId);
if (info)
{
dir = info->LayoutDirection;
}
}
}
return dir;
}
int
wxUILocale::CompareStrings(const wxString& lhs,
const wxString& rhs,
int flags) const
{
if ( !m_impl )
{
const int rc = flags & wxCompare_CaseInsensitive ? lhs.CmpNoCase(rhs)
: lhs.Cmp(rhs);
if ( rc < 0 )
return -1;
if ( rc > 0 )
return 1;
return 0;
}
return m_impl->CompareStrings(lhs, rhs, flags);
}
wxUILocale::~wxUILocale()
{
if ( m_impl )
m_impl->DecRef();
}
/*static*/
int wxUILocale::GetSystemLanguage()
{
const wxLanguageInfos& languagesDB = wxGetLanguageInfos();
size_t count = languagesDB.size();
wxVector<wxString> preferred = wxUILocaleImpl::GetPreferredUILanguages();
for (wxVector<wxString>::const_iterator j = preferred.begin();
j != preferred.end();
++j)
{
wxLocaleIdent localeId = wxLocaleIdent::FromTag(*j);
wxString lang = localeId.GetTag(wxLOCALE_TAGTYPE_BCP47);
size_t pos = lang.find('-');
wxString langShort = (pos != wxString::npos) ? lang.substr(0, pos) : wxString();
size_t ixShort = count;
for (size_t ixLanguage = 0; ixLanguage < count; ++ixLanguage)
{
if (languagesDB[ixLanguage].LocaleTag == lang)
{
return languagesDB[ixLanguage].Language;
}
if (pos != wxString::npos)
{
if (languagesDB[ixLanguage].LocaleTag == langShort)
{
ixShort = ixLanguage;
}
}
}
if (ixShort < count)
{
return languagesDB[ixShort].Language;
}
}
// no info about the preferred UI language in the database
// fall back to default locale
return GetSystemLocale();
}
/*static*/
int wxUILocale::GetSystemLocale()
{
// Create default wxUILocale
wxUILocale defaultLocale(wxUILocaleImpl::CreateUserDefault());
// Find corresponding wxLanguageInfo
const wxLanguageInfo* defaultLanguage = wxUILocale::FindLanguageInfo(defaultLocale.GetLocaleId());
return defaultLanguage ? defaultLanguage->Language : wxLANGUAGE_UNKNOWN;
}
/* static */
wxVector<wxString> wxUILocale::GetPreferredUILanguages()
{
return wxUILocaleImpl::GetPreferredUILanguages();
}
/* static */
const wxLanguageInfo* wxUILocale::GetLanguageInfo(int lang)
{
CreateLanguagesDB();
// calling GetLanguageInfo(wxLANGUAGE_DEFAULT) is a natural thing to do, so
// make it work
if (lang == wxLANGUAGE_DEFAULT)
lang = GetSystemLanguage();
if (lang == wxLANGUAGE_UNKNOWN)
return NULL;
const wxLanguageInfos& languagesDB = wxGetLanguageInfos();
const size_t count = languagesDB.size();
for (size_t i = 0; i < count; i++)
{
if (languagesDB[i].Language == lang)
return &languagesDB[i];
}
return NULL;
}
/* static */
wxString wxUILocale::GetLanguageName(int lang)
{
wxString string;
if (lang == wxLANGUAGE_DEFAULT || lang == wxLANGUAGE_UNKNOWN)
return string;
const wxLanguageInfo* info = GetLanguageInfo(lang);
if (info)
string = info->Description;
return string;
}
/* static */
wxString wxUILocale::GetLanguageCanonicalName(int lang)
{
wxString string;
if (lang == wxLANGUAGE_DEFAULT || lang == wxLANGUAGE_UNKNOWN)
return string;
const wxLanguageInfo* info = GetLanguageInfo(lang);
if (info)
string = info->CanonicalName;
return string;
}
/* static */
const wxLanguageInfo* wxUILocale::FindLanguageInfo(const wxString& locale)
{
if (locale.empty())
return NULL;
CreateLanguagesDB();
// Determine full language and region names, which will be compared
// to the entry description in the language database.
// The locale string may have the form "language[_region][.codeset]".
// We ignore the "codeset" part here.
wxString region;
wxString languageOnly = locale.BeforeFirst('.').BeforeFirst('_', &region);
wxString language= languageOnly;
if (!region.empty())
{
// Construct description consisting of language and region
language << " (" << region << ")";
}
const wxLanguageInfo* infoRet = NULL;
const wxLanguageInfos& languagesDB = wxGetLanguageInfos();
const size_t count = languagesDB.size();
for (size_t i = 0; i < count; i++)
{
const wxLanguageInfo* info = &languagesDB[i];
if (wxStricmp(locale, info->CanonicalName) == 0 ||
wxStricmp(language, info->Description) == 0)
{
// exact match, stop searching
infoRet = info;
break;
}
if (wxStricmp(locale, info->CanonicalName.BeforeFirst(wxS('_'))) == 0 ||
wxStricmp(languageOnly, info->Description) == 0)
{
// a match -- but maybe we'll find an exact one later, so continue
// looking
//
// OTOH, maybe we had already found a language match and in this
// case don't overwrite it because the entry for the default
// country always appears first in gs_languagesDB
if (!infoRet)
infoRet = info;
}
}
return infoRet;
}
/* static */
const wxLanguageInfo* wxUILocale::FindLanguageInfo(const wxLocaleIdent& locId)
{
if (locId.IsEmpty())
return NULL;
CreateLanguagesDB();
const wxLanguageInfo* infoRet = NULL;
wxString localeTag = locId.GetTag(wxLOCALE_TAGTYPE_BCP47);
const wxLanguageInfos& languagesDB = wxGetLanguageInfos();
const size_t count = languagesDB.size();
for (size_t i = 0; i < count; i++)
{
const wxLanguageInfo* info = &languagesDB[i];
if (wxStricmp(localeTag, info->LocaleTag) == 0)
{
// exact match, stop searching
infoRet = info;
break;
}
if (wxStricmp(localeTag, info->LocaleTag.BeforeFirst(wxS('-'))) == 0)
{
// a match -- but maybe we'll find an exact one later, so continue
// looking
//
// OTOH, maybe we had already found a language match and in this
// case don't overwrite it because the entry for the default
// country always appears first in gs_languagesDB
if (!infoRet)
infoRet = info;
}
}
return infoRet;
}
#endif // wxUSE_INTL