// ************************************************************************** // * This file is part of the FreeFileSync project. It is distributed under * // * GNU General Public License: http://www.gnu.org/licenses/gpl.html * // * Copyright (C) Zenju (zenju AT gmx DOT de) - All Rights Reserved * // ************************************************************************** #include "localization.h" #include #include #include #include #include #include #include #include #include #include #include "parse_plural.h" #include "parse_lng.h" #include "ffs_paths.h" #ifdef ZEN_LINUX #include //wcscasecmp #elif defined ZEN_MAC #include #endif using namespace zen; namespace { class FFSTranslation : public TranslationHandler { public: FFSTranslation(const Zstring& filename, wxLanguage languageId); //throw lngfile::ParsingError, parse_plural::ParsingError wxLanguage langId() const { return langId_; } virtual std::wstring translate(const std::wstring& text) override { //look for translation in buffer table auto it = transMapping.find(text); if (it != transMapping.end() && !it->second.empty()) return it->second; return text; //fallback } virtual std::wstring translate(const std::wstring& singular, const std::wstring& plural, std::int64_t n) override { auto it = transMappingPl.find(std::make_pair(singular, plural)); if (it != transMappingPl.end()) { const size_t formNo = pluralParser->getForm(n); if (formNo < it->second.size()) return replaceCpy(it->second[formNo], L"%x", toGuiString(n)); } return replaceCpy(std::abs(n) == 1 ? singular : plural, L"%x", toGuiString(n)); //fallback } private: typedef hash_map Translation; //hash_map is 15% faster than std::map on GCC typedef std::map, std::vector> TranslationPlural; Translation transMapping; //map original text |-> translation TranslationPlural transMappingPl; std::unique_ptr pluralParser; //bound! wxLanguage langId_; }; FFSTranslation::FFSTranslation(const Zstring& filename, wxLanguage languageId) : langId_(languageId) //throw lngfile::ParsingError, parse_plural::ParsingError { std::string inputStream; try { inputStream = loadBinStream(filename); //throw FileError } catch (const FileError& e) { throw lngfile::ParsingError(e.toString(), 0, 0); //passing FileError is too high a level for Parsing error, OTOH user is unlikely to see this since file I/O issues are sorted out by ExistingTranslations()! } lngfile::TransHeader header; lngfile::TranslationMap transInput; lngfile::TranslationPluralMap transPluralInput; lngfile::parseLng(inputStream, header, transInput, transPluralInput); //throw ParsingError for (const auto& item : transInput) { const std::wstring original = utfCvrtTo(item.first); const std::wstring translation = utfCvrtTo(item.second); transMapping.insert(std::make_pair(original, translation)); } for (const auto& item : transPluralInput) { const std::wstring engSingular = utfCvrtTo(item.first.first); const std::wstring engPlural = utfCvrtTo(item.first.second); std::vector plFormsWide; for (const std::string& pf : item.second) plFormsWide.push_back(utfCvrtTo(pf)); transMappingPl.insert(std::make_pair(std::make_pair(engSingular, engPlural), plFormsWide)); } pluralParser = make_unique(header.pluralDefinition); //throw parse_plural::ParsingError } class FindLngfiles : public zen::TraverseCallback { public: FindLngfiles(std::vector& lngFiles) : lngFiles_(lngFiles) {} virtual void onFile(const Zchar* shortName, const Zstring& fullName, const FileInfo& details) { if (endsWith(fullName, Zstr(".lng"))) lngFiles_.push_back(fullName); } virtual HandleLink onSymlink(const Zchar* shortName, const Zstring& fullName, const SymlinkInfo& details) { return LINK_SKIP; } virtual TraverseCallback* onDir(const Zchar* shortName, const Zstring& fullName) { return nullptr; } virtual HandleError reportDirError (const std::wstring& msg, size_t retryNumber) { assert(false); return ON_ERROR_IGNORE; } //errors are not really critical in this context virtual HandleError reportItemError(const std::wstring& msg, size_t retryNumber, const Zchar* shortName) { assert(false); return ON_ERROR_IGNORE; } // private: std::vector& lngFiles_; }; struct LessTranslation { bool operator()(const ExistingTranslations::Entry& lhs, const ExistingTranslations::Entry& rhs) const { //use a more "natural" sort: ignore case and diacritics #ifdef ZEN_WIN const int rv = ::CompareString(LOCALE_USER_DEFAULT, //__in LCID Locale, NORM_IGNORECASE, //__in DWORD dwCmpFlags, lhs.languageName.c_str(), //__in LPCTSTR lpString1, static_cast(lhs.languageName.size()), //__in int cchCount1, rhs.languageName.c_str(), //__in LPCTSTR lpString2, static_cast(rhs.languageName.size())); //__in int cchCount2 if (rv == 0) throw std::runtime_error("Error comparing strings."); else return rv == CSTR_LESS_THAN; //convert to C-style string compare result #elif defined ZEN_LINUX return ::wcscasecmp(lhs.languageName.c_str(), rhs.languageName.c_str()) < 0; //ignores case; locale-dependent! //return lhs.languageName.CmpNoCase(rhs.languageName) < 0; #elif defined ZEN_MAC auto allocCFStringRef = [](const std::wstring& str) -> CFStringRef //output not owned! { return ::CFStringCreateWithCString(nullptr, //CFAllocatorRef alloc, utfCvrtTo(str).c_str(), //const char *cStr, kCFStringEncodingUTF8); //CFStringEncoding encoding }; CFStringRef langL = allocCFStringRef(lhs.languageName); ZEN_ON_SCOPE_EXIT(::CFRelease(langL)); CFStringRef langR = allocCFStringRef(rhs.languageName); ZEN_ON_SCOPE_EXIT(::CFRelease(langR)); return::CFStringCompare(langL, langR, kCFCompareLocalized | kCFCompareCaseInsensitive) == kCFCompareLessThan; //no-fail #endif } }; } ExistingTranslations::ExistingTranslations() { { //default entry: ExistingTranslations::Entry newEntry; newEntry.languageID = wxLANGUAGE_ENGLISH_US; newEntry.languageName = L"English (US)"; newEntry.languageFile = L""; newEntry.translatorName = L"Zenju"; newEntry.languageFlag = L"flag_usa.png"; locMapping.push_back(newEntry); } //search language files available std::vector lngFiles; FindLngfiles traverseCallback(lngFiles); traverseFolder(zen::getResourceDir() + Zstr("Languages"), //throw(); traverseCallback); for (const Zstring& filename : lngFiles) { try { const std::string stream = loadBinStream(filename); //throw FileError lngfile::TransHeader lngHeader; lngfile::parseHeader(stream, lngHeader); //throw ParsingError assert(!lngHeader.languageName .empty()); assert(!lngHeader.translatorName.empty()); assert(!lngHeader.localeName .empty()); assert(!lngHeader.flagFile .empty()); /* There is some buggy behavior in wxWidgets which maps "zh_TW" to simplified chinese. Fortunately locales can be also entered as description. I changed to "Chinese (Traditional)" which works fine. */ if (const wxLanguageInfo* locInfo = wxLocale::FindLanguageInfo(utfCvrtTo(lngHeader.localeName))) { ExistingTranslations::Entry newEntry; newEntry.languageID = locInfo->Language; newEntry.languageName = utfCvrtTo(lngHeader.languageName); newEntry.languageFile = utfCvrtTo(filename); newEntry.translatorName = utfCvrtTo(lngHeader.translatorName); newEntry.languageFlag = utfCvrtTo(lngHeader.flagFile); locMapping.push_back(newEntry); } else assert(false); } catch (FileError&) { assert(false); } catch (lngfile::ParsingError&) { assert(false); } //better not show an error message here; scenario: batch jobs } std::sort(locMapping.begin(), locMapping.end(), LessTranslation()); } const std::vector& ExistingTranslations::get() { static ExistingTranslations instance; return instance.locMapping; } namespace { wxLanguage mapLanguageDialect(wxLanguage language) { switch (static_cast(language)) //avoid enumeration value wxLANGUAGE_*' not handled in switch [-Wswitch-enum] { //variants of wxLANGUAGE_ARABIC case wxLANGUAGE_ARABIC_ALGERIA: case wxLANGUAGE_ARABIC_BAHRAIN: case wxLANGUAGE_ARABIC_EGYPT: case wxLANGUAGE_ARABIC_IRAQ: case wxLANGUAGE_ARABIC_JORDAN: case wxLANGUAGE_ARABIC_KUWAIT: case wxLANGUAGE_ARABIC_LEBANON: case wxLANGUAGE_ARABIC_LIBYA: case wxLANGUAGE_ARABIC_MOROCCO: case wxLANGUAGE_ARABIC_OMAN: case wxLANGUAGE_ARABIC_QATAR: case wxLANGUAGE_ARABIC_SAUDI_ARABIA: case wxLANGUAGE_ARABIC_SUDAN: case wxLANGUAGE_ARABIC_SYRIA: case wxLANGUAGE_ARABIC_TUNISIA: case wxLANGUAGE_ARABIC_UAE: case wxLANGUAGE_ARABIC_YEMEN: return wxLANGUAGE_ARABIC; //variants of wxLANGUAGE_CHINESE_SIMPLIFIED case wxLANGUAGE_CHINESE: case wxLANGUAGE_CHINESE_SINGAPORE: return wxLANGUAGE_CHINESE_SIMPLIFIED; //variants of wxLANGUAGE_CHINESE_TRADITIONAL case wxLANGUAGE_CHINESE_TAIWAN: case wxLANGUAGE_CHINESE_HONGKONG: case wxLANGUAGE_CHINESE_MACAU: return wxLANGUAGE_CHINESE_TRADITIONAL; //variants of wxLANGUAGE_DUTCH case wxLANGUAGE_DUTCH_BELGIAN: return wxLANGUAGE_DUTCH; //variants of wxLANGUAGE_ENGLISH_UK case wxLANGUAGE_ENGLISH_AUSTRALIA: case wxLANGUAGE_ENGLISH_NEW_ZEALAND: case wxLANGUAGE_ENGLISH_TRINIDAD: case wxLANGUAGE_ENGLISH_CARIBBEAN: case wxLANGUAGE_ENGLISH_JAMAICA: case wxLANGUAGE_ENGLISH_BELIZE: case wxLANGUAGE_ENGLISH_EIRE: case wxLANGUAGE_ENGLISH_SOUTH_AFRICA: case wxLANGUAGE_ENGLISH_ZIMBABWE: case wxLANGUAGE_ENGLISH_BOTSWANA: case wxLANGUAGE_ENGLISH_DENMARK: return wxLANGUAGE_ENGLISH_UK; //variants of wxLANGUAGE_ENGLISH_US case wxLANGUAGE_ENGLISH: case wxLANGUAGE_ENGLISH_CANADA: case wxLANGUAGE_ENGLISH_PHILIPPINES: return wxLANGUAGE_ENGLISH_US; //variants of wxLANGUAGE_FRENCH case wxLANGUAGE_FRENCH_BELGIAN: case wxLANGUAGE_FRENCH_CANADIAN: case wxLANGUAGE_FRENCH_LUXEMBOURG: case wxLANGUAGE_FRENCH_MONACO: case wxLANGUAGE_FRENCH_SWISS: return wxLANGUAGE_FRENCH; //variants of wxLANGUAGE_GERMAN case wxLANGUAGE_GERMAN_AUSTRIAN: case wxLANGUAGE_GERMAN_BELGIUM: case wxLANGUAGE_GERMAN_LIECHTENSTEIN: case wxLANGUAGE_GERMAN_LUXEMBOURG: case wxLANGUAGE_GERMAN_SWISS: return wxLANGUAGE_GERMAN; //variants of wxLANGUAGE_ITALIAN case wxLANGUAGE_ITALIAN_SWISS: return wxLANGUAGE_ITALIAN; //variants of wxLANGUAGE_NORWEGIAN_BOKMAL case wxLANGUAGE_NORWEGIAN_NYNORSK: return wxLANGUAGE_NORWEGIAN_BOKMAL; //variants of wxLANGUAGE_ROMANIAN case wxLANGUAGE_MOLDAVIAN: return wxLANGUAGE_ROMANIAN; //variants of wxLANGUAGE_RUSSIAN case wxLANGUAGE_RUSSIAN_UKRAINE: return wxLANGUAGE_RUSSIAN; //variants of wxLANGUAGE_SERBIAN case wxLANGUAGE_SERBIAN_CYRILLIC: case wxLANGUAGE_SERBIAN_LATIN: case wxLANGUAGE_SERBO_CROATIAN: return wxLANGUAGE_SERBIAN; //variants of wxLANGUAGE_SPANISH case wxLANGUAGE_SPANISH_ARGENTINA: case wxLANGUAGE_SPANISH_BOLIVIA: case wxLANGUAGE_SPANISH_CHILE: case wxLANGUAGE_SPANISH_COLOMBIA: case wxLANGUAGE_SPANISH_COSTA_RICA: case wxLANGUAGE_SPANISH_DOMINICAN_REPUBLIC: case wxLANGUAGE_SPANISH_ECUADOR: case wxLANGUAGE_SPANISH_EL_SALVADOR: case wxLANGUAGE_SPANISH_GUATEMALA: case wxLANGUAGE_SPANISH_HONDURAS: case wxLANGUAGE_SPANISH_MEXICAN: case wxLANGUAGE_SPANISH_MODERN: case wxLANGUAGE_SPANISH_NICARAGUA: case wxLANGUAGE_SPANISH_PANAMA: case wxLANGUAGE_SPANISH_PARAGUAY: case wxLANGUAGE_SPANISH_PERU: case wxLANGUAGE_SPANISH_PUERTO_RICO: case wxLANGUAGE_SPANISH_URUGUAY: case wxLANGUAGE_SPANISH_US: case wxLANGUAGE_SPANISH_VENEZUELA: return wxLANGUAGE_SPANISH; //variants of wxLANGUAGE_SWEDISH case wxLANGUAGE_SWEDISH_FINLAND: return wxLANGUAGE_SWEDISH; //languages without variants: //case wxLANGUAGE_CROATIAN: //case wxLANGUAGE_CZECH: //case wxLANGUAGE_DANISH: //case wxLANGUAGE_FINNISH: //case wxLANGUAGE_GREEK: //case wxLANGUAGE_HEBREW: //case wxLANGUAGE_HUNGARIAN: //case wxLANGUAGE_JAPANESE: //case wxLANGUAGE_KOREAN: //case wxLANGUAGE_LITHUANIAN: //case wxLANGUAGE_POLISH: //case wxLANGUAGE_PORTUGUESE: //case wxLANGUAGE_PORTUGUESE_BRAZILIAN: //case wxLANGUAGE_SCOTS_GAELIC: //case wxLANGUAGE_SLOVENIAN: //case wxLANGUAGE_TURKISH: //case wxLANGUAGE_UKRAINIAN: default: return language; } } //global wxWidgets localization: sets up C localization runtime as well! class wxWidgetsLocale { public: static void init(wxLanguage lng) { locale.reset(); //avoid global locale lifetime overlap! wxWidgets cannot handle this and will crash! locale.reset(new wxLocale); const wxLanguageInfo* sysLngInfo = wxLocale::GetLanguageInfo(wxLocale::GetSystemLanguage()); const wxLanguageInfo* selLngInfo = wxLocale::GetLanguageInfo(lng); const bool sysLangIsRTL = sysLngInfo ? sysLngInfo->LayoutDirection == wxLayout_RightToLeft : false; const bool selectedLangIsRTL = selLngInfo ? selLngInfo->LayoutDirection == wxLayout_RightToLeft : false; #ifdef NDEBUG wxLogNull dummy; //rather than implementing a reasonable error handling wxWidgets decides to shows a modal dialog in wxLocale::Init -> at least we can shut it up! #endif if (sysLangIsRTL == selectedLangIsRTL) locale->Init(wxLANGUAGE_DEFAULT); //use sys-lang to preserve sub-language specific rules (e.g. german swiss number punctation) else locale->Init(lng); //have to use the supplied language to enable RTL layout different than user settings locLng = lng; } static void release() { locale.reset(); locLng = wxLANGUAGE_UNKNOWN; } static wxLanguage getLanguage() { return locLng; } private: static std::unique_ptr locale; static wxLanguage locLng; }; std::unique_ptr wxWidgetsLocale::locale; wxLanguage wxWidgetsLocale::locLng = wxLANGUAGE_UNKNOWN; } void zen::releaseWxLocale() { wxWidgetsLocale::release(); } void zen::setLanguage(int language) //throw FileError { if (language == getLanguage() && wxWidgetsLocale::getLanguage() == language) return; //support polling //(try to) retrieve language file std::wstring languageFile; for (auto it = ExistingTranslations::get().begin(); it != ExistingTranslations::get().end(); ++it) if (it->languageID == language) { languageFile = it->languageFile; break; } //load language file into buffer if (languageFile.empty()) //if languageFile is empty, texts will be english by default zen::setTranslator(); else try { zen::setTranslator(new FFSTranslation(utfCvrtTo(languageFile), static_cast(language))); //throw lngfile::ParsingError, parse_plural::ParsingError } catch (lngfile::ParsingError& e) { throw FileError(replaceCpy(replaceCpy(replaceCpy(_("Error parsing file %x, row %y, column %z."), L"%x", fmtFileName(utfCvrtTo(languageFile))), L"%y", numberTo(e.row_ + 1)), L"%z", numberTo(e.col_ + 1)) + L"\n\n" + e.msg_); } catch (parse_plural::ParsingError&) { throw FileError(L"Invalid plural form definition"); //user should never see this! } //handle RTL swapping: we need wxWidgets to do this wxWidgetsLocale::init(languageFile.empty() ? wxLANGUAGE_ENGLISH : static_cast(language)); } int zen::getLanguage() { const FFSTranslation* loc = dynamic_cast(zen::getTranslator()); return loc ? loc->langId() : wxLANGUAGE_ENGLISH_US; } int zen::retrieveSystemLanguage() { return mapLanguageDialect(static_cast(wxLocale::GetSystemLanguage())); }