wxwidgets/src/common/uilocale.cpp
Václav Slavík 1f658ff729 Override GetPreferredUILanguages with WXLANGUAGE
Add support for overriding GetPreferredUILanguages() return value by
setting the WXLANGUAGE environment variable to a colon-separated list
of desired languages (same as GNU's LANGUAGE variable).

Primarily used for unit tests, but may be more generally useful.
2024-02-13 18:23:06 +01:00

959 lines
26 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"
#include "wx/log.h"
#include "wx/tokenzr.h"
#include "wx/utils.h"
#ifndef __WINDOWS__
#include "wx/language.h"
#endif
#include "wx/private/uilocale.h"
#define TRACE_I18N wxS("i18n")
// ----------------------------------------------------------------------------
// 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());
}
}
inline bool IsDefaultCLocale(const wxString& locale)
{
return locale.IsSameAs("C", false) || locale.IsSameAs("POSIX", false);
}
} // 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;
// 0. Check for special locale identifiers "C" and "POSIX"
if (IsDefaultCLocale(tag))
{
locId.Language(tag);
return 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.BeforeLast('_', &tagRest);
if (!tagTemp.empty() &&
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->Capitalize();
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 (IsDefaultCLocale(language))
{
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.Capitalize();
}
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 = nullptr;
if (IsDefaultCLocale(localeName))
{
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 = nullptr;
return;
}
if (IsDefaultCLocale(localeId.GetLanguage()))
{
m_impl = wxUILocaleImpl::CreateStdC();
}
else
{
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 != nullptr;
}
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);
}
#if wxUSE_DATETIME
wxString wxUILocale::GetMonthName(wxDateTime::Month month, wxDateTime::NameForm form) const
{
if (!m_impl)
return wxString();
return m_impl->GetMonthName(month, form);
}
wxString wxUILocale::GetWeekDayName(wxDateTime::WeekDay weekday, wxDateTime::NameForm form) const
{
if (!m_impl)
return wxString();
return m_impl->GetWeekDayName(weekday, form);
}
#endif // wxUSE_DATETIME
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 */
wxLocaleIdent wxUILocale::GetSystemLocaleId()
{
wxUILocale defaultLocale(wxUILocaleImpl::CreateUserDefault());
return defaultLocale.GetLocaleId();
}
/*static*/
int wxUILocale::GetSystemLanguage()
{
const wxLanguageInfos& languagesDB = wxGetLanguageInfos();
size_t count = languagesDB.size();
wxVector<wxString> preferred = wxUILocale::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()
{
const wxLocaleIdent locId = GetSystemLocaleId();
// 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 */
wxVector<wxString> wxUILocale::GetPreferredUILanguages()
{
// The WXLANGUAGE variable may contain a colon separated list of language
// codes in the order of preference. It is modelled after GNU's LANGUAGE:
// http://www.gnu.org/software/gettext/manual/html_node/The-LANGUAGE-variable.html
wxString languageFromEnv;
if (wxGetEnv("WXLANGUAGE", &languageFromEnv) && !languageFromEnv.empty())
{
wxVector<wxString> preferred;
wxStringTokenizer tknzr(languageFromEnv, ":");
while (tknzr.HasMoreTokens())
{
const wxString tok = tknzr.GetNextToken();
if (const wxLanguageInfo* li = wxUILocale::FindLanguageInfo(tok))
{
preferred.push_back(li->CanonicalName);
}
}
if (!preferred.empty())
{
wxLogTrace(TRACE_I18N, " - using languages override from WXLANGUAGE: '%s'", languageFromEnv);
return preferred;
}
}
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 nullptr;
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 nullptr;
}
/* 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& localeOrig)
{
if (localeOrig.empty())
return nullptr;
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 locale = localeOrig;
if (IsDefaultCLocale(locale))
{
locale = "en_US";
}
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 = nullptr;
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 nullptr;
CreateLanguagesDB();
const wxLanguageInfo* infoRet = nullptr;
wxString lang = locId.GetLanguage();
wxString localeTag = locId.GetTag(wxLOCALE_TAGTYPE_BCP47);
if (IsDefaultCLocale(lang))
{
lang = wxS("en");
localeTag = "en-US";
}
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(lang, 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;
}
#if wxUSE_DATETIME
int wxUILocaleImpl::ArrayIndexFromFlag(wxDateTime::NameFlags flags)
{
switch (flags)
{
case wxDateTime::Name_Full:
return 0;
case wxDateTime::Name_Abbr:
return 1;
case wxDateTime::Name_Shortest:
return 2;
default:
wxFAIL_MSG("unknown wxDateTime::NameFlags value");
}
return -1;
}
#endif // wxUSE_DATETIME
#endif // wxUSE_INTL