wiRedPanda
Logic Circuit Simulator
Loading...
Searching...
No Matches
LanguageManager.cpp
Go to the documentation of this file.
1// Copyright 2015 - 2026, GIBIS-UNIFESP and the wiRedPanda contributors
2// SPDX-License-Identifier: GPL-3.0-or-later
3
5
6#include <QDir>
7#include <QLocale>
8#include <QResource>
9#include <QTranslator>
10
11#ifdef Q_OS_WASM
12#include <QFontDatabase>
13#endif
14
16#include "App/Core/Settings.h"
17
18#ifdef Q_OS_WASM
19static void loadAllFonts()
20{
21 static const QStringList kFontPaths = {
22 ":/Fonts/NotoSansArabic-Regular.ttf",
23 ":/Fonts/NotoSansBengali-Regular.ttf",
24 ":/Fonts/NotoSansDevanagari-Regular.ttf",
25 ":/Fonts/NotoSansHebrew-Regular.ttf",
26 ":/Fonts/NotoSansKR-Regular.ttf",
27 ":/Fonts/NotoSansSC-Regular.ttf",
28 ":/Fonts/NotoSansThai-Regular.ttf",
29 };
30
31 for (const auto &path : kFontPaths) {
32 if (QFontDatabase::addApplicationFont(path) == -1) {
33 qWarning() << "Failed to load WASM font:" << path;
34 }
35 }
36}
37#endif
38
40 : QObject(parent)
41{
42#ifdef Q_OS_WASM
43 // Load all fonts upfront so the language menu can render native script names.
44 loadAllFonts();
45#endif
46}
47
49{
50 // Remove translator from Application::instance() before it is deleted by the QObject parent tree.
51 Application::instance()->removeTranslator(m_translator);
52}
53
54void LanguageManager::loadTranslation(const QString &language)
55{
56 if (language.isEmpty()) {
57 return;
58 }
59
60 Settings::setLanguage(language);
61
62 // Always recreate the translator rather than re-loading; Qt does not
63 // guarantee that calling load() on an existing translator re-emits
64 // languageChanged.
65 Application::instance()->removeTranslator(m_translator);
66 delete m_translator;
67 m_translator = nullptr;
68
69 if (language == "en") {
70 emit translationChanged();
71 return;
72 }
73
74 // qt_add_translations embeds wpanda_<lang>.qm at :/i18n/ at build time.
75 // On Qt 6.9+ MERGE_QT_TRANSLATIONS also bakes qtbase / Widgets / Gui /
76 // Multimedia / Svg catalogs into the same file, so a single translator
77 // covers both application and Qt-owned strings. On Qt 6.2..6.8 the
78 // merge isn't available — Qt's own dialog strings (file picker, message
79 // boxes) stay English, but application strings still translate.
80 const QString qmFile = QStringLiteral(":/i18n/wpanda_%1.qm").arg(language);
81
82 if (QResource(qmFile).isValid()) { // LCOV_EXCL_LINE — qt_add_translations() embeds wpanda_*.qm only into the production "wiredpanda" executable target (CMakeLists.txt), by design, not into test_wiredpanda, so this is never true in the test binary; see .claude/COVERAGE_100_PLAN.md pattern 37.
83 m_translator = new QTranslator(this); // LCOV_EXCL_LINE — see above.
84
85 if (!m_translator->load(qmFile) || !Application::instance()->installTranslator(m_translator)) { // LCOV_EXCL_LINE — see above.
86 qWarning() << "Failed to load translation for" << language << ", falling back to English"; // LCOV_EXCL_LINE — see above.
87 delete m_translator; // LCOV_EXCL_LINE — see above.
88 m_translator = nullptr; // LCOV_EXCL_LINE — see above.
89 loadTranslation("en"); // LCOV_EXCL_LINE — see above.
90 return; // LCOV_EXCL_LINE — see above.
91 }
92 }
93
94 emit translationChanged();
95}
96
98{
99 QStringList languages = {"en"};
100
101 // Qt's resource system exposes ":/i18n" as a virtual directory.
102 QDir translationsDir(QStringLiteral(":/i18n"));
103 if (translationsDir.exists()) {
104 const QStringList qmFiles = translationsDir.entryList({"wpanda_*.qm"}, QDir::Files);
105
106 // LCOV_EXCL_START — wpanda_*.qm is embedded only into the production "wiredpanda"
107 // executable target, not test_wiredpanda (see loadTranslation()'s matching exclusion
108 // and .claude/COVERAGE_100_PLAN.md pattern 37), so qmFiles is always empty here; this
109 // loop body only ever runs in the real app.
110 for (const QString &file : qmFiles) {
111 QString langCode = file;
112 langCode.remove("wpanda_");
113 langCode.remove(".qm");
114
115 if (!langCode.isEmpty() && QResource(QStringLiteral(":/i18n/") + file).isValid()) {
116 languages << langCode;
117 }
118 }
119 // LCOV_EXCL_STOP
120 } else {
121 // Fallback: probe a known set of language codes when directory listing fails.
122 // LCOV_EXCL_START — :/i18n itself always exists in every target here (wiredpanda_resources
123 // registers :/i18n/ExerciseTour/ regardless of target), so this branch is unreachable;
124 // same root cause as the exclusions above.
125 static const QStringList kKnownLanguages = {
126 "ar", "bg", "bn", "cs", "da", "de", "el", "es", "et", "fa", "fi", "fr",
127 "he", "hi", "hr", "hu", "id", "it", "ja", "ko", "lt", "lv", "ms", "nb",
128 "nl", "pl", "pt", "pt_BR", "ro", "ru", "sk", "sv", "th", "tr", "uk",
129 "vi", "zh_Hans", "zh_Hant"
130 };
131
132 for (const QString &langCode : kKnownLanguages) {
133 if (QResource(QStringLiteral(":/i18n/wpanda_%1.qm").arg(langCode)).isValid()) {
134 languages << langCode;
135 }
136 }
137 // LCOV_EXCL_STOP
138 }
139
140 languages.removeDuplicates();
141 languages.sort();
142 return languages;
143}
144
145QString LanguageManager::displayName(const QString &langCode) const
146{
147 QLocale locale(langCode);
148
149 if (langCode == "pt_BR") {
150 locale = QLocale(QLocale::Portuguese, QLocale::Brazil);
151 }
152
153 QString name = locale.nativeLanguageName();
154
155 if (langCode.contains('_')) {
156 const QString country = locale.nativeTerritoryName();
157 if (!country.isEmpty() && country != name) {
158 name += QString(" (%1)").arg(country);
159 }
160 }
161
162 if (name.isEmpty()) {
163 // Manual fallbacks for languages Qt may not fully support.
164 static const QMap<QString, QString> kFallbacks = {
165 {"bn", "বাংলা"},
166 {"fa", "فارسی"},
167 {"he", "עברית"},
168 {"th", "ไทย"}
169 };
170 name = kFallbacks.value(langCode, langCode);
171 }
172
173 if (!name.isEmpty() && name.at(0).isLetter()) {
174 name[0] = name.at(0).toUpper();
175 }
176
177 return name;
178}
179
180QString LanguageManager::flagIcon(const QString &langCode) const
181{
182 // Single authoritative map keyed by BCP-47 language code.
183 static const QMap<QString, QString> kFlags = {
184 {"ar", ":/Interface/Locale/arabic.svg"},
185 {"bg", ":/Interface/Locale/bulgarian.svg"},
186 {"bn", ":/Interface/Locale/bangladesh.svg"},
187 {"cs", ":/Interface/Locale/czech.svg"},
188 {"da", ":/Interface/Locale/danish.svg"},
189 {"de", ":/Interface/Locale/german.svg"},
190 {"el", ":/Interface/Locale/greek.svg"},
191 {"en", ":/Interface/Locale/usa.svg"},
192 {"es", ":/Interface/Locale/spanish.svg"},
193 {"et", ":/Interface/Locale/estonian.svg"},
194 {"fa", ":/Interface/Locale/iranian.svg"},
195 {"fi", ":/Interface/Locale/finnish.svg"},
196 {"fr", ":/Interface/Locale/french.svg"},
197 {"he", ":/Interface/Locale/hebrew.svg"},
198 {"hi", ":/Interface/Locale/hindi.svg"},
199 {"hr", ":/Interface/Locale/croatian.svg"},
200 {"hu", ":/Interface/Locale/hungarian.svg"},
201 {"id", ":/Interface/Locale/indonesian.svg"},
202 {"it", ":/Interface/Locale/italian.svg"},
203 {"ja", ":/Interface/Locale/japanese.svg"},
204 {"ko", ":/Interface/Locale/korean.svg"},
205 {"lt", ":/Interface/Locale/lithuanian.svg"},
206 {"lv", ":/Interface/Locale/latvian.svg"},
207 {"ms", ":/Interface/Locale/malaysian.svg"},
208 {"nb", ":/Interface/Locale/norwegian.svg"},
209 {"nl", ":/Interface/Locale/dutch.svg"},
210 {"pl", ":/Interface/Locale/polish.svg"},
211 {"pt", ":/Interface/Locale/portuguese.svg"},
212 {"pt_BR", ":/Interface/Locale/brasil.svg"},
213 {"ro", ":/Interface/Locale/romanian.svg"},
214 {"ru", ":/Interface/Locale/russian.svg"},
215 {"sk", ":/Interface/Locale/slovak.svg"},
216 {"sv", ":/Interface/Locale/swedish.svg"},
217 {"th", ":/Interface/Locale/thai.svg"},
218 {"tr", ":/Interface/Locale/turkish.svg"},
219 {"uk", ":/Interface/Locale/ukrainian.svg"},
220 {"vi", ":/Interface/Locale/vietnamese.svg"},
221 {"zh_Hans", ":/Interface/Locale/chinese.svg"},
222 {"zh_Hant", ":/Interface/Locale/chinese_traditional.svg"},
223 };
224
225 return kFlags.value(langCode, ":/Interface/Locale/default.svg");
226}
Custom QApplication subclass with exception handling and main-window access.
LanguageManager: Qt translation loading and language metadata.
Typed wrappers around QSettings for all application preferences.
static Application * instance()
Returns the application instance cast to Application*.
void loadTranslation(const QString &language)
Loads and installs the Qt translation for language.
LanguageManager(QObject *parent=nullptr)
void translationChanged()
Emitted after a translation has been successfully loaded (or reset to English).
QString flagIcon(const QString &langCode) const
Returns the flag icon resource path for langCode.
QStringList availableLanguages() const
Returns all available translation language codes, sorted, with "en" first.
~LanguageManager() override
QString displayName(const QString &langCode) const
Returns the native display name for langCode (e.g. "Deutsch" for "de").
static void setLanguage(const QString &lang)
Definition Settings.cpp:147