// SPDX-License-Identifier: LGPL-2.1-or-later /*************************************************************************** * Copyright (c) 2004 Werner Mayer * * * * This file is part of the FreeCAD CAx development system. * * * * This library is free software; you can redistribute it and/or * * modify it under the terms of the GNU Library General Public * * License as published by the Free Software Foundation; either * * version 2 of the License, or (at your option) any later version. * * * * This library is distributed in the hope that it will be useful, * * but WITHOUT ANY WARRANTY; without even the implied warranty of * * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * * GNU Library General Public License for more details. * * * * You should have received a copy of the GNU Library General Public * * License along with this library; see the file COPYING.LIB. If not, * * write to the Free Software Foundation, Inc., 59 Temple Place, * * Suite 330, Boston, MA 02111-1307, USA * * * ***************************************************************************/ #include #include #include #include #include #include #include #include #include #include #include "Translator.h" using namespace Gui; /** \defgroup i18n Internationalization with FreeCAD * \ingroup GUI * * The internationalization of FreeCAD makes heavy use of the internationalization * support of Qt. For more details refer to your Qt documentation. * * \section stepbystep_language Step by step * To integrate a new language into FreeCAD or one of its application modules * you have to perform the following steps: * * \subsection tsfile Creation of a .ts file * First you have to generate a .ts file for the language to be translated. You can do this * by running the \a lupdate tool in the \a bin path of your Qt installation. As argument * you can specify either all related source files and the .ts output file or a Qt project * file (.pro) which contains all relevant source files. * * \subsection translate Translation into your language * To translate the english string literals into the language you want to support you can open your * .ts file with \a QtLinguist and translate all literals by hand. Another way * for translation is to use the tool \a tsauto from Sebastien Fricker.This tool uses the * engine from Google web page (www.google.com). tsauto supports the languages * \li english * \li french * \li german * \li italian * \li portuguese and * \li spanish * * \remark To get most of the literals translated you should have removed all * special characters (like &, !, ?, ...). Otherwise the translation could fail. * After having translated all literals you can load the .ts file into QtLinguist and * invoke the menu item \a Release which generates the binary .qm file. * * \subsection usets Integration of the .qm file * The .qm file should now be integrated into the GUI library (either of FreeCAD * itself or its application module). The .qm file will be embedded into the * resulting binary file. So, at runtime you don't need any .qm files any * more. Indeed you will have a bigger binary file but you haven't any troubles * concerning missing .qm files. * * To integrate the .qm file into the executable you have to create a resource file (.qrc), first. * This is an XML file where you can append the .qm file. For the .qrc file you have to define the * following curstom build step inside the Visual Studio project file: * * Command Line: rcc.exe -name $(InputName) $(InputPath) -o "$(InputDir)qrc_$(InputName).cpp" * Outputs: $(InputDir)qrc_$(InputName).cpp * * For the gcc build system you just have to add the line \.qrc to the BUILT_SOURCES * sources section of the Makefile.am, run automake and configure (or ./confog.status) afterwards. * * Finally, you have to add a the line * \code * * Q_INIT_RESOURCE(resource); * * \endcode * * where \a resource is the name of the .qrc file. That's all! */ /* TRANSLATOR Gui::Translator */ Translator* Translator::_pcSingleton = nullptr; namespace Gui { class TranslatorP { public: std::string activatedLanguage; /**< Active language */ std::map mapLanguageTopLevelDomain; TStringMap mapSupportedLocales; std::list translators; /**< A list of all created translators */ QStringList paths; }; } // namespace Gui class Translator::ParameterObserver: public ParameterGrp::ObserverType { public: ParameterObserver(Translator* client) : client(client) { hGrp = App::GetApplication().GetParameterGroupByPath( "User parameter:BaseApp/Preferences/General" ); hGrp->Attach(this); } void OnChange(Base::Subject& caller, const char* creason) override { (void)caller; std::string_view reason = creason; if (reason == "UseLocaleFormatting") { int format = hGrp->GetInt("UseLocaleFormatting"); if (format == 0) { client->setLocale(); // Defaults to system locale } else if (format == 1) { // Language must need to be set before locale. How do we ensure this? std::string language = hGrp->GetASCII("Language"); client->setLocale(language); } else if (format == 2) { client->setLocale("C"); } else { throw Base::ValueError("Parameter \"UseLocaleFormatting\" value out of bounds for Translator::formattingOptions"); } } else if (reason == "SubstituteDecimalSeparator") { bool value = hGrp->GetBool("SubstituteDecimal"); client->enableDecimalPointConversion(value); } } Translator* client; static ParameterGrp::handle hGrp; }; ParameterGrp::handle Translator::ParameterObserver::hGrp; // definition for export Translator* Translator::instance() { if (!_pcSingleton) { _pcSingleton = new Translator; } return _pcSingleton; } void Translator::destruct() { if (_pcSingleton) { delete _pcSingleton; } _pcSingleton = nullptr; } Translator::Translator() { observer = std::make_unique(this); // This is needed for Qt's lupdate // clang-format off d = new TranslatorP; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Afrikaans" )] = "af"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Arabic" )] = "ar"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Basque" )] = "eu"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Belarusian" )] = "be"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Bulgarian" )] = "bg"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Catalan" )] = "ca"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Chinese (Simplified)" )] = "zh-CN"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Chinese (Traditional)" )] = "zh-TW"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Croatian" )] = "hr"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Czech" )] = "cs"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Danish" )] = "da"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Dutch" )] = "nl"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("English" )] = "en"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Filipino" )] = "fil"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Finnish" )] = "fi"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("French" )] = "fr"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Galician" )] = "gl"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Georgian" )] = "ka"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("German" )] = "de"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Greek" )] = "el"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Hungarian" )] = "hu"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Indonesian" )] = "id"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Italian" )] = "it"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Japanese" )] = "ja"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Kabyle" )] = "kab"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Korean" )] = "ko"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Lithuanian" )] = "lt"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Norwegian" )] = "no"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Polish" )] = "pl"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Portuguese (Brazilian)")] = "pt-BR"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Portuguese" )] = "pt-PT"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Romanian" )] = "ro"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Russian" )] = "ru"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Serbian" )] = "sr"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Serbian (Latin)" )] = "sr-CS"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Slovak" )] = "sk"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Slovenian" )] = "sl"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Spanish" )] = "es-ES"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Spanish (Argentina)" )] = "es-AR"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Swedish" )] = "sv-SE"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Turkish" )] = "tr"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Ukrainian" )] = "uk"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Valencian" )] = "val-ES"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Vietnamese" )] = "vi"; d->mapLanguageTopLevelDomain[QT_TR_NOOP("Malay")] = "ms"; auto hGrp = App::GetApplication().GetParameterGroupByPath("User parameter:BaseApp/Preferences/General"); auto entries = hGrp->GetASCII("AdditionalLanguageDomainEntries", ""); // The format of the entries is "Language Name 1"="code1";"Language Name 2"="code2";... // Example: "Romanian"="ro";"Polish"="pl"; QRegularExpression matchingRE(QStringLiteral("\"(.*[^\\s]+.*)\"\\s*=\\s*\"([^\\s]+)\";?")); auto matches = matchingRE.globalMatch(QString::fromStdString(entries)); while (matches.hasNext()) { QRegularExpressionMatch match = matches.next(); QString language = match.captured(1); QString tld = match.captured(2); d->mapLanguageTopLevelDomain[language.toStdString()] = tld.toStdString(); } // clang-format on d->activatedLanguage = "English"; d->paths = directories(); enableDecimalPointConversion(hGrp->GetBool("SubstituteDecimalSeparator", false)); } Translator::~Translator() { removeTranslators(); delete d; } TStringList Translator::supportedLanguages() const { TStringList languages; TStringMap locales = supportedLocales(); for (const auto& it : locales) { languages.push_back(it.first); } return languages; } TStringMap Translator::supportedLocales() const { if (!d->mapSupportedLocales.empty()) { return d->mapSupportedLocales; } // List all .qm files for (const auto& domainMap : d->mapLanguageTopLevelDomain) { for (const auto& directoryName : std::as_const(d->paths)) { QDir dir(directoryName); QString filter = QStringLiteral("*_%1.qm").arg(QString::fromStdString(domainMap.second)); QStringList fileNames = dir.entryList(QStringList(filter), QDir::Files, QDir::Name); if (!fileNames.isEmpty()) { d->mapSupportedLocales[domainMap.first] = domainMap.second; break; } } } return d->mapSupportedLocales; } void Translator::activateLanguage(const char* lang) { removeTranslators(); // remove the currently installed translators d->activatedLanguage = lang; TStringList languages = supportedLanguages(); if (std::ranges::find(languages, lang) != languages.end()) { refresh(); } } std::string Translator::activeLanguage() const { return d->activatedLanguage; } std::string Translator::locale(const std::string& lang) const { std::string loc; std::map::const_iterator tld = d->mapLanguageTopLevelDomain.find(lang); if (tld != d->mapLanguageTopLevelDomain.end()) { loc = tld->second; } return loc; } void Translator::setLocale(const std::string& language) const { auto loc = QLocale::system(); // Defaulting to OS locale if (language == "C" || language == "c") { loc = QLocale::c(); } else { auto bcp47 = locale(language); if (!bcp47.empty()) { loc = QLocale(QString::fromStdString(bcp47)); } } QLocale::setDefault(loc); updateLocaleChange(); #ifdef FC_DEBUG Base::Console() .log("Locale changed to %s => %s\n", qPrintable(loc.bcp47Name()), qPrintable(loc.name())); #endif } void Translator::updateLocaleChange() const { for (auto& topLevelWidget : qApp->topLevelWidgets()) { topLevelWidget->setLocale(QLocale()); } } QStringList Translator::directories() const { QStringList list; auto dir = App::GetApplication() .GetParameterGroupByPath("User parameter:BaseApp/Preferences/General") ->GetASCII("AdditionalTranslationsDirectory", ""); if (!dir.empty()) { list.push_back(QString::fromStdString(dir)); } QDir home(QString::fromUtf8(App::Application::getUserAppDataDir().c_str())); list.push_back(home.absoluteFilePath(QLatin1String("translations"))); QDir resc(QString::fromUtf8(App::Application::getResourceDir().c_str())); list.push_back(resc.absoluteFilePath(QLatin1String("translations"))); list.push_back(QLatin1String(":/translations")); return list; } void Translator::addPath(const QString& path) { d->paths.push_back(path); } void Translator::installQMFiles(const QDir& dir, const char* locale) { QString filter = QStringLiteral("*_%1.qm").arg(QLatin1String(locale)); QStringList fileNames = dir.entryList(QStringList(filter), QDir::Files, QDir::Name); for (const auto& it : fileNames) { bool ok = false; for (std::list::const_iterator tt = d->translators.begin(); tt != d->translators.end(); ++tt) { if ((*tt)->objectName() == it) { ok = true; // this file is already installed break; } } // okay, we need to install this file if (!ok) { auto translator = new QTranslator; translator->setObjectName(it); if (translator->load(dir.filePath(it))) { qApp->installTranslator(translator); d->translators.push_back(translator); } else { delete translator; } } } } /** * This method checks for newly added (internal) .qm files which might be added at runtime. This * e.g. happens if a plugin gets loaded at runtime. For each newly added files that supports the * currently set language a new translator object is created to load the file. */ void Translator::refresh() { std::map::iterator tld = d->mapLanguageTopLevelDomain.find( d->activatedLanguage ); if (tld == d->mapLanguageTopLevelDomain.end()) { return; // no language activated } for (const QString& it : d->paths) { QDir dir(it); installQMFiles(dir, tld->second.c_str()); } } /** * Uninstalls all translators. */ void Translator::removeTranslators() { for (QTranslator* it : d->translators) { qApp->removeTranslator(it); delete it; } d->translators.clear(); } bool Translator::eventFilter(QObject* obj, QEvent* ev) { if (ev->type() == QEvent::KeyPress || ev->type() == QEvent::KeyRelease) { QKeyEvent* kev = static_cast(ev); Qt::KeyboardModifiers mod = kev->modifiers(); int key = kev->key(); if ((mod & Qt::KeypadModifier) && (key == Qt::Key_Period || key == Qt::Key_Comma)) { if (ev->spontaneous()) { auto dp = QString(QLocale().decimalPoint()); #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0) int dpcode = QKeySequence(dp)[0]; #else int dpcode = QKeySequence(dp)[0].key(); #endif if (kev->text() != dp) { QKeyEvent modifiedKeyEvent( kev->type(), dpcode, mod, dp, kev->isAutoRepeat(), kev->count() ); qApp->sendEvent(obj, &modifiedKeyEvent); return true; } } if (dynamic_cast(obj) && key != Qt::Key_Period) { QKeyEvent modifiedKeyEvent( kev->type(), Qt::Key_Period, mod, QChar::fromLatin1('.'), kev->isAutoRepeat(), kev->count() ); qApp->sendEvent(obj, &modifiedKeyEvent); return true; } } } return false; } void Translator::enableDecimalPointConversion(bool on) { if (!qApp) { return; } if (!on) { decimalPointConverter.reset(); return; } #if FC_DEBUG if (on && decimalPointConverter) { Base::Console().instance().warning( "Translator: decimal point converter is already installed\n" ); } #endif if (on && !decimalPointConverter) { decimalPointConverter = std::unique_ptr>( this, [](Translator* evFilter) { qApp->removeEventFilter(evFilter); } ); qApp->installEventFilter(decimalPointConverter.get()); } } bool Translator::isEnabledDecimalPointConversion() const { return static_cast(decimalPointConverter); } #include "moc_Translator.cpp"