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()) {