From c387a4b5c9205bedd046ec81722e0ef2746344a7 Mon Sep 17 00:00:00 2001 From: Chris Hennes Date: Sun, 22 Dec 2024 10:47:43 -0600 Subject: [PATCH] App: Create addTranslatableExportType method This method aims to solve the problem of untranslatable description strings on file types. Some, but not all, file format description strings contain translatable elements. For example, some languages may translate "FEM mesh formats". Many format descriptions include the words "file," "format," or "mesh" -- these could potentially be replaced with localized versions. --- src/App/Application.cpp | 89 ++++++++++++++++++++++++++++++++++++++- src/App/Application.h | 17 ++++++++ src/App/ApplicationPy.cpp | 47 +++++++++++++++++++++ src/App/ApplicationPy.h | 1 + src/Gui/MainWindow.cpp | 3 ++ 5 files changed, 156 insertions(+), 1 deletion(-) diff --git a/src/App/Application.cpp b/src/App/Application.cpp index 0f86fbf69e..657325578d 100644 --- a/src/App/Application.cpp +++ b/src/App/Application.cpp @@ -1452,6 +1452,93 @@ void Application::addExportType(const char* filter, const char* moduleName) } } +namespace { + // To enable changing languages while the program is running, cache the translatable export type + // entries so that their addition can be "replayed" when the language changes (after removing + // the originals). + + struct TranslatableTypeCacheEntry { + std::string description; + const std::vector extensions; + std::string moduleName; + }; + + class TranslatableTypeCache { + public: + TranslatableTypeCache() = default; + void addCacheEntry(TranslatableTypeCacheEntry entry) { + _cache.push_back(std::move(entry)); + } + std::vector getCache() const { + return _cache; + } + void clear() + { + _cache.clear(); + } + private: + std::vector _cache; + }; + + TranslatableTypeCache translatableExportTypeCache; + + // Given a description string and a list of extensions, construct a type string that Qt's file + // dialogs will recognize + void appendTypeString(std::string &description, const std::vector &extensions) { + description = fmt::format("{} (*.{})", description, fmt::join(extensions, " *.")); + } +} + +void Application::addTranslatableExportType(const std::string &description, + const std::vector &extensions, + const std::string &moduleName) +{ + assert(!extensions.empty()); // Programming error, there must be extensions + + // Branding: replace "FreeCAD" in a file type description with the branded application name + auto replaceFreeCAD = + [](std::string& s) + { + constexpr std::string_view freecad = "FreeCAD"; + if (auto pos = s.find(freecad); pos != std::string::npos) { + s.replace(pos, freecad.size(), getExecutableName()); + return true; // Contained the app name + } + return false; // Did NOT contain the app name + }; + + translatableExportTypeCache.addCacheEntry({description, extensions, moduleName}); + auto translatedDescription = QCoreApplication::translate("FileFormat", description.c_str()).toStdString(); + bool containsAppName = replaceFreeCAD(translatedDescription); // Run *AFTER* translation + appendTypeString(translatedDescription, extensions); + + FileTypeItem item; + item.filter = translatedDescription; + item.module = moduleName; + item.types = extensions; + item.translatable = true; + + if (containsAppName) { + // put to the front of the array + _mExportTypes.insert(_mExportTypes.begin(),std::move(item)); + } + else { + _mExportTypes.push_back(std::move(item)); + } +} + +void Application::retranslateExportTypes() +{ + auto cache = translatableExportTypeCache.getCache(); + translatableExportTypeCache.clear(); + std::erase_if(_mExportTypes, [](const FileTypeItem& item) { + return item.translatable; + }); + for (const auto &cacheEntry : translatableExportTypeCache.getCache()) { + addTranslatableExportType(cacheEntry.description, cacheEntry.extensions, cacheEntry.moduleName); + } +} + void Application::changeExportModule(const char* filter, const char* oldModuleName, const char* newModuleName) { for (auto& it : _mExportTypes) { @@ -3618,7 +3705,7 @@ void Application::getVerboseCommonInfo(QTextStream& str, const std::map &extensions, + const std::string &moduleName); + + /// Intended to be called when the language is changed, this retranslates the export type. + void retranslateExportTypes(); + /** * @copydoc changeImportModule */ @@ -995,6 +1011,7 @@ private: std::string filter; std::string module; std::vector types; + bool translatable = false; }; // open ending information diff --git a/src/App/ApplicationPy.cpp b/src/App/ApplicationPy.cpp index dfa64df930..bb015f1b08 100644 --- a/src/App/ApplicationPy.cpp +++ b/src/App/ApplicationPy.cpp @@ -89,6 +89,12 @@ PyMethodDef ApplicationPy::Methods[] = { (PyCFunction)ApplicationPy::sAddExportType, METH_VARARGS, "Register filetype for export"}, + {"addTranslatableExportType", + (PyCFunction)ApplicationPy::sAddTranslatableExportType, + METH_VARARGS, + "addTranslatableExportType(description:str, extensions:list[str], module_name:str)\n\n" + "Register filetype with translatable description for export. Description should be a\n" + "string registered with the translation system using the 'FileFormat' context."}, {"changeExportModule", (PyCFunction)ApplicationPy::sChangeExportModule, METH_VARARGS, @@ -751,6 +757,47 @@ PyObject* ApplicationPy::sAddExportType(PyObject* /*self*/, PyObject* args) Py_Return; } +PyObject* ApplicationPy::sAddTranslatableExportType(PyObject* /*self*/, PyObject* args) +{ + char *description {}; + PyObject *pyExtensions {}; + char *moduleName {}; + + if (!PyArg_ParseTuple(args, "sOs", &description, &pyExtensions, &moduleName)) { + return nullptr; + } + + if (!PyList_Check(pyExtensions)) { + PyErr_SetString(PyExc_TypeError, + "Expected a list of strings as second argument"); + return nullptr; + } + + Py_ssize_t n = PyList_Size(pyExtensions); + + std::vector extensions; + for (Py_ssize_t i = 0; i < n; ++i) { + PyObject *item = PyList_GetItem(pyExtensions, i); + + if (!PyUnicode_Check(item)) { + PyErr_SetString(PyExc_TypeError, + "Extensions list elements must be strings"); + return nullptr; + } + + const char *value = PyUnicode_AsUTF8(item); + if (!value) { + return nullptr; + } + + extensions.emplace_back(value); + } + + GetApplication().addTranslatableExportType(description, extensions, moduleName); + + Py_Return; +} + PyObject* ApplicationPy::sChangeExportModule(PyObject* /*self*/, PyObject* args) { char *key {}; diff --git a/src/App/ApplicationPy.h b/src/App/ApplicationPy.h index ba94ee71b0..c7393326ac 100644 --- a/src/App/ApplicationPy.h +++ b/src/App/ApplicationPy.h @@ -48,6 +48,7 @@ public: static PyObject* sChangeImportModule (PyObject *self, PyObject *args); static PyObject* sGetImportType (PyObject *self, PyObject *args); static PyObject* sAddExportType (PyObject *self, PyObject *args); + static PyObject* sAddTranslatableExportType (PyObject *self, PyObject *args); static PyObject* sChangeExportModule (PyObject *self, PyObject *args); static PyObject* sGetExportType (PyObject *self, PyObject *args); static PyObject* sGetResourcePath (PyObject *self, PyObject *args); diff --git a/src/Gui/MainWindow.cpp b/src/Gui/MainWindow.cpp index f2b11c80bc..0531f323b5 100644 --- a/src/Gui/MainWindow.cpp +++ b/src/Gui/MainWindow.cpp @@ -2512,6 +2512,9 @@ void MainWindow::changeEvent(QEvent* e) if (wb) { wb->retranslate(); } + + // reload all translatable export type strings: + App::GetApplication().retranslateExportTypes(); } else if (e->type() == QEvent::ActivationChange) { if (isActiveWindow()) {