localization.cpp
1 //-----------------------------------------------------------------------------
2 // localization.cpp
3 //-----------------------------------------------------------------------------
4 // This library is free software; you can redistribute it and/or
5 // modify it under the terms of the GNU Lesser General Public
6 // License as published by the Free Software Foundation; either
7 // version 2.1 of the License, or (at your option) any later version.
8 //
9 // This library is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 // Lesser General Public License for more details.
13 //
14 // You should have received a copy of the GNU Lesser General Public
15 // License along with this library; if not, write to the Free Software
16 // Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
17 // 02110-1301 USA
18 //-----------------------------------------------------------------------------
19 // Copyright (C) 2012 "Zalewa" <zalewapl@gmail.com>
20 //-----------------------------------------------------------------------------
21 #include "localization.h"
22 
23 #include "configuration/doomseekerconfig.h"
24 #include "datapaths.h"
25 #include "log.h"
26 #include "plugins/engineplugin.h"
27 #include "plugins/pluginloader.h"
28 #include <QApplication>
29 #include <QDebug>
30 #include <QDir>
31 #include <QFile>
32 #include <QLibraryInfo>
33 #include <QMessageBox>
34 #include <QSet>
35 #include <QStringList>
36 #include <QTranslator>
37 
38 const QString DEFINITION_FILE_PATTERN = "*.def";
39 const QString TRANSLATIONS_LOCATION_SUBDIR = "translations";
40 const int NUM_VALID_VERSION_TOKENS = 2;
41 const int NUM_VALID_TOKENS = 3;
42 
43 Localization *Localization::instance = nullptr;
44 
45 class Localization::LocalizationLoader
46 {
47 public:
48  LocalizationLoader();
49 
50  QList<LocalizationInfo> loadLocalizationsList(const QStringList &definitionsFileSearchDirs);
51 
52 private:
53  QList<LocalizationInfo> localizations;
54 
55  void loadLocalizationsListFile(const QString &definitionsFilePath);
56  void loadLocalizationsListFile(QIODevice &io);
57 
64  int obtainVersion(QIODevice &io);
65 
66  void sort();
67 };
68 
69 bool localizationInfoLessThan(const LocalizationInfo &o1, const LocalizationInfo &o2)
70 {
71  // Ensure the "system follow" fake locale is at the beginning of the list
73  return true;
75  return false;
76  // Sort other locales alphabetically
77  return o1.localeName.toLower() < o2.localeName.toLower();
78 }
79 
80 Localization::Localization()
81 {
82  currentLocalization_ = LocalizationInfo::PROGRAM_NATIVE;
83 }
84 
86 {
87  LocalizationInfo localization = LocalizationInfo::findBestMatch(localizations, localeName);
88  return localization.isValid() ? localization : LocalizationInfo::PROGRAM_NATIVE;
89 }
90 
91 const LocalizationInfo &Localization::currentLocalization() const
92 {
93  return currentLocalization_;
94 }
95 
96 Localization *Localization::get()
97 {
98  if (instance == nullptr)
99  instance = new Localization();
100  return instance;
101 }
102 
103 bool Localization::isCurrentlyLoaded(const QString &localeName) const
104 {
105  return localeName == currentLocalization_.localeName ||
106  localeName == gConfig.doomseeker.localization;
107 }
108 
109 QList<LocalizationInfo> Localization::loadLocalizationsList(const QStringList &definitionsFileSearchDirs)
110 {
111  LocalizationLoader l;
112  this->localizations = l.loadLocalizationsList(definitionsFileSearchDirs);
113  return this->localizations;
114 }
115 
116 bool Localization::loadTranslation(const QString &localeName)
117 {
118  QString localizationToFind = (localeName == LocalizationInfo::SYSTEM_FOLLOW.localeName) ?
119  QLocale::system().name() :
120  localeName;
121  LocalizationInfo localization = coerceBestMatchingLocalization(localizationToFind);
122  this->currentLocalization_ = localization;
123 
124  // Out with the old.
125  qDeleteAll(currentlyLoadedTranslations);
126  currentlyLoadedTranslations.clear();
127  if (localization == LocalizationInfo::PROGRAM_NATIVE)
128  return true;
129  // In with the new.
130  QStringList searchPaths = DataPaths::staticDataSearchDirs(
131  TRANSLATIONS_LOCATION_SUBDIR);
132  // Qt library translator.
133  installQtTranslations(localization.localeName, searchPaths);
134 
135  // Doomseeker translator.
136  bool installed = installTranslation(localization.localeName, searchPaths);
137 
138  // Plugins translators.
139  for (const PluginLoader::Plugin *plugin : gPlugins->plugins())
140  {
141  QString name = plugin->info()->nameCanonical();
142  QTranslator *pluginTranslator = loadTranslationFile(
143  QString("%1_%2").arg(name, localization.localeName),
144  searchPaths);
145  if (pluginTranslator)
146  {
147  gLog << QString("Loaded translation for plugin %1").arg(name);
148  QCoreApplication::installTranslator(pluginTranslator);
149  currentlyLoadedTranslations.append(pluginTranslator);
150  }
151  }
152  return installed;
153 }
154 
155 void Localization::installQtTranslations(const QString &localeName, QStringList searchPaths)
156 {
157  // First let's try to load translation that is bundled with program.
158  // This behavior is valid for Windows.
159  //
160  // If Qt translation is not bundled with program then try to load
161  // it from system location. This behavior is valid for Linux.
162  searchPaths << QLibraryInfo::location(QLibraryInfo::TranslationsPath);
163 
164  #if (QT_VERSION < QT_VERSION_CHECK(5, 3, 0))
165  // https://sourceforge.net/p/nootka/hg/ci/d00c13d9223b5bf74802aaec209ebc171efb27ce/tree/src/libs/core/tinitcorelib.cpp?diff=ce9e1fc25b8c23c9713c0cfa4c350d5ac2452fab
166  installTranslation("qt_" + localeName, searchPaths);
167  #else
168  installTranslation("qtbase_" + localeName, searchPaths);
169  installTranslation("qtmultimedia_" + localeName, searchPaths);
170  #endif
171 }
172 
173 bool Localization::installTranslation(const QString &translationName, const QStringList &searchPaths)
174 {
175  QTranslator *translator = loadTranslationFile(translationName, searchPaths);
176  if (translator != nullptr)
177  {
178  QCoreApplication::installTranslator(translator);
179  currentlyLoadedTranslations.append(translator);
180  return true;
181  }
182  else
183  {
184  gLog << QString("Translation '%1' not found.").arg(translationName);
185  return false;
186  }
187 }
188 
189 QTranslator *Localization::loadTranslationFile(const QString &translationName, const QStringList &searchPaths)
190 {
191  auto pTranslator = new QTranslator();
192  bool bLoaded = false;
193  for (const QString &dir : searchPaths)
194  {
195  if (pTranslator->load(translationName, dir))
196  {
197  gLog << QString("Found translation '%1' in dir '%2'.").arg(translationName, dir);
198  bLoaded = true;
199  break;
200  }
201  }
202  if (!bLoaded)
203  {
204  delete pTranslator;
205  pTranslator = nullptr;
206  }
207  return pTranslator;
208 }
210 Localization::LocalizationLoader::LocalizationLoader()
211 {
212  localizations << LocalizationInfo::SYSTEM_FOLLOW;
213  localizations << LocalizationInfo::PROGRAM_NATIVE;
214 }
215 
216 QList<LocalizationInfo> Localization::LocalizationLoader::loadLocalizationsList(const QStringList &definitionsFileSearchDirs)
217 {
218  for (const QString &dirPath : definitionsFileSearchDirs)
219  {
220  loadLocalizationsListFile(dirPath);
221  }
222  sort();
223  return localizations;
224 }
225 
226 void Localization::LocalizationLoader::loadLocalizationsListFile(const QString &definitionsFilePath)
227 {
228  QDir dir(definitionsFilePath);
229  QStringList defFiles = dir.entryList(QStringList(DEFINITION_FILE_PATTERN), QDir::Files);
230  for (const QString &defFileName : defFiles)
231  {
232  // No point in translating strings in this class because
233  // translation is not loaded yet.
234  gLog << QString("Reading localizations definitions file: %1").arg(defFileName);
235  QString filePath = dir.absoluteFilePath(defFileName);
236  QFile file(filePath);
237  if (file.open(QIODevice::ReadOnly))
238  {
239  loadLocalizationsListFile(file);
240  file.close();
241  }
242  else
243  {
244  gLog << QString("Failed to open localizations definitions file: %1").arg(definitionsFilePath);
245  }
246  }
247 }
248 
249 void Localization::LocalizationLoader::loadLocalizationsListFile(QIODevice &io)
250 {
251  int version = obtainVersion(io);
252  if (version <= 0)
253  {
254  gLog << QString("Translation definition file doesn't contain valid protocol version information.");
255  return;
256  }
257 
258  QString line = QString::fromUtf8(io.readLine());
259  while (!line.isEmpty())
260  {
261  line = line.trimmed();
262  // Discard empty and comment lines.
263  if (!line.isEmpty() && !line.startsWith("#"))
264  {
265  QStringList tokens = line.split(";");
266  if (tokens.size() == NUM_VALID_TOKENS)
267  {
268  LocalizationInfo info;
269  info.countryCodeName = tokens[0].trimmed();
270  info.localeName = tokens[1].trimmed();
271  info.niceName = tokens[2].trimmed();
272  if (!localizations.contains(info))
273  {
274  localizations.append(info);
275  }
276  }
277  else
278  {
279  gLog << QString("Invalid localization definition: %1").arg(line);
280  }
281  }
282  line = QString::fromUtf8(io.readLine());
283  }
284 }
285 
286 int Localization::LocalizationLoader::obtainVersion(QIODevice &io)
287 {
288  QString line = io.readLine();
289  int version = -1;
290  if (!line.isNull())
291  {
292  // First line contains PROTOCOL_VERSION.
293  QStringList tokens = line.split("=");
294  if (tokens.size() == NUM_VALID_VERSION_TOKENS)
295  {
296  QString versionToken = tokens[1];
297  bool bOk = false;
298  version = versionToken.toInt(&bOk);
299  if (!bOk)
300  {
301  version = -1;
302  }
303  }
304  }
305  return version;
306 }
307 
308 void Localization::LocalizationLoader::sort()
309 {
310  std::sort(localizations.begin(), localizations.end(), localizationInfoLessThan);
311 }