diff --git a/include/wx/translation.h b/include/wx/translation.h index 67b2bd8afc..c12ae52046 100644 --- a/include/wx/translation.h +++ b/include/wx/translation.h @@ -132,6 +132,11 @@ public: // get languages available for this app wxArrayString GetAvailableTranslations(const wxString& domain) const; + // find best translation language for given domain + wxString GetBestTranslation(const wxString& domain, wxLanguage msgIdLanguage); + wxString GetBestTranslation(const wxString& domain, + const wxString& msgIdLanguage = "en"); + // add standard wxWidgets catalog ("wxstd") bool AddStdCatalog(); @@ -168,10 +173,6 @@ private: // perform loading of the catalog via m_loader bool LoadCatalog(const wxString& domain, const wxString& lang); - // find best translation for given domain - wxString ChooseLanguageForDomain(const wxString& domain, - const wxString& msgIdLang); - // find catalog by name in a linked list, return NULL if !found wxMsgCatalog *FindCatalog(const wxString& domain) const; diff --git a/interface/wx/intl.h b/interface/wx/intl.h index 85264edfdf..ca4587ae8f 100644 --- a/interface/wx/intl.h +++ b/interface/wx/intl.h @@ -391,10 +391,17 @@ public: static wxString GetSystemEncodingName(); /** - Tries to detect the user's default language setting. + 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 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. + + @see wxTranslations::GetBestTranslation(). */ static int GetSystemLanguage(); diff --git a/interface/wx/translation.h b/interface/wx/translation.h index e0f2587940..791cb757a9 100644 --- a/interface/wx/translation.h +++ b/interface/wx/translation.h @@ -30,7 +30,7 @@ @since 2.9.1 - @see wxLocale + @see wxLocale, wxTranslationsLoader, wxFileTranslationsLoader */ class wxTranslations { @@ -85,9 +85,58 @@ public: This method can be used e.g. to populate list of application's translations offered to the user. To do this, pass the app's main catalog as @a domain. + + @see GetBestTranslation() */ wxArrayString GetAvailableTranslations(const wxString& domain) const; + /** + Returns the best UI language for the @a domain. + + The language is determined from the preferred UI language or languages + list the user configured in the OS. Notice that this may or may not + correspond to the default @em locale as obtained from + wxLocale::GetSystemLanguage(); modern operation systems (Windows + Vista+, OS X) have separate language and regional (= locale) settings. + + @param domain + The catalog domain to look for. + + @param msgIdLanguage + Specifies the language of "msgid" strings in source code + (i.e. arguments to GetString(), wxGetTranslation() and the _() macro). + + @return Language code if a suitable match was found, empty string + otherwise. + + @since 2.9.5 + */ + wxString GetBestTranslation(const wxString& domain, wxLanguage msgIdLanguage); + + /** + Returns the best UI language for the @a domain. + + The language is determined from the preferred UI language or languages + list the user configured in the OS. Notice that this may or may not + correspond to the default @em locale as obtained from + wxLocale::GetSystemLanguage(); modern operation systems (Windows + Vista+, OS X) have separate language and regional (= locale) settings. + + @param domain + The catalog domain to look for. + + @param msgIdLanguage + Specifies the language of "msgid" strings in source code + (i.e. arguments to GetString(), wxGetTranslation() and the _() macro). + + @return Language code if a suitable match was found, empty string + otherwise. + + @since 2.9.5 + */ + wxString GetBestTranslation(const wxString& domain, + const wxString& msgIdLanguage = "en"); + /** Add standard wxWidgets catalogs ("wxstd" and possible port-specific catalogs). diff --git a/src/common/translation.cpp b/src/common/translation.cpp index c3ee0d73f2..ce26443960 100644 --- a/src/common/translation.cpp +++ b/src/common/translation.cpp @@ -51,8 +51,15 @@ #include "wx/hashset.h" #ifdef __WINDOWS__ + #include "wx/dynlib.h" + #include "wx/scopedarray.h" #include "wx/msw/wrapwin.h" #endif +#ifdef __WXOSX__ + #include "wx/osx/core/cfstring.h" + #include + #include +#endif // ---------------------------------------------------------------------------- // simple types @@ -84,6 +91,148 @@ namespace wxStringToStringHashMap gs_msgIdCharset; #endif +// ---------------------------------------------------------------------------- +// Platform specific helpers +// ---------------------------------------------------------------------------- + +void LogTraceArray(const char *prefix, const wxArrayString& arr) +{ + wxLogTrace(TRACE_I18N, "%s: [%s]", prefix, wxJoin(arr, ',')); +} + +// Use locale-based detection as a fallback +wxString GetPreferredUILanguageFallback(const wxArrayString& WXUNUSED(available)) +{ + const wxString lang = wxLocale::GetLanguageCanonicalName(wxLocale::GetSystemLanguage()); + wxLogTrace(TRACE_I18N, " - obtained best language from locale: %s", lang); + return lang; +} + +#ifdef __WINDOWS__ + +wxString GetPreferredUILanguage(const wxArrayString& available) +{ + typedef BOOL (WINAPI *GetUserPreferredUILanguages_t)(DWORD, PULONG, PWSTR, PULONG); + static GetUserPreferredUILanguages_t s_pfnGetUserPreferredUILanguages = NULL; + static bool s_initDone = false; + if ( !s_initDone ) + { + wxLoadedDLL dllKernel32("kernel32.dll"); + wxDL_INIT_FUNC(s_pfn, GetUserPreferredUILanguages, dllKernel32); + s_initDone = true; + } + + if ( s_pfnGetUserPreferredUILanguages ) + { + ULONG numLangs; + ULONG bufferSize = 0; + if ( (*s_pfnGetUserPreferredUILanguages)(MUI_LANGUAGE_NAME, + &numLangs, + NULL, + &bufferSize) ) + { + wxScopedArray langs(new WCHAR[bufferSize]); + if ( (*s_pfnGetUserPreferredUILanguages)(MUI_LANGUAGE_NAME, + &numLangs, + langs.get(), + &bufferSize) ) + { + wxArrayString preferred; + + WCHAR *buf = langs.get(); + for ( unsigned i = 0; i < numLangs; i++ ) + { + const wxString lang(buf); + preferred.push_back(lang); + buf += lang.length() + 1; + } + LogTraceArray(" - system preferred languages", preferred); + + for ( wxArrayString::const_iterator i = preferred.begin(); + i != preferred.end(); + ++i ) + { + wxString lang(*i); + lang.Replace("-", "_"); + if ( available.Index(lang) != wxNOT_FOUND ) + return lang; + size_t pos = lang.find('_'); + if ( pos != wxString::npos ) + { + lang = lang.substr(0, pos); + if ( available.Index(lang) != wxNOT_FOUND ) + return lang; + } + } + } + } + } + + return GetPreferredUILanguageFallback(available); +} + +#elif defined(__WXOSX__) + +void LogTraceArray(const char *prefix, CFArrayRef arr) +{ + wxString s; + const unsigned count = CFArrayGetCount(arr); + if ( count ) + { + s += wxCFStringRef::AsString((CFStringRef)CFArrayGetValueAtIndex(arr, 0)); + for ( unsigned i = 1 ; i < count; i++ ) + s += "," + wxCFStringRef::AsString((CFStringRef)CFArrayGetValueAtIndex(arr, i)); + } + wxLogTrace(TRACE_I18N, "%s: [%s]", prefix, s); +} + +wxString GetPreferredUILanguage(const wxArrayString& available) +{ + wxStringToStringHashMap availableNormalized; + wxCFRef availableArr( + CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks)); + + for ( wxArrayString::const_iterator i = available.begin(); + i != available.end(); + ++i ) + { + wxString lang(*i); + wxCFStringRef code_wx(*i); + wxCFStringRef code_norm( + CFLocaleCreateCanonicalLanguageIdentifierFromString(kCFAllocatorDefault, code_wx)); + CFArrayAppendValue(availableArr, code_norm); + availableNormalized[code_norm.AsString()] = *i; + } + LogTraceArray(" - normalized available list", availableArr); + + wxCFRef prefArr( + CFBundleCopyLocalizationsForPreferences(availableArr, NULL)); + LogTraceArray(" - system preferred languages", prefArr); + + unsigned prefArrLength = CFArrayGetCount(prefArr); + if ( prefArrLength > 0 ) + { + // Lookup the name in 'available' by index -- we need to get the + // original value corresponding to the normalized one chosen. + wxString lang(wxCFStringRef::AsString((CFStringRef)CFArrayGetValueAtIndex(prefArr, 0))); + wxStringToStringHashMap::const_iterator i = availableNormalized.find(lang); + if ( i == availableNormalized.end() ) + return lang; + else + return i->second; + } + + return GetPreferredUILanguageFallback(available); +} + +#else + +// On Unix, there's just one language=locale setting, so we should always +// use that. +#define GetPreferredUILanguage GetPreferredUILanguageFallback + +#endif + } // anonymous namespace // ---------------------------------------------------------------------------- @@ -1336,7 +1485,7 @@ bool wxTranslations::AddCatalog(const wxString& domain, wxLanguage msgIdLanguage) { const wxString msgIdLang = wxLocale::GetLanguageCanonicalName(msgIdLanguage); - const wxString domain_lang = ChooseLanguageForDomain(domain, msgIdLang); + const wxString domain_lang = GetBestTranslation(domain, msgIdLang); if ( domain_lang.empty() ) { @@ -1423,18 +1572,30 @@ bool wxTranslations::IsLoaded(const wxString& domain) const return FindCatalog(domain) != NULL; } +wxString wxTranslations::GetBestTranslation(const wxString& domain, + wxLanguage msgIdLanguage) +{ + const wxString lang = wxLocale::GetLanguageCanonicalName(msgIdLanguage); + return GetBestTranslation(domain, lang); +} -wxString wxTranslations::ChooseLanguageForDomain(const wxString& WXUNUSED(domain), - const wxString& WXUNUSED(msgIdLang)) +wxString wxTranslations::GetBestTranslation(const wxString& domain, + const wxString& msgIdLanguage) { // explicitly set language should always be respected if ( !m_lang.empty() ) return m_lang; - // TODO: if the default language is used, pick the best (by comparing - // available languages with user's preferences), instead of blindly - // trusting availability of system language translation - return wxLocale::GetLanguageCanonicalName(wxLocale::GetSystemLanguage()); + wxArrayString available(GetAvailableTranslations(domain)); + // it's OK to have duplicates, so just add msgid language + available.push_back(msgIdLanguage); + available.push_back(msgIdLanguage.BeforeFirst('_')); + + wxLogTrace(TRACE_I18N, "choosing best language for domain '%s'", domain); + LogTraceArray(" - available translations", available); + const wxString lang = GetPreferredUILanguage(available); + wxLogTrace(TRACE_I18N, " => using language '%s'", lang); + return lang; }