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>
32 #include "gui/GUIManager.h"
33 #include "lib/file/file_system.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
);
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();
63 RegisterFileReloadFunc(ReloadChangedFileCB
, this);
68 UnregisterFileReloadFunc(ReloadChangedFileCB
, this);
70 for (Locale
* const& locale
: availableLocales
)
75 Locale
L10n::GetCurrentLocale() const
80 bool L10n::SaveLocale(const std::string
& localeCode
) const
82 if (localeCode
== "long" && InDevelopmentCopy())
84 g_ConfigDB
.SetValueString(CFG_USER
, "locale", "long");
87 return SaveLocale(Locale(Locale::createCanonical(localeCode
.c_str())));
90 bool L10n::SaveLocale(const Locale
& locale
) const
92 if (!ValidateLocale(locale
))
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())
112 return !GetFallbackToAvailableDictLocale(locale
).empty();
115 std::vector
<std::wstring
> L10n::GetDictionariesForLocale(const std::string
& locale
) const
117 std::vector
<std::wstring
> ret
;
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());
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();
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();
163 std::string
L10n::GetDictionaryLocale(const std::string
& configLocaleString
) const
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
;
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
;
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()
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;
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
;
219 const Locale
* icuSupportedLocales
= Locale::getAvailableLocales(count
);
220 for (int i
=0; i
<count
; ++i
)
221 ret
.push_back(icuSupportedLocales
[i
].getName());
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)
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")));
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
);
304 std::string
L10n::TranslateWithContext(const std::string
& context
, const std::string
& sourceString
) const
306 if (!currentLocaleIsOriginalGameLocale
)
307 return dictionary
->translate_ctxt(context
, 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
);
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
);
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
);
340 while (std::getline(stringOfLines
, line
))
343 targetString
.append(Translate(line
));
344 targetString
.append("\n");
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
;
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
);
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
);
400 dateString
.toUTF8String(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
))
424 return localizedPath
;
427 Status
L10n::ReloadChangedFile(const VfsPath
& path
)
429 if (!boost::algorithm::starts_with(path
.string(), L
"l10n/"))
432 if (path
.Extension() != L
".po")
435 // If the file was deleted, ignore it
436 if (!VfsFileExists(path
))
439 std::wstring dictName
= GetFallbackToAvailableDictLocale(currentLocale
);
442 if (dictName
.empty())
445 // Only the currently used language is loaded, so ignore all others
446 if (path
.string().rfind(dictName
) == std::string::npos
)
449 LOGMESSAGE("Hotloading translations from '%s'", path
.string8());
452 if (file
.Load(g_VFS
, path
) != PSRETURN_OK
)
454 LOGERROR("Failed to read translations from '%s'", path
.string8());
458 std::string content
= file
.DecodeUTF8();
459 ReadPoIntoDictionary(content
, dictionary
);
462 g_GUI
->ReloadAllPages();
467 void L10n::LoadDictionaryForCurrentLocale()
470 dictionary
= new tinygettext::Dictionary();
476 if (vfs::GetPathnames(g_VFS
, L
"l10n/", L
"long.*.po", filenames
) < 0)
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!");
489 for (const VfsPath
& path
: filenames
)
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
)
502 availableLocales
.clear();
504 Locale
* defaultLocale
= new Locale(Locale::getUS());
505 availableLocales
.push_back(defaultLocale
); // Always available.
508 if (vfs::GetPathnames(g_VFS
, L
"l10n/", L
"*.po", filenames
) < 0)
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())
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
550 return SimpleDateFormat::createDateInstance(style
, locale
);
553 return SimpleDateFormat::createTimeInstance(style
, locale
);
557 return SimpleDateFormat::createDateTimeInstance(style
, style
, locale
);