Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions src/model_editor/InspectorGadget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
#include <QIntValidator>
#include <QLabel>
#include <QLineEdit>
#include <QCoreApplication>
#include <QLocale>
#include <QPainter>
#include <QPushButton>
#include <QRadioButton>
Expand All @@ -53,6 +55,30 @@
#include <QVBoxLayout>

using namespace openstudio;

// Returns "Traducción (Field Name)" when the app locale is non-English and
// a translation exists in the "IDD" context of the loaded .qm file;
// otherwise returns the plain English field name.
//
// Format rationale: the translated term is shown first for readability in the
// target language. The original English IDD field name is retained in
// parentheses because these names are the canonical identifiers used in
// EnergyPlus documentation, the OpenStudio SDK, and .idf/.osm model files.
// A Spanish-speaking engineer who needs to cross-reference the EnergyPlus
// I/O Reference or file a bug report can read the English name without
// having to switch the application language and reload the model.
//
// To add or refine translations: add entries to the IDD context in
// OpenStudioApp_<lang>.ts and recompile with lrelease — no C++ changes needed.
static QString iddFieldDisplayName(const std::string& englishName) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really cool, I just wonder how we will be able to maintain this. The Translation test could possibly loop over each IddObject and verify that there is an entry for each one?

Same concern for the OutputVariables

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was one my concerns as well. I did a handful of them, but I could imagine the list of these objects would be enormous and difficult to maintain. It also makes the .ts file huge; no small task for the language contributors to review. I envision it would be more of a reactionary process whereby the contributors review the general GUI stuff, but we would get feedback as users stumble upon poorly translated objects.

The OutputVariables should be complete though. I will look into modifying the Translation test.

const QString qname = QString::fromStdString(englishName);
if (QLocale().language() != QLocale::English) {
const QString translated = QCoreApplication::translate("IDD", englishName.c_str());
if (translated != qname)
return translated + " (" + qname + ")";
}
return qname;
}
using namespace openstudio::model;

const char* InspectorGadget::s_indexSlotName = "indexSlot";
Expand Down Expand Up @@ -507,7 +533,7 @@ void InspectorGadget::layoutText(QVBoxLayout* layout, QWidget* parent, openstudi
auto* vbox = new QVBoxLayout();
frame->setLayout(vbox);

auto* label = new QLabel(QString(name.c_str()), parent);
auto* label = new QLabel(iddFieldDisplayName(name), parent);
label->setWordWrap(true);
vbox->addWidget(label);

Expand Down Expand Up @@ -751,7 +777,7 @@ void InspectorGadget::layoutComboBox(QVBoxLayout* layout, QWidget* parent, opens
auto* frame = new QFrame(parent);
auto* vbox = new QVBoxLayout();
frame->setLayout(vbox);
auto* label = new QLabel(QString(name.c_str()), parent);
auto* label = new QLabel(iddFieldDisplayName(name), parent);
label->setWordWrap(true);

QComboBox* combo = new IGComboBox(parent);
Expand Down
1 change: 1 addition & 0 deletions src/openstudio_app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@ set(${target_name}_test_src
test/OpenStudioAppFixture.hpp
test/OpenStudioAppFixture.cpp
test/Resources_GTest.cpp
test/Translation_GTest.cpp
test/Units_GTest.cpp
)

Expand Down
280 changes: 280 additions & 0 deletions src/openstudio_app/test/Translation_GTest.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
/***********************************************************************************************************************
* OpenStudio(R), Copyright (c) OpenStudio Coalition and other contributors.
* See also https://openstudiocoalition.org/about/software_license/
***********************************************************************************************************************/

#include <gtest/gtest.h>

#include "OpenStudioAppFixture.hpp"
#include "../../utilities/OpenStudioApplicationPathHelpers.hpp"
#include "../../model_editor/Utilities.hpp"

#include <QCoreApplication>
#include <QDomDocument>
#include <QFile>
#include <QString>
#include <QTranslator>

using namespace openstudio;

// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

static openstudio::path translationsSourceDir() {
return getOpenStudioApplicationSourceDirectory() / toPath("translations");
}

// Locate the compiled .qm file. It is generated into the build tree under
// Products/Release/translations/ on Windows and into the equivalent on other
// platforms. We try a few candidate paths so the test works from different
// build configurations.
static openstudio::path findQmFile(const std::string& language) {
const std::string filename = "OpenStudioApp_" + language + ".qm";

// 1. Next to the test executable (CTest sets the working directory here)
openstudio::path candidates[] = {
toPath("translations") / toPath(filename),
toPath("../translations") / toPath(filename),
toPath("../../Products/Release/translations") / toPath(filename),
toPath("../../Products/Debug/translations") / toPath(filename),
translationsSourceDir() / toPath(filename), // committed .qm (if present)
};

for (const auto& p : candidates) {
if (openstudio::filesystem::exists(p)) {
return p;
}
}
return {}; // empty = not found
}

// ---------------------------------------------------------------------------
// Test Suite: Translation_ts (validates the .ts source file – no build dep)
// ---------------------------------------------------------------------------

class Translation_ts : public OpenStudioAppFixture
{
protected:
QDomDocument m_doc;

void SetUp() override {
openstudio::path tsPath = translationsSourceDir() / toPath("OpenStudioApp_es.ts");
ASSERT_TRUE(openstudio::filesystem::exists(tsPath))
<< "Translation source file not found: " << tsPath;

QFile file(toQString(tsPath));
ASSERT_TRUE(file.open(QIODevice::ReadOnly)) << "Cannot open OpenStudioApp_es.ts";

QString errorMsg;
int errorLine = 0;
ASSERT_TRUE(m_doc.setContent(&file, &errorMsg, &errorLine))
<< "XML parse error in OpenStudioApp_es.ts at line " << errorLine << ": "
<< errorMsg.toStdString();
}
};

TEST_F(Translation_ts, ValidXml) {
// Root element should be <TS>
EXPECT_EQ(m_doc.documentElement().tagName(), "TS");
}

TEST_F(Translation_ts, HasExpectedContexts) {
// Verify contexts we introduced are present in the file
const QStringList requiredContexts = {
"openstudio::SimSettingsView",
"openstudio::RunView",
"openstudio::RunTabView",
"openstudio::ResultsView",
"openstudio::ResultsTabController",
"openstudio::VariablesList",
"openstudio::ScriptsTabView",
"openstudio::LocalLibraryView",
"openstudio::measuretab::WorkflowController",
"openstudio::measuretab::NewMeasureDropZone",
"IDD",
"OutputVariables",
"TaxonomyCategories",
};

QSet<QString> foundContexts;
QDomNodeList contextNodes = m_doc.elementsByTagName("context");
for (int i = 0; i < contextNodes.count(); ++i) {
QDomElement nameEl = contextNodes.at(i).firstChildElement("name");
if (!nameEl.isNull()) {
foundContexts.insert(nameEl.text());
}
}

for (const QString& ctx : requiredContexts) {
EXPECT_TRUE(foundContexts.contains(ctx))
<< "Missing translation context: " << ctx.toStdString();
}
}

TEST_F(Translation_ts, TranslationCountIsSubstantial) {
// Sanity check: the file should contain at least 2000 translated messages.
// This catches accidental truncation of the file.
int count = 0;
QDomNodeList messages = m_doc.elementsByTagName("message");
for (int i = 0; i < messages.count(); ++i) {
QDomElement translation = messages.at(i).firstChildElement("translation");
if (!translation.isNull() && translation.attribute("type") != "unfinished"
&& !translation.text().isEmpty()) {
++count;
}
}
EXPECT_GE(count, 2000) << "Unexpectedly few finished translations: " << count;
}

TEST_F(Translation_ts, IddContextHasEntries) {
int iddCount = 0;
QDomNodeList contextNodes = m_doc.elementsByTagName("context");
for (int i = 0; i < contextNodes.count(); ++i) {
QDomElement nameEl = contextNodes.at(i).firstChildElement("name");
if (!nameEl.isNull() && nameEl.text() == "IDD") {
iddCount = contextNodes.at(i).toElement().elementsByTagName("message").count();
break;
}
}
EXPECT_GT(iddCount, 50) << "IDD context has unexpectedly few entries: " << iddCount;
}

TEST_F(Translation_ts, OutputVariablesContextHasEntries) {
int count = 0;
QDomNodeList contextNodes = m_doc.elementsByTagName("context");
for (int i = 0; i < contextNodes.count(); ++i) {
QDomElement nameEl = contextNodes.at(i).firstChildElement("name");
if (!nameEl.isNull() && nameEl.text() == "OutputVariables") {
count = contextNodes.at(i).toElement().elementsByTagName("message").count();
break;
}
}
// There are 1051 output variable names
EXPECT_GE(count, 1000) << "OutputVariables context has unexpectedly few entries: " << count;
}

TEST_F(Translation_ts, TaxonomyCategoriesContextHasEntries) {
int count = 0;
QDomNodeList contextNodes = m_doc.elementsByTagName("context");
for (int i = 0; i < contextNodes.count(); ++i) {
QDomElement nameEl = contextNodes.at(i).firstChildElement("name");
if (!nameEl.isNull() && nameEl.text() == "TaxonomyCategories") {
count = contextNodes.at(i).toElement().elementsByTagName("message").count();
break;
}
}
EXPECT_GT(count, 30) << "TaxonomyCategories context has unexpectedly few entries: " << count;
}

// ---------------------------------------------------------------------------
// Test Suite: Translation_qm (validates the compiled .qm and live translate)
// ---------------------------------------------------------------------------

class Translation_qm : public OpenStudioAppFixture
{
protected:
QTranslator m_translator;
bool m_loaded = false;

void SetUp() override {
openstudio::path qmPath = findQmFile("es");
if (!qmPath.empty()) {
m_loaded = m_translator.load(toQString(qmPath));
if (m_loaded) {
QCoreApplication::installTranslator(&m_translator);
}
}
}

void TearDown() override {
if (m_loaded) {
QCoreApplication::removeTranslator(&m_translator);
}
}
};

TEST_F(Translation_qm, QmFileLoads) {
openstudio::path qmPath = findQmFile("es");
if (qmPath.empty()) {
GTEST_SKIP() << "OpenStudioApp_es.qm not found in candidate paths; skipping runtime translation tests. "
"Build the translations target and re-run.";
}
EXPECT_TRUE(m_loaded) << "QTranslator::load() failed for: " << qmPath;
}

TEST_F(Translation_qm, SpanishSimSettingsStringsTranslated) {
if (!m_loaded) {
GTEST_SKIP() << "Spanish .qm not loaded.";
}

// Spot-check a few strings from the Simulation Settings tab
EXPECT_EQ(QCoreApplication::translate("openstudio::SimSettingsView", "Run Period"),
QString("Período de Ejecución"));
EXPECT_EQ(QCoreApplication::translate("openstudio::SimSettingsView", "Timestep"),
QString("Paso de Tiempo"));
EXPECT_EQ(QCoreApplication::translate("openstudio::SimSettingsView", "Shadow Calculation"),
QString("Cálculo de Sombras"));
EXPECT_EQ(QCoreApplication::translate("openstudio::SimSettingsView", "Algorithm"),
QString("Algoritmo"));
}

TEST_F(Translation_qm, SpanishRunViewStringsTranslated) {
if (!m_loaded) {
GTEST_SKIP() << "Spanish .qm not loaded.";
}

EXPECT_EQ(QCoreApplication::translate("openstudio::RunView", "Run"), QString("Ejecutar"));
EXPECT_EQ(QCoreApplication::translate("openstudio::RunView", "Verbose"), QString("Detallado"));
EXPECT_EQ(QCoreApplication::translate("openstudio::RunView", "Show Simulation"),
QString("Mostrar Simulación"));
EXPECT_EQ(QCoreApplication::translate("openstudio::RunView", "Initializing workflow."),
QString("Inicializando flujo de trabajo."));
}

TEST_F(Translation_qm, TaxonomyCategoriesTranslated) {
if (!m_loaded) {
GTEST_SKIP() << "Spanish .qm not loaded.";
}

EXPECT_EQ(QCoreApplication::translate("TaxonomyCategories", "Envelope"), QString("Envolvente"));
EXPECT_EQ(QCoreApplication::translate("TaxonomyCategories", "HVAC"), QString("HVAC"));
EXPECT_EQ(QCoreApplication::translate("TaxonomyCategories", "Refrigeration"),
QString("Refrigeración"));
EXPECT_EQ(QCoreApplication::translate("TaxonomyCategories", "Whole Building"),
QString("Edificio Completo"));
EXPECT_EQ(QCoreApplication::translate("TaxonomyCategories", "Troubleshooting"),
QString("Solución de Problemas"));
}

TEST_F(Translation_qm, OutputVariablesSampleTranslated) {
if (!m_loaded) {
GTEST_SKIP() << "Spanish .qm not loaded.";
}

// A sampling of output variable names
EXPECT_EQ(QCoreApplication::translate("OutputVariables", "Zone Air Temperature"),
QString("Temperatura del Aire de la Zona"));
EXPECT_EQ(QCoreApplication::translate("OutputVariables", "Fan Electricity Energy"),
QString("Energía Eléctrica del Ventilador"));
EXPECT_EQ(QCoreApplication::translate("OutputVariables", "Boiler Heating Energy"),
QString("Energía de Calefacción de la Caldera"));
}

TEST_F(Translation_qm, EnglishStringsReturnedWithoutTranslator) {
// Remove the translator to verify English fallback works
if (m_loaded) {
QCoreApplication::removeTranslator(&m_translator);
}

// tr() / translate() must return the source string when no translator is loaded
EXPECT_EQ(QCoreApplication::translate("openstudio::RunView", "Run"), QString("Run"));
EXPECT_EQ(QCoreApplication::translate("TaxonomyCategories", "Envelope"), QString("Envelope"));
EXPECT_EQ(QCoreApplication::translate("openstudio::SimSettingsView", "Timestep"),
QString("Timestep"));

// Re-install for TearDown
if (m_loaded) {
QCoreApplication::installTranslator(&m_translator);
}
}
Loading
Loading