Don't crash while in the lobby when receiving an error IQ stanza without an error...
[0ad.git] / source / i18n / L10n.cpp
blobba0d321f86143903ecfba3a5becde6a5ab1f7764
1 /* Copyright (C) 2017 Wildfire Games.
3 * Permission is hereby granted, free of charge, to any person obtaining
4 * a copy of this software and associated documentation files (the
5 * "Software"), to deal in the Software without restriction, including
6 * without limitation the rights to use, copy, modify, merge, publish,
7 * distribute, sublicense, and/or sell copies of the Software, and to
8 * permit persons to whom the Software is furnished to do so, subject to
9 * the following conditions:
11 * The above copyright notice and this permission notice shall be included
12 * in all copies or substantial portions of the Software.
14 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17 * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
18 * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
19 * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
20 * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23 #include "precompiled.h"
25 #include "i18n/L10n.h"
27 #include <boost/algorithm/string.hpp>
28 #include <boost/concept_check.hpp>
29 #include <iostream>
30 #include <string>
32 #include "gui/GUIManager.h"
33 #include "lib/file/file_system.h"
34 #include "lib/utf8.h"
35 #include "ps/CLogger.h"
36 #include "ps/ConfigDB.h"
37 #include "ps/Filesystem.h"
38 #include "ps/GameSetup/GameSetup.h"
40 static Status ReloadChangedFileCB(void* param, const VfsPath& path)
42 return static_cast<L10n*>(param)->ReloadChangedFile(path);
45 L10n::L10n()
46 : dictionary(new tinygettext::Dictionary()), currentLocaleIsOriginalGameLocale(false), useLongStrings(false)
48 // Determine whether or not to print tinygettext messages to the standard
49 // error output, which it tinygettext’s default behavior, but not ours.
50 bool tinygettext_debug = false;
51 CFG_GET_VAL("tinygettext.debug", tinygettext_debug);
52 if (!tinygettext_debug)
54 tinygettext::Log::log_info_callback = 0;
55 tinygettext::Log::log_warning_callback = 0;
56 tinygettext::Log::log_error_callback = 0;
59 LoadListOfAvailableLocales();
60 ReevaluateCurrentLocaleAndReload();
62 // Handle hotloading
63 RegisterFileReloadFunc(ReloadChangedFileCB, this);
66 L10n::~L10n()
68 UnregisterFileReloadFunc(ReloadChangedFileCB, this);
70 for (Locale* const& locale : availableLocales)
71 delete locale;
72 delete dictionary;
75 Locale L10n::GetCurrentLocale() const
77 return currentLocale;
80 bool L10n::SaveLocale(const std::string& localeCode) const
82 if (localeCode == "long" && InDevelopmentCopy())
84 g_ConfigDB.SetValueString(CFG_USER, "locale", "long");
85 return true;
87 return SaveLocale(Locale(Locale::createCanonical(localeCode.c_str())));
90 bool L10n::SaveLocale(const Locale& locale) const
92 if (!ValidateLocale(locale))
93 return false;
95 g_ConfigDB.SetValueString(CFG_USER, "locale", locale.getName());
96 return g_ConfigDB.WriteValueToFile(CFG_USER, "locale", locale.getName());
99 bool L10n::ValidateLocale(const std::string& localeCode) const
101 return ValidateLocale(Locale::createCanonical(localeCode.c_str()));
104 // Returns true if both of these conditions are true:
105 // 1. ICU has resources for that locale (which also ensures it's a valid locale string)
106 // 2. Either a dictionary for language_country or for language is available.
107 bool L10n::ValidateLocale(const Locale& locale) const
109 if (locale.isBogus())
110 return false;
112 return !GetFallbackToAvailableDictLocale(locale).empty();
115 std::vector<std::wstring> L10n::GetDictionariesForLocale(const std::string& locale) const
117 std::vector<std::wstring> ret;
118 VfsPaths filenames;
120 std::wstring dictName = GetFallbackToAvailableDictLocale(Locale::createCanonical(locale.c_str()));
121 vfs::GetPathnames(g_VFS, L"l10n/", dictName.append(L".*.po").c_str(), filenames);
123 for (const VfsPath& path : filenames)
124 ret.push_back(path.Filename().string());
126 return ret;
129 std::wstring L10n::GetFallbackToAvailableDictLocale(const std::string& locale) const
131 return GetFallbackToAvailableDictLocale(Locale::createCanonical(locale.c_str()));
134 std::wstring L10n::GetFallbackToAvailableDictLocale(const Locale& locale) const
136 std::wstringstream stream;
138 std::function<bool(const Locale* const&)> checkLangAndCountry = [&locale](const Locale* const& l) {
139 return strcmp(locale.getLanguage(), l->getLanguage()) == 0
140 && strcmp(locale.getCountry(), l->getCountry()) == 0;
143 if (strcmp(locale.getCountry(), "") != 0
144 && std::find_if(availableLocales.begin(), availableLocales.end(), checkLangAndCountry) != availableLocales.end())
146 stream << locale.getLanguage() << L"_" << locale.getCountry();
147 return stream.str();
150 std::function<bool(const Locale* const&)> checkLang = [&locale](const Locale* const& l) {
151 return strcmp(locale.getLanguage(), l->getLanguage()) == 0;
154 if (std::find_if(availableLocales.begin(), availableLocales.end(), checkLang) != availableLocales.end())
156 stream << locale.getLanguage();
157 return stream.str();
160 return L"";
163 std::string L10n::GetDictionaryLocale(const std::string& configLocaleString) const
165 Locale out;
166 GetDictionaryLocale(configLocaleString, out);
167 return out.getName();
170 // First, try to get a valid locale from the config, then check if the system locale can be used and otherwise fall back to en_US.
171 void L10n::GetDictionaryLocale(const std::string& configLocaleString, Locale& outLocale) const
173 if (!configLocaleString.empty())
175 Locale configLocale = Locale::createCanonical(configLocaleString.c_str());
176 if (ValidateLocale(configLocale))
178 outLocale = configLocale;
179 return;
181 else
182 LOGWARNING("The configured locale is not valid or no translations are available. Falling back to another locale.");
185 Locale systemLocale = Locale::getDefault();
186 if (ValidateLocale(systemLocale))
187 outLocale = systemLocale;
188 else
189 outLocale = Locale::getUS();
192 // Try to find the best dictionary locale based on user configuration and system locale, set the currentLocale and reload the dictionary.
193 void L10n::ReevaluateCurrentLocaleAndReload()
195 std::string locale;
196 CFG_GET_VAL("locale", locale);
198 if (locale == "long")
200 // Set ICU to en_US to have a valid language for displaying dates
201 currentLocale = Locale::getUS();
202 currentLocaleIsOriginalGameLocale = false;
203 useLongStrings = true;
205 else
207 GetDictionaryLocale(locale, currentLocale);
208 currentLocaleIsOriginalGameLocale = (currentLocale == Locale::getUS()) == TRUE;
209 useLongStrings = false;
211 LoadDictionaryForCurrentLocale();
214 // Get all locales supported by ICU.
215 std::vector<std::string> L10n::GetAllLocales() const
217 std::vector<std::string> ret;
218 int32_t count;
219 const Locale* icuSupportedLocales = Locale::getAvailableLocales(count);
220 for (int i=0; i<count; ++i)
221 ret.push_back(icuSupportedLocales[i].getName());
222 return ret;
226 bool L10n::UseLongStrings() const
228 return useLongStrings;
231 std::vector<std::string> L10n::GetSupportedLocaleBaseNames() const
233 std::vector<std::string> supportedLocaleCodes;
234 for (Locale* const& locale : availableLocales)
236 if (!InDevelopmentCopy() && strcmp(locale->getBaseName(), "long") == 0)
237 continue;
238 supportedLocaleCodes.push_back(locale->getBaseName());
240 return supportedLocaleCodes;
243 std::vector<std::wstring> L10n::GetSupportedLocaleDisplayNames() const
245 std::vector<std::wstring> supportedLocaleDisplayNames;
246 for (Locale* const& locale : availableLocales)
248 if (strcmp(locale->getBaseName(), "long") == 0)
250 if (InDevelopmentCopy())
251 supportedLocaleDisplayNames.push_back(wstring_from_utf8(Translate("Long strings")));
252 continue;
255 UnicodeString utf16LocaleDisplayName;
256 locale->getDisplayName(*locale, utf16LocaleDisplayName);
257 char localeDisplayName[512];
258 CheckedArrayByteSink sink(localeDisplayName, ARRAY_SIZE(localeDisplayName));
259 utf16LocaleDisplayName.toUTF8(sink);
260 ENSURE(!sink.Overflowed());
262 supportedLocaleDisplayNames.push_back(wstring_from_utf8(std::string(localeDisplayName, sink.NumberOfBytesWritten())));
264 return supportedLocaleDisplayNames;
267 std::string L10n::GetCurrentLocaleString() const
269 return currentLocale.getName();
272 std::string L10n::GetLocaleLanguage(const std::string& locale) const
274 Locale loc = Locale::createCanonical(locale.c_str());
275 return loc.getLanguage();
278 std::string L10n::GetLocaleBaseName(const std::string& locale) const
280 Locale loc = Locale::createCanonical(locale.c_str());
281 return loc.getBaseName();
284 std::string L10n::GetLocaleCountry(const std::string& locale) const
286 Locale loc = Locale::createCanonical(locale.c_str());
287 return loc.getCountry();
290 std::string L10n::GetLocaleScript(const std::string& locale) const
292 Locale loc = Locale::createCanonical(locale.c_str());
293 return loc.getScript();
296 std::string L10n::Translate(const std::string& sourceString) const
298 if (!currentLocaleIsOriginalGameLocale)
299 return dictionary->translate(sourceString);
301 return sourceString;
304 std::string L10n::TranslateWithContext(const std::string& context, const std::string& sourceString) const
306 if (!currentLocaleIsOriginalGameLocale)
307 return dictionary->translate_ctxt(context, sourceString);
309 return sourceString;
312 std::string L10n::TranslatePlural(const std::string& singularSourceString, const std::string& pluralSourceString, int number) const
314 if (!currentLocaleIsOriginalGameLocale)
315 return dictionary->translate_plural(singularSourceString, pluralSourceString, number);
317 if (number == 1)
318 return singularSourceString;
320 return pluralSourceString;
323 std::string L10n::TranslatePluralWithContext(const std::string& context, const std::string& singularSourceString, const std::string& pluralSourceString, int number) const
325 if (!currentLocaleIsOriginalGameLocale)
326 return dictionary->translate_ctxt_plural(context, singularSourceString, pluralSourceString, number);
328 if (number == 1)
329 return singularSourceString;
331 return pluralSourceString;
334 std::string L10n::TranslateLines(const std::string& sourceString) const
336 std::string targetString;
337 std::stringstream stringOfLines(sourceString);
338 std::string line;
340 while (std::getline(stringOfLines, line))
342 if (!line.empty())
343 targetString.append(Translate(line));
344 targetString.append("\n");
347 return targetString;
350 UDate L10n::ParseDateTime(const std::string& dateTimeString, const std::string& dateTimeFormat, const Locale& locale) const
352 UErrorCode success = U_ZERO_ERROR;
353 UnicodeString utf16DateTimeString = UnicodeString::fromUTF8(dateTimeString.c_str());
354 UnicodeString utf16DateTimeFormat = UnicodeString::fromUTF8(dateTimeFormat.c_str());
356 DateFormat* dateFormatter = new SimpleDateFormat(utf16DateTimeFormat, locale, success);
357 UDate date = dateFormatter->parse(utf16DateTimeString, success);
358 delete dateFormatter;
360 return date;
363 std::string L10n::LocalizeDateTime(const UDate dateTime, const DateTimeType& type, const DateFormat::EStyle& style) const
365 UnicodeString utf16Date;
367 DateFormat* dateFormatter = CreateDateTimeInstance(type, style, currentLocale);
368 dateFormatter->format(dateTime, utf16Date);
369 char utf8Date[512];
370 CheckedArrayByteSink sink(utf8Date, ARRAY_SIZE(utf8Date));
371 utf16Date.toUTF8(sink);
372 ENSURE(!sink.Overflowed());
373 delete dateFormatter;
375 return std::string(utf8Date, sink.NumberOfBytesWritten());
378 std::string L10n::FormatMillisecondsIntoDateString(const UDate milliseconds, const std::string& formatString, bool useLocalTimezone) const
380 UErrorCode status = U_ZERO_ERROR;
381 UnicodeString dateString;
382 std::string resultString;
384 UnicodeString unicodeFormat = UnicodeString::fromUTF8(formatString.c_str());
385 SimpleDateFormat* dateFormat = new SimpleDateFormat(unicodeFormat, status);
386 if (U_FAILURE(status))
387 LOGERROR("Error creating SimpleDateFormat: %s", u_errorName(status));
389 const TimeZone* timeZone = useLocalTimezone ? TimeZone::createDefault() : TimeZone::getGMT() ;
391 status = U_ZERO_ERROR;
392 Calendar* calendar = Calendar::createInstance(*timeZone, currentLocale, status);
393 if (U_FAILURE(status))
394 LOGERROR("Error creating calendar: %s", u_errorName(status));
396 dateFormat->adoptCalendar(calendar);
397 dateFormat->format(milliseconds, dateString);
398 delete dateFormat;
400 dateString.toUTF8String(resultString);
401 return resultString;
404 std::string L10n::FormatDecimalNumberIntoString(double number) const
406 UErrorCode success = U_ZERO_ERROR;
407 UnicodeString utf16Number;
408 NumberFormat* numberFormatter = NumberFormat::createInstance(currentLocale, UNUM_DECIMAL, success);
409 numberFormatter->format(number, utf16Number);
410 char utf8Number[512];
411 CheckedArrayByteSink sink(utf8Number, ARRAY_SIZE(utf8Number));
412 utf16Number.toUTF8(sink);
413 ENSURE(!sink.Overflowed());
415 return std::string(utf8Number, sink.NumberOfBytesWritten());
418 VfsPath L10n::LocalizePath(const VfsPath& sourcePath) const
420 VfsPath localizedPath = sourcePath.Parent() / L"l10n" / wstring_from_utf8(currentLocale.getLanguage()) / sourcePath.Filename();
421 if (!VfsFileExists(localizedPath))
422 return sourcePath;
424 return localizedPath;
427 Status L10n::ReloadChangedFile(const VfsPath& path)
429 if (!boost::algorithm::starts_with(path.string(), L"l10n/"))
430 return INFO::OK;
432 if (path.Extension() != L".po")
433 return INFO::OK;
435 // If the file was deleted, ignore it
436 if (!VfsFileExists(path))
437 return INFO::OK;
439 std::wstring dictName = GetFallbackToAvailableDictLocale(currentLocale);
440 if (useLongStrings)
441 dictName = L"long";
442 if (dictName.empty())
443 return INFO::OK;
445 // Only the currently used language is loaded, so ignore all others
446 if (path.string().rfind(dictName) == std::string::npos)
447 return INFO::OK;
449 LOGMESSAGE("Hotloading translations from '%s'", path.string8());
451 CVFSFile file;
452 if (file.Load(g_VFS, path) != PSRETURN_OK)
454 LOGERROR("Failed to read translations from '%s'", path.string8());
455 return ERR::FAIL;
458 std::string content = file.DecodeUTF8();
459 ReadPoIntoDictionary(content, dictionary);
461 if (g_GUI)
462 g_GUI->ReloadAllPages();
464 return INFO::OK;
467 void L10n::LoadDictionaryForCurrentLocale()
469 delete dictionary;
470 dictionary = new tinygettext::Dictionary();
472 VfsPaths filenames;
474 if (useLongStrings)
476 if (vfs::GetPathnames(g_VFS, L"l10n/", L"long.*.po", filenames) < 0)
477 return;
479 else
481 std::wstring dictName = GetFallbackToAvailableDictLocale(currentLocale);
482 if (vfs::GetPathnames(g_VFS, L"l10n/", dictName.append(L".*.po").c_str(), filenames) < 0)
484 LOGERROR("No files for the dictionary found, but at this point the input should already be validated!");
485 return;
489 for (const VfsPath& path : filenames)
491 CVFSFile file;
492 file.Load(g_VFS, path);
493 std::string content = file.DecodeUTF8();
494 ReadPoIntoDictionary(content, dictionary);
498 void L10n::LoadListOfAvailableLocales()
500 for (Locale* const& locale : availableLocales)
501 delete locale;
502 availableLocales.clear();
504 Locale* defaultLocale = new Locale(Locale::getUS());
505 availableLocales.push_back(defaultLocale); // Always available.
507 VfsPaths filenames;
508 if (vfs::GetPathnames(g_VFS, L"l10n/", L"*.po", filenames) < 0)
509 return;
511 for (const VfsPath& path : filenames)
513 // Note: PO files follow this naming convention: “l10n/<locale code>.<mod name>.po”. For example: “l10n/gl.public.po”.
514 std::string filename = utf8_from_wstring(path.string()).substr(strlen("l10n/"));
515 size_t lengthToFirstDot = filename.find('.');
516 std::string localeCode = filename.substr(0, lengthToFirstDot);
517 Locale* locale = new Locale(Locale::createCanonical(localeCode.c_str()));
519 auto it = std::find_if(availableLocales.begin(), availableLocales.end(), [&locale](Locale* const& l) {
520 return *locale == *l;
522 if (it != availableLocales.end())
524 delete locale;
525 continue;
528 availableLocales.push_back(locale);
532 void L10n::ReadPoIntoDictionary(const std::string& poContent, tinygettext::Dictionary* dictionary) const
536 std::istringstream inputStream(poContent);
537 tinygettext::POParser::parse("virtual PO file", inputStream, *dictionary);
539 catch(std::exception& e)
541 LOGERROR("[Localization] Exception while reading virtual PO file: %s", e.what());
545 DateFormat* L10n::CreateDateTimeInstance(const L10n::DateTimeType& type, const DateFormat::EStyle& style, const Locale& locale) const
547 switch(type)
549 case Date:
550 return SimpleDateFormat::createDateInstance(style, locale);
552 case Time:
553 return SimpleDateFormat::createTimeInstance(style, locale);
555 case DateTime:
556 default:
557 return SimpleDateFormat::createDateTimeInstance(style, style, locale);