diff --git a/src/App/Application.cpp b/src/App/Application.cpp index d06e9586f2..02a8482e6e 100644 --- a/src/App/Application.cpp +++ b/src/App/Application.cpp @@ -1476,6 +1476,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) { @@ -3526,7 +3613,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 4c0bd34dfc..6392d794df 100644 --- a/src/App/ApplicationPy.h +++ b/src/App/ApplicationPy.h @@ -47,6 +47,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/FreeCADGuiInit.py b/src/Gui/FreeCADGuiInit.py index cada5dec75..0b3673fe39 100644 --- a/src/Gui/FreeCADGuiInit.py +++ b/src/Gui/FreeCADGuiInit.py @@ -49,6 +49,8 @@ import FreeCADGui Gui = FreeCADGui App = FreeCAD +translate = FreeCAD.Qt.translate + App.Console.PrintLog("Init: Running FreeCADGuiInit.py start script...\n") App.Console.PrintLog("░░░▀█▀░█▀█░▀█▀░▀█▀░░░█▀▀░█░█░▀█▀░░\n") App.Console.PrintLog("░░░░█░░█░█░░█░░░█░░░░█░█░█░█░░█░░░\n") @@ -459,7 +461,9 @@ FreeCAD.addExportType("Inventor V2.1 (*.iv)", "FreeCADGui") FreeCAD.addExportType("VRML V2.0 (*.wrl *.vrml *.wrz *.wrl.gz)", "FreeCADGui") FreeCAD.addExportType("X3D Extensible 3D (*.x3d *.x3dz)", "FreeCADGui") FreeCAD.addExportType("WebGL/X3D (*.xhtml)", "FreeCADGui") -FreeCAD.addExportType("Portable Document Format (*.pdf)", "FreeCADGui") +FreeCAD.addTranslatableExportType( + translate("FileFormat", "Portable Document Format"), ["pdf"], "FreeCADGui" +) # FreeCAD.addExportType("IDTF (for 3D PDF) (*.idtf)","FreeCADGui") # FreeCAD.addExportType("3D View (*.svg)","FreeCADGui") 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()) { diff --git a/src/Mod/BIM/Init.py b/src/Mod/BIM/Init.py index 82f90777b3..d8be0185fa 100644 --- a/src/Mod/BIM/Init.py +++ b/src/Mod/BIM/Init.py @@ -23,18 +23,17 @@ # *************************************************************************** # add import/export types - FreeCAD.addExportType("Industry Foundation Classes (*.ifc)", "importers.exportIFC") # FreeCAD.addImportType("Industry Foundation Classes (*.ifc)","importIFC") FreeCAD.addImportType("Industry Foundation Classes (*.ifc)", "nativeifc.ifc_import") FreeCAD.addExportType("Industry Foundation Classes - IFCJSON (*.ifcJSON)", "importers.exportIFC") -FreeCAD.addImportType("Wavefront OBJ - Arch module (*.obj *.OBJ)", "importers.importOBJ") -FreeCAD.addExportType("Wavefront OBJ - Arch module (*.obj)", "importers.importOBJ") -FreeCAD.addExportType("WebGL file (*.html)", "importers.importWebGL") -FreeCAD.addExportType("JavaScript Object Notation (*.json)", "importers.importJSON") +FreeCAD.addImportType("Wavefront OBJ - BIM (*.obj *.OBJ)", "importers.importOBJ") +FreeCAD.addExportType("Wavefront OBJ - BIM (*.obj)", "importers.importOBJ") +FreeCAD.addExportType("WebGL (*.html)", "importers.importWebGL") +FreeCAD.addExportType("JSON (*.json)", "importers.importJSON") FreeCAD.addImportType("Collada (*.dae *.DAE)", "importers.importDAE") FreeCAD.addExportType("Collada (*.dae)", "importers.importDAE") -FreeCAD.addImportType("3D Studio mesh (*.3ds *.3DS)", "importers.import3DS") +FreeCAD.addImportType("3D Studio mesh (*.3ds *3DS)", "importers.import3DS") FreeCAD.addImportType("SweetHome3D (*.sh3d)", "importers.importSH3D") FreeCAD.addImportType("Shapefile (*.shp *.SHP)", "importers.importSHP") diff --git a/src/Mod/Draft/Init.py b/src/Mod/Draft/Init.py index 2d751fb34b..4b78af839a 100644 --- a/src/Mod/Draft/Init.py +++ b/src/Mod/Draft/Init.py @@ -24,14 +24,16 @@ import FreeCAD as App +translate = FreeCAD.Qt.translate + # add Import/Export types App.addImportType("Autodesk DXF 2D (*.dxf *.DXF)", "importDXF") App.addImportType("SVG as geometry (*.svg *.SVG)", "importSVG") App.addImportType("Open CAD Format (*.oca *.gcad *.OCA *.GCAD)", "importOCA") App.addImportType("Common airfoil data (*.dat *.DAT)", "importAirfoilDAT") App.addExportType("Autodesk DXF 2D (*.dxf)", "importDXF") -App.addExportType("Flattened SVG (*.svg)", "importSVG") -App.addExportType("Open CAD Format (*.oca)", "importOCA") +App.addTranslatableExportType(translate("FileFormat", "Flattened SVG"), ["svg"], "importSVG") +App.addExportType("Open CAD (*.oca)", "importOCA") App.addImportType("Autodesk DWG 2D (*.dwg *.DWG)", "importDWG") App.addExportType("Autodesk DWG 2D (*.dwg)", "importDWG") diff --git a/src/Mod/Fem/Init.py b/src/Mod/Fem/Init.py index 33bbd3d613..18ac379e06 100644 --- a/src/Mod/Fem/Init.py +++ b/src/Mod/Fem/Init.py @@ -48,6 +48,8 @@ import FreeCAD from femtools.migrate_app import FemMigrateApp +translate = FreeCAD.Qt.translate + # migrate old FEM App objects sys.meta_path.append(FemMigrateApp()) @@ -57,35 +59,50 @@ FreeCAD.__unit_test__ += ["TestFemApp"] # add import and export file types -FreeCAD.addExportType("FEM mesh Python (*.meshpy)", "feminout.importPyMesh") +FreeCAD.addTranslatableExportType( + translate("FileFormat", "FEM mesh Python"), ["meshpy"], "feminout.importPyMesh" +) -FreeCAD.addExportType("FEM mesh TetGen (*.poly)", "feminout.convert2TetGen") +FreeCAD.addTranslatableExportType( + translate("FileFormat", "FEM mesh TetGen"), ["poly"], "feminout.convert2TetGen" +) # see FemMesh::read() and FemMesh::write() methods in src/Mod/Fem/App/FemMesh.cpp FreeCAD.addImportType( "FEM mesh formats (*.bdf *.BDF *.dat *.DAT *.inp *.INP *.med *.MED *.unv *.UNV *.vtk *.VTK *.vtu *.VTU *.pvtu *.PVTU *.z88 *.Z88)", "Fem", ) -FreeCAD.addExportType("FEM mesh formats (*.dat *.inp *.med *.stl *.unv *.vtk *.vtu *.z88)", "Fem") +FreeCAD.addTranslatableExportType( + translate("FileFormat", "FEM mesh formats"), + ["dat", "inp", "med", "stl", "unv", "vtk", "vtu", "z88"], + "Fem", +) -FreeCAD.addExportType("FEM mesh Nastran (*.bdf)", "feminout.exportNastranMesh") +FreeCAD.addTranslatableExportType( + translate("FileFormat", "FEM mesh Nastran"), ["bdf"], "feminout.exportNastranMesh" +) FreeCAD.addImportType("FEM result CalculiX (*.frd *.FRD)", "feminout.importCcxFrdResults") FreeCAD.addImportType("FEM mesh Fenics (*.xml *.XML *.xdmf *.XDMF)", "feminout.importFenicsMesh") -FreeCAD.addExportType("FEM mesh Fenics (*.xml *.xdmf)", "feminout.importFenicsMesh") +FreeCAD.addTranslatableExportType( + translate("FileFormat", "FEM mesh Fenics"), ["xml", "xdmf"], "feminout.importFenicsMesh" +) FreeCAD.addImportType( "FEM mesh YAML/JSON (*.meshyaml *.MESHYAML *.meshjson *.MESHJSON *.yaml *.YAML *.json *.JSON)", "feminout.importYamlJsonMesh", ) -FreeCAD.addExportType( - "FEM mesh YAML/JSON (*.meshyaml *.meshjson *.yaml *.json)", +FreeCAD.addTranslatableExportType( + translate("FileFormat", "FEM mesh YAML/JSON"), + ["meshyaml", "meshjson", "yaml", "json"], "feminout.importYamlJsonMesh", ) FreeCAD.addImportType("FEM mesh Z88 (*.txt *.TXT)", "feminout.importZ88Mesh") -FreeCAD.addExportType("FEM mesh Z88 (*.txt)", "feminout.importZ88Mesh") +FreeCAD.addTranslatableExportType( + translate("FileFormat", "FEM mesh Z88"), ["txt"], "feminout.importZ88Mesh" +) FreeCAD.addImportType("FEM result Z88 displacements (*.txt *.TXT)", "feminout.importZ88O2Results") @@ -94,6 +111,8 @@ if "BUILD_FEM_VTK" in FreeCAD.__cmake__: "FEM result VTK (*.vtk *.VTK *.vtu *.VTU *.pvtu *.PVTU *.vtm *.VTM, *.pvd)", "feminout.importVTKResults", ) - FreeCAD.addExportType( - "FEM result VTK (*.vtu *.vtp *.vts *.vtr *.vti *.vtm)", "feminout.importVTKResults" + FreeCAD.addTranslatableExportType( + translate("FileFormat", "FEM result VTK"), + ["vtu", "vtp", "vts", "vtr", "vti", "vtm"], + "feminout.importVTKResults", ) diff --git a/src/Mod/Import/Init.py b/src/Mod/Import/Init.py index 0c7004fa91..bfd121a8f2 100644 --- a/src/Mod/Import/Init.py +++ b/src/Mod/Import/Init.py @@ -26,6 +26,9 @@ # * * # ***************************************************************************/ +import FreeCAD + +translate = FreeCAD.Qt.translate # Append the open handler # FreeCAD.addImportType("STEP 214 (*.step *.stp)","ImportGui") @@ -34,5 +37,7 @@ FreeCAD.addImportType("PLMXML files (*.plmxml *.PLMXML)", "PlmXmlParser") FreeCAD.addImportType("STEPZ Zip File Type (*.stpZ *.stpz *.STPZ)", "stepZ") FreeCAD.addImportType("glTF (*.gltf *.GLTF *.glb *.GLB)", "ImportGui") -FreeCAD.addExportType("STEPZ zip File Type (*.stpZ *.stpz)", "stepZ") +FreeCAD.addTranslatableExportType( + translate("FileFormat", "STEPZ (Zipped STEP)"), ["stpZ", "stpz"], "stepZ" +) FreeCAD.addExportType("glTF (*.gltf *.glb)", "ImportGui") diff --git a/src/Mod/Mesh/Init.py b/src/Mod/Mesh/Init.py index a28022c94b..bf03b95568 100644 --- a/src/Mod/Mesh/Init.py +++ b/src/Mod/Mesh/Init.py @@ -5,6 +5,8 @@ import FreeCAD +translate = FreeCAD.Qt.translate + # Append the open handler FreeCAD.addImportType("STL Mesh (*.stl *.STL *.ast *.AST)", "Mesh") FreeCAD.addImportType("Binary Mesh (*.bms *.BMS)", "Mesh") @@ -14,10 +16,17 @@ FreeCAD.addImportType("Stanford Triangle Mesh (*.ply *.PLY)", "Mesh") FreeCAD.addImportType("Simple Model Format (*.smf *.SMF)", "Mesh") FreeCAD.addImportType("3D Manufacturing Format (*.3mf *.3MF)", "Mesh") -FreeCAD.addExportType("STL Mesh (*.stl *.ast)", "Mesh") -FreeCAD.addExportType("Binary Mesh (*.bms)", "Mesh") -FreeCAD.addExportType("Alias Mesh (*.obj)", "Mesh") -FreeCAD.addExportType("Object File Format Mesh (*.off)", "Mesh") +FreeCAD.addTranslatableExportType(translate("FileFormat", "STL Mesh"), ["stl", "ast"], "Mesh") +FreeCAD.addTranslatableExportType(translate("FileFormat", "Binary Mesh"), ["bms"], "Mesh") + +#: Translation note: "Alias" in this case is a product/format name and should not be translated +FreeCAD.addTranslatableExportType(translate("FileFormat", "Alias Mesh"), ["obj"], "Mesh") + +#: Translation note: "Object File Format" is the official name and should not be translated +FreeCAD.addTranslatableExportType( + translate("FileFormat", "Object File Format Mesh"), ["off"], "Mesh" +) + FreeCAD.addExportType("Stanford Triangle Mesh (*.ply)", "Mesh") FreeCAD.addExportType("Additive Manufacturing Format (*.amf)", "Mesh") FreeCAD.addExportType("Simple Model Format (*.smf)", "Mesh") diff --git a/src/Mod/OpenSCAD/Init.py b/src/Mod/OpenSCAD/Init.py index 05ad8f0657..2635dbf970 100644 --- a/src/Mod/OpenSCAD/Init.py +++ b/src/Mod/OpenSCAD/Init.py @@ -28,7 +28,7 @@ import os import FreeCAD -FreeCAD.addImportType("OpenSCAD CSG Format (*.csg *.CSG)", "importCSG") +FreeCAD.addImportType("OpenSCAD CSG (*.csg *.CSG)", "importCSG") param = FreeCAD.ParamGet(\ "User parameter:BaseApp/Preferences/Mod/OpenSCAD") @@ -36,8 +36,8 @@ openscadfilename = param.GetString('openscadexecutable') openscadbin = openscadfilename and os.path.isfile(openscadfilename) if openscadbin: - FreeCAD.addImportType("OpenSCAD Format (*.scad *.SCAD)", "importCSG") + FreeCAD.addImportType("OpenSCAD (*.scad *.SCAD)", "importCSG") FreeCAD.__unit_test__ += ["TestOpenSCADApp"] -FreeCAD.addExportType("OpenSCAD CSG Format (*.csg)", "exportCSG") -FreeCAD.addExportType("OpenSCAD Format (*.scad)", "exportCSG") +FreeCAD.addExportType("OpenSCAD CSG (*.csg)", "exportCSG") +FreeCAD.addExportType("OpenSCAD (*.scad)", "exportCSG") diff --git a/src/Mod/Part/Init.py b/src/Mod/Part/Init.py index 99f8f65490..60d83b0ec3 100644 --- a/src/Mod/Part/Init.py +++ b/src/Mod/Part/Init.py @@ -25,13 +25,21 @@ # FreeCAD init script of the part module +import FreeCAD + +translate = FreeCAD.Qt.translate + # FreeCAD.addImportType("CAD formats (*.igs *.iges *.step *.stp *.brep *.brp)","Part") # FreeCAD.addExportType("CAD formats (*.igs *.iges *.step *.stp *.brep *.brp)","Part") -FreeCAD.addImportType("BREP format (*.brep *.BREP *.brp *.BRP)", "Part") -FreeCAD.addExportType("BREP format (*.brep *.brp)", "Part") -FreeCAD.addImportType("IGES format (*.iges *.IGES *.igs *.IGS)", "Part") -FreeCAD.addExportType("IGES format (*.iges *.igs)", "Part") +FreeCAD.addImportType("BREP (*.brep *.BREP *.brp *.BRP)", "Part") +FreeCAD.addExportType("BREP (*.brep *.brp)", "Part") +FreeCAD.addImportType("IGES (*.iges *.IGES *.igs *.IGS)", "Part") +FreeCAD.addExportType("IGES (*.iges *.igs)", "Part") FreeCAD.addImportType("STEP with colors (*.step *.STEP *.stp *.STP)", "Import") -FreeCAD.addExportType("STEP with colors (*.step *.stp)", "Import") + +#: Translation note: "STEP" is a file type end should not be translated +FreeCAD.addTranslatableExportType( + translate("FileFormat", "STEP with colors"), ["step", "stp"], "Import" +) FreeCAD.__unit_test__ += ["TestPartApp"] diff --git a/src/Mod/Points/Init.py b/src/Mod/Points/Init.py index 9b3af71114..0ea2069238 100644 --- a/src/Mod/Points/Init.py +++ b/src/Mod/Points/Init.py @@ -25,6 +25,12 @@ # FreeCAD init script of the Points module +import FreeCAD + +translate = FreeCAD.Qt.translate + # Append the open handler FreeCAD.addImportType("Point formats (*.asc *.ASC *.pcd *.PCD *.ply *.PLY *.e57 *.E57)", "Points") -FreeCAD.addExportType("Point formats (*.asc *.pcd *.ply)", "Points") +FreeCAD.addTranslatableExportType( + translate("FileFormat", "Point formats"), ["asc", "pcd", "ply"], "Points" +)