Skip to content

Commit bce5a70

Browse files
Ski90Mooclaude
andcommitted
test: add GTest suite for i18n translation (issue #680)
Adds Translation_GTest.cpp to the OpenStudioApp test target, covering: Translation_ts suite (no build-path dependency, uses .ts source file): - ValidXml: verifies OpenStudioApp_es.ts parses as well-formed XML - HasExpectedContexts: checks all new translation contexts are present (IDD, OutputVariables, TaxonomyCategories, SimSettingsView, RunView, etc.) - TranslationCountIsSubstantial: guards against accidental file truncation - IddContextHasEntries: IDD context has >50 field-name translations - OutputVariablesContextHasEntries: OutputVariables context has >=1000 entries - TaxonomyCategoriesContextHasEntries: taxonomy categories are present Translation_qm suite (requires compiled .qm, skipped gracefully if absent): - QmFileLoads: QTranslator::load() succeeds for OpenStudioApp_es.qm - SpanishSimSettingsStringsTranslated: spot-checks Simulation Settings labels - SpanishRunViewStringsTranslated: spot-checks Run Simulation labels - TaxonomyCategoriesTranslated: spot-checks library sidebar category names - OutputVariablesSampleTranslated: spot-checks output variable name translations - EnglishStringsReturnedWithoutTranslator: verifies English fallback when no QTranslator is installed Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent b1c5733 commit bce5a70

2 files changed

Lines changed: 281 additions & 0 deletions

File tree

src/openstudio_app/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -537,6 +537,7 @@ set(${target_name}_test_src
537537
test/OpenStudioAppFixture.hpp
538538
test/OpenStudioAppFixture.cpp
539539
test/Resources_GTest.cpp
540+
test/Translation_GTest.cpp
540541
test/Units_GTest.cpp
541542
)
542543

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
/***********************************************************************************************************************
2+
* OpenStudio(R), Copyright (c) OpenStudio Coalition and other contributors.
3+
* See also https://openstudiocoalition.org/about/software_license/
4+
***********************************************************************************************************************/
5+
6+
#include <gtest/gtest.h>
7+
8+
#include "OpenStudioAppFixture.hpp"
9+
#include "../../utilities/OpenStudioApplicationPathHelpers.hpp"
10+
#include "../../model_editor/Utilities.hpp"
11+
12+
#include <QCoreApplication>
13+
#include <QDomDocument>
14+
#include <QFile>
15+
#include <QString>
16+
#include <QTranslator>
17+
18+
using namespace openstudio;
19+
20+
// ---------------------------------------------------------------------------
21+
// Helpers
22+
// ---------------------------------------------------------------------------
23+
24+
static openstudio::path translationsSourceDir() {
25+
return getOpenStudioApplicationSourceDirectory() / toPath("translations");
26+
}
27+
28+
// Locate the compiled .qm file. It is generated into the build tree under
29+
// Products/Release/translations/ on Windows and into the equivalent on other
30+
// platforms. We try a few candidate paths so the test works from different
31+
// build configurations.
32+
static openstudio::path findQmFile(const std::string& language) {
33+
const std::string filename = "OpenStudioApp_" + language + ".qm";
34+
35+
// 1. Next to the test executable (CTest sets the working directory here)
36+
openstudio::path candidates[] = {
37+
toPath("translations") / toPath(filename),
38+
toPath("../translations") / toPath(filename),
39+
toPath("../../Products/Release/translations") / toPath(filename),
40+
toPath("../../Products/Debug/translations") / toPath(filename),
41+
translationsSourceDir() / toPath(filename), // committed .qm (if present)
42+
};
43+
44+
for (const auto& p : candidates) {
45+
if (openstudio::filesystem::exists(p)) {
46+
return p;
47+
}
48+
}
49+
return {}; // empty = not found
50+
}
51+
52+
// ---------------------------------------------------------------------------
53+
// Test Suite: Translation_ts (validates the .ts source file – no build dep)
54+
// ---------------------------------------------------------------------------
55+
56+
class Translation_ts : public OpenStudioAppFixture
57+
{
58+
protected:
59+
QDomDocument m_doc;
60+
61+
void SetUp() override {
62+
openstudio::path tsPath = translationsSourceDir() / toPath("OpenStudioApp_es.ts");
63+
ASSERT_TRUE(openstudio::filesystem::exists(tsPath))
64+
<< "Translation source file not found: " << tsPath;
65+
66+
QFile file(toQString(tsPath));
67+
ASSERT_TRUE(file.open(QIODevice::ReadOnly)) << "Cannot open OpenStudioApp_es.ts";
68+
69+
QString errorMsg;
70+
int errorLine = 0;
71+
ASSERT_TRUE(m_doc.setContent(&file, &errorMsg, &errorLine))
72+
<< "XML parse error in OpenStudioApp_es.ts at line " << errorLine << ": "
73+
<< errorMsg.toStdString();
74+
}
75+
};
76+
77+
TEST_F(Translation_ts, ValidXml) {
78+
// Root element should be <TS>
79+
EXPECT_EQ(m_doc.documentElement().tagName(), "TS");
80+
}
81+
82+
TEST_F(Translation_ts, HasExpectedContexts) {
83+
// Verify contexts we introduced are present in the file
84+
const QStringList requiredContexts = {
85+
"openstudio::SimSettingsView",
86+
"openstudio::RunView",
87+
"openstudio::RunTabView",
88+
"openstudio::ResultsView",
89+
"openstudio::ResultsTabController",
90+
"openstudio::VariablesList",
91+
"openstudio::ScriptsTabView",
92+
"openstudio::LocalLibraryView",
93+
"openstudio::measuretab::WorkflowController",
94+
"openstudio::measuretab::NewMeasureDropZone",
95+
"IDD",
96+
"OutputVariables",
97+
"TaxonomyCategories",
98+
};
99+
100+
QSet<QString> foundContexts;
101+
QDomNodeList contextNodes = m_doc.elementsByTagName("context");
102+
for (int i = 0; i < contextNodes.count(); ++i) {
103+
QDomElement nameEl = contextNodes.at(i).firstChildElement("name");
104+
if (!nameEl.isNull()) {
105+
foundContexts.insert(nameEl.text());
106+
}
107+
}
108+
109+
for (const QString& ctx : requiredContexts) {
110+
EXPECT_TRUE(foundContexts.contains(ctx))
111+
<< "Missing translation context: " << ctx.toStdString();
112+
}
113+
}
114+
115+
TEST_F(Translation_ts, TranslationCountIsSubstantial) {
116+
// Sanity check: the file should contain at least 2000 translated messages.
117+
// This catches accidental truncation of the file.
118+
int count = 0;
119+
QDomNodeList messages = m_doc.elementsByTagName("message");
120+
for (int i = 0; i < messages.count(); ++i) {
121+
QDomElement translation = messages.at(i).firstChildElement("translation");
122+
if (!translation.isNull() && translation.attribute("type") != "unfinished"
123+
&& !translation.text().isEmpty()) {
124+
++count;
125+
}
126+
}
127+
EXPECT_GE(count, 2000) << "Unexpectedly few finished translations: " << count;
128+
}
129+
130+
TEST_F(Translation_ts, IddContextHasEntries) {
131+
int iddCount = 0;
132+
QDomNodeList contextNodes = m_doc.elementsByTagName("context");
133+
for (int i = 0; i < contextNodes.count(); ++i) {
134+
QDomElement nameEl = contextNodes.at(i).firstChildElement("name");
135+
if (!nameEl.isNull() && nameEl.text() == "IDD") {
136+
iddCount = contextNodes.at(i).toElement().elementsByTagName("message").count();
137+
break;
138+
}
139+
}
140+
EXPECT_GT(iddCount, 50) << "IDD context has unexpectedly few entries: " << iddCount;
141+
}
142+
143+
TEST_F(Translation_ts, OutputVariablesContextHasEntries) {
144+
int count = 0;
145+
QDomNodeList contextNodes = m_doc.elementsByTagName("context");
146+
for (int i = 0; i < contextNodes.count(); ++i) {
147+
QDomElement nameEl = contextNodes.at(i).firstChildElement("name");
148+
if (!nameEl.isNull() && nameEl.text() == "OutputVariables") {
149+
count = contextNodes.at(i).toElement().elementsByTagName("message").count();
150+
break;
151+
}
152+
}
153+
// There are 1051 output variable names
154+
EXPECT_GE(count, 1000) << "OutputVariables context has unexpectedly few entries: " << count;
155+
}
156+
157+
TEST_F(Translation_ts, TaxonomyCategoriesContextHasEntries) {
158+
int count = 0;
159+
QDomNodeList contextNodes = m_doc.elementsByTagName("context");
160+
for (int i = 0; i < contextNodes.count(); ++i) {
161+
QDomElement nameEl = contextNodes.at(i).firstChildElement("name");
162+
if (!nameEl.isNull() && nameEl.text() == "TaxonomyCategories") {
163+
count = contextNodes.at(i).toElement().elementsByTagName("message").count();
164+
break;
165+
}
166+
}
167+
EXPECT_GT(count, 30) << "TaxonomyCategories context has unexpectedly few entries: " << count;
168+
}
169+
170+
// ---------------------------------------------------------------------------
171+
// Test Suite: Translation_qm (validates the compiled .qm and live translate)
172+
// ---------------------------------------------------------------------------
173+
174+
class Translation_qm : public OpenStudioAppFixture
175+
{
176+
protected:
177+
QTranslator m_translator;
178+
bool m_loaded = false;
179+
180+
void SetUp() override {
181+
openstudio::path qmPath = findQmFile("es");
182+
if (!qmPath.empty()) {
183+
m_loaded = m_translator.load(toQString(qmPath));
184+
if (m_loaded) {
185+
QCoreApplication::installTranslator(&m_translator);
186+
}
187+
}
188+
}
189+
190+
void TearDown() override {
191+
if (m_loaded) {
192+
QCoreApplication::removeTranslator(&m_translator);
193+
}
194+
}
195+
};
196+
197+
TEST_F(Translation_qm, QmFileLoads) {
198+
openstudio::path qmPath = findQmFile("es");
199+
if (qmPath.empty()) {
200+
GTEST_SKIP() << "OpenStudioApp_es.qm not found in candidate paths; skipping runtime translation tests. "
201+
"Build the translations target and re-run.";
202+
}
203+
EXPECT_TRUE(m_loaded) << "QTranslator::load() failed for: " << qmPath;
204+
}
205+
206+
TEST_F(Translation_qm, SpanishSimSettingsStringsTranslated) {
207+
if (!m_loaded) {
208+
GTEST_SKIP() << "Spanish .qm not loaded.";
209+
}
210+
211+
// Spot-check a few strings from the Simulation Settings tab
212+
EXPECT_EQ(QCoreApplication::translate("openstudio::SimSettingsView", "Run Period"),
213+
QString("Período de Ejecución"));
214+
EXPECT_EQ(QCoreApplication::translate("openstudio::SimSettingsView", "Timestep"),
215+
QString("Paso de Tiempo"));
216+
EXPECT_EQ(QCoreApplication::translate("openstudio::SimSettingsView", "Shadow Calculation"),
217+
QString("Cálculo de Sombras"));
218+
EXPECT_EQ(QCoreApplication::translate("openstudio::SimSettingsView", "Algorithm"),
219+
QString("Algoritmo"));
220+
}
221+
222+
TEST_F(Translation_qm, SpanishRunViewStringsTranslated) {
223+
if (!m_loaded) {
224+
GTEST_SKIP() << "Spanish .qm not loaded.";
225+
}
226+
227+
EXPECT_EQ(QCoreApplication::translate("openstudio::RunView", "Run"), QString("Ejecutar"));
228+
EXPECT_EQ(QCoreApplication::translate("openstudio::RunView", "Verbose"), QString("Detallado"));
229+
EXPECT_EQ(QCoreApplication::translate("openstudio::RunView", "Show Simulation"),
230+
QString("Mostrar Simulación"));
231+
EXPECT_EQ(QCoreApplication::translate("openstudio::RunView", "Initializing workflow."),
232+
QString("Inicializando flujo de trabajo."));
233+
}
234+
235+
TEST_F(Translation_qm, TaxonomyCategoriesTranslated) {
236+
if (!m_loaded) {
237+
GTEST_SKIP() << "Spanish .qm not loaded.";
238+
}
239+
240+
EXPECT_EQ(QCoreApplication::translate("TaxonomyCategories", "Envelope"), QString("Envolvente"));
241+
EXPECT_EQ(QCoreApplication::translate("TaxonomyCategories", "HVAC"), QString("HVAC"));
242+
EXPECT_EQ(QCoreApplication::translate("TaxonomyCategories", "Refrigeration"),
243+
QString("Refrigeración"));
244+
EXPECT_EQ(QCoreApplication::translate("TaxonomyCategories", "Whole Building"),
245+
QString("Edificio Completo"));
246+
EXPECT_EQ(QCoreApplication::translate("TaxonomyCategories", "Troubleshooting"),
247+
QString("Solución de Problemas"));
248+
}
249+
250+
TEST_F(Translation_qm, OutputVariablesSampleTranslated) {
251+
if (!m_loaded) {
252+
GTEST_SKIP() << "Spanish .qm not loaded.";
253+
}
254+
255+
// A sampling of output variable names
256+
EXPECT_EQ(QCoreApplication::translate("OutputVariables", "Zone Air Temperature"),
257+
QString("Temperatura del Aire de la Zona"));
258+
EXPECT_EQ(QCoreApplication::translate("OutputVariables", "Fan Electricity Energy"),
259+
QString("Energía Eléctrica del Ventilador"));
260+
EXPECT_EQ(QCoreApplication::translate("OutputVariables", "Boiler Heating Energy"),
261+
QString("Energía de Calefacción de la Caldera"));
262+
}
263+
264+
TEST_F(Translation_qm, EnglishStringsReturnedWithoutTranslator) {
265+
// Remove the translator to verify English fallback works
266+
if (m_loaded) {
267+
QCoreApplication::removeTranslator(&m_translator);
268+
}
269+
270+
// tr() / translate() must return the source string when no translator is loaded
271+
EXPECT_EQ(QCoreApplication::translate("openstudio::RunView", "Run"), QString("Run"));
272+
EXPECT_EQ(QCoreApplication::translate("TaxonomyCategories", "Envelope"), QString("Envelope"));
273+
EXPECT_EQ(QCoreApplication::translate("openstudio::SimSettingsView", "Timestep"),
274+
QString("Timestep"));
275+
276+
// Re-install for TearDown
277+
if (m_loaded) {
278+
QCoreApplication::installTranslator(&m_translator);
279+
}
280+
}

0 commit comments

Comments
 (0)