2023-01-10 23:19:56 +01:00
|
|
|
/* Copyright (C) 2023 Wildfire Games.
|
2014-04-20 22:03:57 +02:00
|
|
|
*
|
|
|
|
* Permission is hereby granted, free of charge, to any person obtaining
|
|
|
|
* a copy of this software and associated documentation files (the
|
|
|
|
* "Software"), to deal in the Software without restriction, including
|
|
|
|
* without limitation the rights to use, copy, modify, merge, publish,
|
|
|
|
* distribute, sublicense, and/or sell copies of the Software, and to
|
|
|
|
* permit persons to whom the Software is furnished to do so, subject to
|
|
|
|
* the following conditions:
|
|
|
|
*
|
|
|
|
* The above copyright notice and this permission notice shall be included
|
|
|
|
* in all copies or substantial portions of the Software.
|
|
|
|
*
|
|
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
|
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
|
|
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
|
|
|
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
|
|
|
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
|
|
|
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
|
*/
|
|
|
|
|
|
|
|
#include "precompiled.h"
|
2015-07-30 01:44:05 +02:00
|
|
|
|
2014-04-20 22:03:57 +02:00
|
|
|
#include "i18n/L10n.h"
|
|
|
|
|
2014-11-09 17:58:14 +01:00
|
|
|
#include "gui/GUIManager.h"
|
2021-03-12 09:51:50 +01:00
|
|
|
#include "lib/external_libraries/tinygettext.h"
|
2014-04-20 22:03:57 +02:00
|
|
|
#include "lib/file/file_system.h"
|
|
|
|
#include "lib/utf8.h"
|
|
|
|
#include "ps/CLogger.h"
|
|
|
|
#include "ps/ConfigDB.h"
|
|
|
|
#include "ps/Filesystem.h"
|
|
|
|
#include "ps/GameSetup/GameSetup.h"
|
|
|
|
|
2023-06-30 21:10:13 +02:00
|
|
|
#include <boost/algorithm/string/predicate.hpp>
|
2021-03-12 09:51:50 +01:00
|
|
|
#include <sstream>
|
|
|
|
#include <string>
|
|
|
|
|
|
|
|
namespace
|
|
|
|
{
|
2022-10-30 01:41:01 +02:00
|
|
|
/**
|
|
|
|
* Determines the list of locales that the game supports.
|
|
|
|
*
|
|
|
|
* LoadListOfAvailableLocales() checks the locale codes of the translation
|
|
|
|
* files in the 'l10n' folder of the virtual filesystem. If it finds a
|
|
|
|
* translation file prefixed with a locale code followed by a dot, it
|
|
|
|
* determines that the game supports that locale.
|
|
|
|
*/
|
|
|
|
std::vector<icu::Locale> LoadListOfAvailableLocales()
|
|
|
|
{
|
|
|
|
// US is always available.
|
|
|
|
std::vector<icu::Locale> availableLocales{icu::Locale::getUS()};
|
|
|
|
|
|
|
|
VfsPaths filenames;
|
|
|
|
if (vfs::GetPathnames(g_VFS, L"l10n/", L"*.po", filenames) < 0)
|
|
|
|
return availableLocales;
|
|
|
|
|
|
|
|
availableLocales.reserve(filenames.size());
|
|
|
|
|
|
|
|
for (const VfsPath& path : filenames)
|
|
|
|
{
|
|
|
|
// Note: PO files follow this naming convention: "l10n/<locale code>.<mod name>.po". For example: "l10n/gl.public.po".
|
|
|
|
const std::string filename = utf8_from_wstring(path.string()).substr(strlen("l10n/"));
|
|
|
|
const size_t lengthToFirstDot = filename.find('.');
|
|
|
|
const std::string localeCode = filename.substr(0, lengthToFirstDot);
|
|
|
|
icu::Locale locale(icu::Locale::createCanonical(localeCode.c_str()));
|
|
|
|
const auto it = std::find(availableLocales.begin(), availableLocales.end(), locale);
|
|
|
|
if (it != availableLocales.end())
|
|
|
|
continue;
|
|
|
|
|
|
|
|
availableLocales.push_back(std::move(locale));
|
|
|
|
}
|
|
|
|
availableLocales.shrink_to_fit();
|
|
|
|
|
|
|
|
return availableLocales;
|
|
|
|
}
|
2021-03-12 09:51:50 +01:00
|
|
|
|
|
|
|
Status ReloadChangedFileCB(void* param, const VfsPath& path)
|
2014-11-09 17:58:14 +01:00
|
|
|
{
|
|
|
|
return static_cast<L10n*>(param)->ReloadChangedFile(path);
|
|
|
|
}
|
|
|
|
|
2021-03-12 09:51:50 +01:00
|
|
|
/**
|
|
|
|
* Loads the specified content of a PO file into the specified dictionary.
|
|
|
|
*
|
|
|
|
* Used by LoadDictionaryForCurrentLocale() to add entries to the game
|
|
|
|
* translations @link dictionary.
|
|
|
|
*
|
|
|
|
* @param poContent Content of a PO file as a string.
|
|
|
|
* @param dictionary Dictionary where the entries from the PO file should be
|
|
|
|
* stored.
|
|
|
|
*/
|
|
|
|
void ReadPoIntoDictionary(const std::string& poContent, tinygettext::Dictionary* dictionary)
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
std::istringstream inputStream(poContent);
|
|
|
|
tinygettext::POParser::parse("virtual PO file", inputStream, *dictionary);
|
|
|
|
}
|
|
|
|
catch (std::exception& e)
|
|
|
|
{
|
|
|
|
LOGERROR("[Localization] Exception while reading virtual PO file: %s", e.what());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-12 10:11:22 +01:00
|
|
|
/**
|
|
|
|
* Creates an ICU date formatted with the specified settings.
|
|
|
|
*
|
|
|
|
* @param type Whether formatted dates must show both the date and the time,
|
|
|
|
* only the date or only the time.
|
|
|
|
* @param style ICU style to format dates by default.
|
|
|
|
* @param locale Locale that the date formatter should use to parse strings.
|
|
|
|
* It has no relevance for date formatting, only matters for date
|
|
|
|
* parsing.
|
|
|
|
* @return ICU date formatter.
|
|
|
|
*/
|
2023-01-10 23:19:56 +01:00
|
|
|
std::unique_ptr<icu::DateFormat> CreateDateTimeInstance(const L10n::DateTimeType& type, const icu::DateFormat::EStyle& style, const icu::Locale& locale)
|
2021-03-12 10:11:22 +01:00
|
|
|
{
|
|
|
|
switch (type)
|
|
|
|
{
|
|
|
|
case L10n::Date:
|
2023-01-10 23:19:56 +01:00
|
|
|
return std::unique_ptr<icu::DateFormat>{
|
|
|
|
icu::SimpleDateFormat::createDateInstance(style, locale)};
|
2021-03-12 10:11:22 +01:00
|
|
|
|
|
|
|
case L10n::Time:
|
2023-01-10 23:19:56 +01:00
|
|
|
return std::unique_ptr<icu::DateFormat>{
|
|
|
|
icu::SimpleDateFormat::createTimeInstance(style, locale)};
|
2021-03-12 10:11:22 +01:00
|
|
|
|
2023-01-10 23:19:56 +01:00
|
|
|
case L10n::DateTime: FALLTHROUGH;
|
2021-03-12 10:11:22 +01:00
|
|
|
default:
|
2023-01-10 23:19:56 +01:00
|
|
|
return std::unique_ptr<icu::DateFormat>{
|
|
|
|
icu::SimpleDateFormat::createDateTimeInstance(style, style, locale)};
|
2021-03-12 10:11:22 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-03-12 09:51:50 +01:00
|
|
|
} // anonymous namespace
|
|
|
|
|
2014-04-20 22:03:57 +02:00
|
|
|
L10n::L10n()
|
2022-11-04 21:52:28 +01:00
|
|
|
: m_Dictionary(std::make_unique<tinygettext::Dictionary>())
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
2014-04-26 17:32:18 +02:00
|
|
|
// Determine whether or not to print tinygettext messages to the standard
|
2021-01-15 11:07:36 +01:00
|
|
|
// error output, which it tinygettext's default behavior, but not ours.
|
2014-04-26 17:32:18 +02:00
|
|
|
bool tinygettext_debug = false;
|
2014-11-18 00:29:49 +01:00
|
|
|
CFG_GET_VAL("tinygettext.debug", tinygettext_debug);
|
2014-04-26 17:32:18 +02:00
|
|
|
if (!tinygettext_debug)
|
|
|
|
{
|
|
|
|
tinygettext::Log::log_info_callback = 0;
|
|
|
|
tinygettext::Log::log_warning_callback = 0;
|
|
|
|
tinygettext::Log::log_error_callback = 0;
|
|
|
|
}
|
|
|
|
|
2022-10-30 01:41:01 +02:00
|
|
|
m_AvailableLocales = LoadListOfAvailableLocales();
|
|
|
|
|
2014-04-20 22:03:57 +02:00
|
|
|
ReevaluateCurrentLocaleAndReload();
|
2014-11-09 17:58:14 +01:00
|
|
|
|
|
|
|
// Handle hotloading
|
|
|
|
RegisterFileReloadFunc(ReloadChangedFileCB, this);
|
2014-04-20 22:03:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
L10n::~L10n()
|
|
|
|
{
|
2014-11-09 17:58:14 +01:00
|
|
|
UnregisterFileReloadFunc(ReloadChangedFileCB, this);
|
2014-04-20 22:03:57 +02:00
|
|
|
}
|
|
|
|
|
2022-10-30 01:41:01 +02:00
|
|
|
const icu::Locale& L10n::GetCurrentLocale() const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
2022-10-30 01:41:01 +02:00
|
|
|
return m_CurrentLocale;
|
2014-04-20 22:03:57 +02:00
|
|
|
}
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
bool L10n::SaveLocale(const std::string& localeCode) const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
2014-04-22 13:38:15 +02:00
|
|
|
if (localeCode == "long" && InDevelopmentCopy())
|
|
|
|
{
|
|
|
|
g_ConfigDB.SetValueString(CFG_USER, "locale", "long");
|
|
|
|
return true;
|
|
|
|
}
|
2018-04-10 20:13:32 +02:00
|
|
|
return SaveLocale(icu::Locale(icu::Locale::createCanonical(localeCode.c_str())));
|
2014-04-20 22:03:57 +02:00
|
|
|
}
|
|
|
|
|
2018-04-10 20:13:32 +02:00
|
|
|
bool L10n::SaveLocale(const icu::Locale& locale) const
|
2014-04-22 13:38:15 +02:00
|
|
|
{
|
2014-04-20 22:03:57 +02:00
|
|
|
if (!ValidateLocale(locale))
|
|
|
|
return false;
|
|
|
|
|
|
|
|
g_ConfigDB.SetValueString(CFG_USER, "locale", locale.getName());
|
2016-11-18 19:06:01 +01:00
|
|
|
return g_ConfigDB.WriteValueToFile(CFG_USER, "locale", locale.getName());
|
2014-04-20 22:03:57 +02:00
|
|
|
}
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
bool L10n::ValidateLocale(const std::string& localeCode) const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
2018-04-10 20:13:32 +02:00
|
|
|
return ValidateLocale(icu::Locale::createCanonical(localeCode.c_str()));
|
2014-04-20 22:03:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Returns true if both of these conditions are true:
|
2016-11-23 14:02:58 +01:00
|
|
|
// 1. ICU has resources for that locale (which also ensures it's a valid locale string)
|
2014-04-20 22:03:57 +02:00
|
|
|
// 2. Either a dictionary for language_country or for language is available.
|
2018-04-10 20:13:32 +02:00
|
|
|
bool L10n::ValidateLocale(const icu::Locale& locale) const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
2015-07-30 01:44:05 +02:00
|
|
|
if (locale.isBogus())
|
2014-04-20 22:03:57 +02:00
|
|
|
return false;
|
2014-05-07 10:14:57 +02:00
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
return !GetFallbackToAvailableDictLocale(locale).empty();
|
2014-04-20 22:03:57 +02:00
|
|
|
}
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
std::vector<std::wstring> L10n::GetDictionariesForLocale(const std::string& locale) const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
|
|
|
std::vector<std::wstring> ret;
|
|
|
|
VfsPaths filenames;
|
|
|
|
|
2018-04-10 20:13:32 +02:00
|
|
|
std::wstring dictName = GetFallbackToAvailableDictLocale(icu::Locale::createCanonical(locale.c_str()));
|
2014-05-07 10:14:57 +02:00
|
|
|
vfs::GetPathnames(g_VFS, L"l10n/", dictName.append(L".*.po").c_str(), filenames);
|
2014-04-20 22:03:57 +02:00
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
for (const VfsPath& path : filenames)
|
|
|
|
ret.push_back(path.Filename().string());
|
2014-05-07 10:14:57 +02:00
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
return ret;
|
2014-05-07 10:14:57 +02:00
|
|
|
}
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
std::wstring L10n::GetFallbackToAvailableDictLocale(const std::string& locale) const
|
2014-05-07 10:14:57 +02:00
|
|
|
{
|
2018-04-10 20:13:32 +02:00
|
|
|
return GetFallbackToAvailableDictLocale(icu::Locale::createCanonical(locale.c_str()));
|
2014-05-07 10:14:57 +02:00
|
|
|
}
|
|
|
|
|
2018-04-10 20:13:32 +02:00
|
|
|
std::wstring L10n::GetFallbackToAvailableDictLocale(const icu::Locale& locale) const
|
2014-05-07 10:14:57 +02:00
|
|
|
{
|
|
|
|
std::wstringstream stream;
|
2015-07-30 01:44:05 +02:00
|
|
|
|
2022-10-30 01:41:01 +02:00
|
|
|
const auto checkLangAndCountry = [&locale](const icu::Locale& l)
|
|
|
|
{
|
|
|
|
return strcmp(locale.getLanguage(), l.getLanguage()) == 0
|
|
|
|
&& strcmp(locale.getCountry(), l.getCountry()) == 0;
|
2015-07-30 01:44:05 +02:00
|
|
|
};
|
2016-11-23 15:09:58 +01:00
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
if (strcmp(locale.getCountry(), "") != 0
|
2022-10-30 01:41:01 +02:00
|
|
|
&& std::find_if(m_AvailableLocales.begin(), m_AvailableLocales.end(), checkLangAndCountry) !=
|
|
|
|
m_AvailableLocales.end())
|
2014-05-07 10:14:57 +02:00
|
|
|
{
|
2015-07-30 01:44:05 +02:00
|
|
|
stream << locale.getLanguage() << L"_" << locale.getCountry();
|
|
|
|
return stream.str();
|
2014-05-07 10:14:57 +02:00
|
|
|
}
|
|
|
|
|
2022-10-30 01:41:01 +02:00
|
|
|
const auto checkLang = [&locale](const icu::Locale& l)
|
|
|
|
{
|
|
|
|
return strcmp(locale.getLanguage(), l.getLanguage()) == 0;
|
2015-07-30 01:44:05 +02:00
|
|
|
};
|
|
|
|
|
2022-10-30 01:41:01 +02:00
|
|
|
if (std::find_if(m_AvailableLocales.begin(), m_AvailableLocales.end(), checkLang) !=
|
|
|
|
m_AvailableLocales.end())
|
2014-05-07 10:14:57 +02:00
|
|
|
{
|
|
|
|
stream << locale.getLanguage();
|
|
|
|
return stream.str();
|
|
|
|
}
|
2016-11-23 15:09:58 +01:00
|
|
|
|
2014-05-07 10:14:57 +02:00
|
|
|
return L"";
|
|
|
|
}
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
std::string L10n::GetDictionaryLocale(const std::string& configLocaleString) const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
2018-04-10 20:13:32 +02:00
|
|
|
icu::Locale out;
|
2014-04-20 22:03:57 +02:00
|
|
|
GetDictionaryLocale(configLocaleString, out);
|
|
|
|
return out.getName();
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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.
|
2018-04-10 20:13:32 +02:00
|
|
|
void L10n::GetDictionaryLocale(const std::string& configLocaleString, icu::Locale& outLocale) const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
|
|
|
if (!configLocaleString.empty())
|
|
|
|
{
|
2018-04-10 20:13:32 +02:00
|
|
|
icu::Locale configLocale = icu::Locale::createCanonical(configLocaleString.c_str());
|
2014-04-20 22:03:57 +02:00
|
|
|
if (ValidateLocale(configLocale))
|
|
|
|
{
|
|
|
|
outLocale = configLocale;
|
2014-04-26 14:29:01 +02:00
|
|
|
return;
|
2014-04-20 22:03:57 +02:00
|
|
|
}
|
|
|
|
else
|
2015-01-22 21:31:30 +01:00
|
|
|
LOGWARNING("The configured locale is not valid or no translations are available. Falling back to another locale.");
|
2014-04-20 22:03:57 +02:00
|
|
|
}
|
2016-11-23 15:09:58 +01:00
|
|
|
|
2018-04-10 20:13:32 +02:00
|
|
|
icu::Locale systemLocale = icu::Locale::getDefault();
|
2014-04-26 14:29:01 +02:00
|
|
|
if (ValidateLocale(systemLocale))
|
|
|
|
outLocale = systemLocale;
|
|
|
|
else
|
2018-04-10 20:13:32 +02:00
|
|
|
outLocale = icu::Locale::getUS();
|
2014-04-20 22:03:57 +02:00
|
|
|
}
|
|
|
|
|
2014-05-07 10:14:57 +02:00
|
|
|
// Try to find the best dictionary locale based on user configuration and system locale, set the currentLocale and reload the dictionary.
|
2014-04-20 22:03:57 +02:00
|
|
|
void L10n::ReevaluateCurrentLocaleAndReload()
|
|
|
|
{
|
|
|
|
std::string locale;
|
2014-11-18 00:29:49 +01:00
|
|
|
CFG_GET_VAL("locale", locale);
|
2014-04-22 13:38:15 +02:00
|
|
|
|
|
|
|
if (locale == "long")
|
|
|
|
{
|
|
|
|
// Set ICU to en_US to have a valid language for displaying dates
|
2022-10-30 01:41:01 +02:00
|
|
|
m_CurrentLocale = icu::Locale::getUS();
|
|
|
|
m_CurrentLocaleIsOriginalGameLocale = false;
|
|
|
|
m_UseLongStrings = true;
|
2014-04-22 13:38:15 +02:00
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2022-10-30 01:41:01 +02:00
|
|
|
GetDictionaryLocale(locale, m_CurrentLocale);
|
|
|
|
m_CurrentLocaleIsOriginalGameLocale = (m_CurrentLocale == icu::Locale::getUS()) == 1;
|
|
|
|
m_UseLongStrings = false;
|
2014-04-22 13:38:15 +02:00
|
|
|
}
|
2014-04-20 22:03:57 +02:00
|
|
|
LoadDictionaryForCurrentLocale();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Get all locales supported by ICU.
|
2015-07-30 01:44:05 +02:00
|
|
|
std::vector<std::string> L10n::GetAllLocales() const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
|
|
|
std::vector<std::string> ret;
|
|
|
|
int32_t count;
|
2018-04-10 20:13:32 +02:00
|
|
|
const icu::Locale* icuSupportedLocales = icu::Locale::getAvailableLocales(count);
|
2014-04-20 22:03:57 +02:00
|
|
|
for (int i=0; i<count; ++i)
|
|
|
|
ret.push_back(icuSupportedLocales[i].getName());
|
|
|
|
return ret;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
bool L10n::UseLongStrings() const
|
2014-04-22 13:38:15 +02:00
|
|
|
{
|
2022-10-30 01:41:01 +02:00
|
|
|
return m_UseLongStrings;
|
2014-04-22 13:38:15 +02:00
|
|
|
};
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
std::vector<std::string> L10n::GetSupportedLocaleBaseNames() const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
|
|
|
std::vector<std::string> supportedLocaleCodes;
|
2022-10-30 01:41:01 +02:00
|
|
|
for (const icu::Locale& locale : m_AvailableLocales)
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
2022-10-30 01:41:01 +02:00
|
|
|
if (!InDevelopmentCopy() && strcmp(locale.getBaseName(), "long") == 0)
|
2014-04-20 22:03:57 +02:00
|
|
|
continue;
|
2022-10-30 01:41:01 +02:00
|
|
|
supportedLocaleCodes.push_back(locale.getBaseName());
|
2014-04-20 22:03:57 +02:00
|
|
|
}
|
|
|
|
return supportedLocaleCodes;
|
|
|
|
}
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
std::vector<std::wstring> L10n::GetSupportedLocaleDisplayNames() const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
|
|
|
std::vector<std::wstring> supportedLocaleDisplayNames;
|
2022-10-30 01:41:01 +02:00
|
|
|
for (const icu::Locale& locale : m_AvailableLocales)
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
2022-10-30 01:41:01 +02:00
|
|
|
if (strcmp(locale.getBaseName(), "long") == 0)
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
|
|
|
if (InDevelopmentCopy())
|
|
|
|
supportedLocaleDisplayNames.push_back(wstring_from_utf8(Translate("Long strings")));
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2018-04-10 20:13:32 +02:00
|
|
|
icu::UnicodeString utf16LocaleDisplayName;
|
2022-10-30 01:41:01 +02:00
|
|
|
locale.getDisplayName(locale, utf16LocaleDisplayName);
|
2014-04-20 22:03:57 +02:00
|
|
|
char localeDisplayName[512];
|
2018-04-10 20:13:32 +02:00
|
|
|
icu::CheckedArrayByteSink sink(localeDisplayName, ARRAY_SIZE(localeDisplayName));
|
2014-04-20 22:03:57 +02:00
|
|
|
utf16LocaleDisplayName.toUTF8(sink);
|
|
|
|
ENSURE(!sink.Overflowed());
|
|
|
|
|
|
|
|
supportedLocaleDisplayNames.push_back(wstring_from_utf8(std::string(localeDisplayName, sink.NumberOfBytesWritten())));
|
|
|
|
}
|
|
|
|
return supportedLocaleDisplayNames;
|
|
|
|
}
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
std::string L10n::GetCurrentLocaleString() const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
2022-10-30 01:41:01 +02:00
|
|
|
return m_CurrentLocale.getName();
|
2014-04-20 22:03:57 +02:00
|
|
|
}
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
std::string L10n::GetLocaleLanguage(const std::string& locale) const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
2018-04-10 20:13:32 +02:00
|
|
|
icu::Locale loc = icu::Locale::createCanonical(locale.c_str());
|
2014-04-20 22:03:57 +02:00
|
|
|
return loc.getLanguage();
|
|
|
|
}
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
std::string L10n::GetLocaleBaseName(const std::string& locale) const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
2018-04-10 20:13:32 +02:00
|
|
|
icu::Locale loc = icu::Locale::createCanonical(locale.c_str());
|
2014-04-20 22:03:57 +02:00
|
|
|
return loc.getBaseName();
|
|
|
|
}
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
std::string L10n::GetLocaleCountry(const std::string& locale) const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
2018-04-10 20:13:32 +02:00
|
|
|
icu::Locale loc = icu::Locale::createCanonical(locale.c_str());
|
2014-04-20 22:03:57 +02:00
|
|
|
return loc.getCountry();
|
|
|
|
}
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
std::string L10n::GetLocaleScript(const std::string& locale) const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
2018-04-10 20:13:32 +02:00
|
|
|
icu::Locale loc = icu::Locale::createCanonical(locale.c_str());
|
2014-04-20 22:03:57 +02:00
|
|
|
return loc.getScript();
|
|
|
|
}
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
std::string L10n::Translate(const std::string& sourceString) const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
2022-10-30 01:41:01 +02:00
|
|
|
if (!m_CurrentLocaleIsOriginalGameLocale)
|
2020-11-26 23:29:36 +01:00
|
|
|
return m_Dictionary->translate(sourceString);
|
2014-04-20 22:03:57 +02:00
|
|
|
|
|
|
|
return sourceString;
|
|
|
|
}
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
std::string L10n::TranslateWithContext(const std::string& context, const std::string& sourceString) const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
2022-10-30 01:41:01 +02:00
|
|
|
if (!m_CurrentLocaleIsOriginalGameLocale)
|
2020-11-26 23:29:36 +01:00
|
|
|
return m_Dictionary->translate_ctxt(context, sourceString);
|
2014-04-20 22:03:57 +02:00
|
|
|
|
|
|
|
return sourceString;
|
|
|
|
}
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
std::string L10n::TranslatePlural(const std::string& singularSourceString, const std::string& pluralSourceString, int number) const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
2022-10-30 01:41:01 +02:00
|
|
|
if (!m_CurrentLocaleIsOriginalGameLocale)
|
2020-11-26 23:29:36 +01:00
|
|
|
return m_Dictionary->translate_plural(singularSourceString, pluralSourceString, number);
|
2014-04-20 22:03:57 +02:00
|
|
|
|
|
|
|
if (number == 1)
|
|
|
|
return singularSourceString;
|
|
|
|
|
|
|
|
return pluralSourceString;
|
|
|
|
}
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
std::string L10n::TranslatePluralWithContext(const std::string& context, const std::string& singularSourceString, const std::string& pluralSourceString, int number) const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
2022-10-30 01:41:01 +02:00
|
|
|
if (!m_CurrentLocaleIsOriginalGameLocale)
|
2020-11-26 23:29:36 +01:00
|
|
|
return m_Dictionary->translate_ctxt_plural(context, singularSourceString, pluralSourceString, number);
|
2014-04-20 22:03:57 +02:00
|
|
|
|
|
|
|
if (number == 1)
|
|
|
|
return singularSourceString;
|
|
|
|
|
|
|
|
return pluralSourceString;
|
|
|
|
}
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
std::string L10n::TranslateLines(const std::string& sourceString) const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
|
|
|
std::string targetString;
|
|
|
|
std::stringstream stringOfLines(sourceString);
|
|
|
|
std::string line;
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
while (std::getline(stringOfLines, line))
|
|
|
|
{
|
2014-04-26 18:05:41 +02:00
|
|
|
if (!line.empty())
|
|
|
|
targetString.append(Translate(line));
|
2014-04-20 22:03:57 +02:00
|
|
|
targetString.append("\n");
|
|
|
|
}
|
|
|
|
|
|
|
|
return targetString;
|
|
|
|
}
|
|
|
|
|
2018-04-10 20:13:32 +02:00
|
|
|
UDate L10n::ParseDateTime(const std::string& dateTimeString, const std::string& dateTimeFormat, const icu::Locale& locale) const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
|
|
|
UErrorCode success = U_ZERO_ERROR;
|
2018-04-10 20:13:32 +02:00
|
|
|
icu::UnicodeString utf16DateTimeString = icu::UnicodeString::fromUTF8(dateTimeString.c_str());
|
|
|
|
icu::UnicodeString utf16DateTimeFormat = icu::UnicodeString::fromUTF8(dateTimeFormat.c_str());
|
2014-04-20 22:03:57 +02:00
|
|
|
|
2023-01-10 23:19:56 +01:00
|
|
|
const icu::SimpleDateFormat dateFormatter{utf16DateTimeFormat, locale, success};
|
|
|
|
return dateFormatter.parse(utf16DateTimeString, success);
|
2014-04-20 22:03:57 +02:00
|
|
|
}
|
|
|
|
|
2018-04-10 20:13:32 +02:00
|
|
|
std::string L10n::LocalizeDateTime(const UDate dateTime, const DateTimeType& type, const icu::DateFormat::EStyle& style) const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
2018-04-10 20:13:32 +02:00
|
|
|
icu::UnicodeString utf16Date;
|
2014-04-20 22:03:57 +02:00
|
|
|
|
2023-01-10 23:19:56 +01:00
|
|
|
const std::unique_ptr<const icu::DateFormat> dateFormatter{
|
|
|
|
CreateDateTimeInstance(type, style, m_CurrentLocale)};
|
2014-04-20 22:03:57 +02:00
|
|
|
dateFormatter->format(dateTime, utf16Date);
|
|
|
|
char utf8Date[512];
|
2018-04-10 20:13:32 +02:00
|
|
|
icu::CheckedArrayByteSink sink(utf8Date, ARRAY_SIZE(utf8Date));
|
2014-04-20 22:03:57 +02:00
|
|
|
utf16Date.toUTF8(sink);
|
|
|
|
ENSURE(!sink.Overflowed());
|
|
|
|
|
|
|
|
return std::string(utf8Date, sink.NumberOfBytesWritten());
|
|
|
|
}
|
|
|
|
|
2017-01-28 11:59:53 +01:00
|
|
|
std::string L10n::FormatMillisecondsIntoDateString(const UDate milliseconds, const std::string& formatString, bool useLocalTimezone) const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
2014-12-14 18:43:40 +01:00
|
|
|
UErrorCode status = U_ZERO_ERROR;
|
2018-04-10 20:13:32 +02:00
|
|
|
icu::UnicodeString dateString;
|
2014-12-12 02:11:00 +01:00
|
|
|
std::string resultString;
|
2014-04-20 22:03:57 +02:00
|
|
|
|
2018-04-10 20:13:32 +02:00
|
|
|
icu::UnicodeString unicodeFormat = icu::UnicodeString::fromUTF8(formatString.c_str());
|
2023-01-10 23:19:56 +01:00
|
|
|
icu::SimpleDateFormat dateFormat{unicodeFormat, status};
|
2014-12-12 02:11:00 +01:00
|
|
|
if (U_FAILURE(status))
|
2015-01-22 21:36:24 +01:00
|
|
|
LOGERROR("Error creating SimpleDateFormat: %s", u_errorName(status));
|
2014-04-20 22:03:57 +02:00
|
|
|
|
2014-12-14 18:43:40 +01:00
|
|
|
status = U_ZERO_ERROR;
|
2023-01-10 23:19:56 +01:00
|
|
|
std::unique_ptr<icu::Calendar> calendar{
|
|
|
|
useLocalTimezone ?
|
|
|
|
icu::Calendar::createInstance(m_CurrentLocale, status) :
|
|
|
|
icu::Calendar::createInstance(*icu::TimeZone::getGMT(), m_CurrentLocale, status)};
|
2019-08-24 19:33:29 +02:00
|
|
|
|
2014-12-12 02:11:00 +01:00
|
|
|
if (U_FAILURE(status))
|
2015-01-22 21:36:24 +01:00
|
|
|
LOGERROR("Error creating calendar: %s", u_errorName(status));
|
2016-11-23 14:02:58 +01:00
|
|
|
|
2023-01-10 23:19:56 +01:00
|
|
|
dateFormat.adoptCalendar(calendar.release());
|
|
|
|
dateFormat.format(milliseconds, dateString);
|
2014-04-20 22:03:57 +02:00
|
|
|
|
2014-12-12 02:11:00 +01:00
|
|
|
dateString.toUTF8String(resultString);
|
|
|
|
return resultString;
|
2014-04-20 22:03:57 +02:00
|
|
|
}
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
std::string L10n::FormatDecimalNumberIntoString(double number) const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
|
|
|
UErrorCode success = U_ZERO_ERROR;
|
2018-04-10 20:13:32 +02:00
|
|
|
icu::UnicodeString utf16Number;
|
2023-01-10 23:19:56 +01:00
|
|
|
std::unique_ptr<icu::NumberFormat> numberFormatter{
|
|
|
|
icu::NumberFormat::createInstance(m_CurrentLocale, UNUM_DECIMAL, success)};
|
2014-04-20 22:03:57 +02:00
|
|
|
numberFormatter->format(number, utf16Number);
|
|
|
|
char utf8Number[512];
|
2018-04-10 20:13:32 +02:00
|
|
|
icu::CheckedArrayByteSink sink(utf8Number, ARRAY_SIZE(utf8Number));
|
2014-04-20 22:03:57 +02:00
|
|
|
utf16Number.toUTF8(sink);
|
|
|
|
ENSURE(!sink.Overflowed());
|
|
|
|
|
|
|
|
return std::string(utf8Number, sink.NumberOfBytesWritten());
|
|
|
|
}
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
VfsPath L10n::LocalizePath(const VfsPath& sourcePath) const
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
2022-10-30 01:41:01 +02:00
|
|
|
VfsPath localizedPath = sourcePath.Parent() / L"l10n" /
|
|
|
|
wstring_from_utf8(m_CurrentLocale.getLanguage()) / sourcePath.Filename();
|
2014-12-12 02:11:00 +01:00
|
|
|
if (!VfsFileExists(localizedPath))
|
|
|
|
return sourcePath;
|
2014-04-20 22:03:57 +02:00
|
|
|
|
2014-12-12 02:11:00 +01:00
|
|
|
return localizedPath;
|
2014-04-20 22:03:57 +02:00
|
|
|
}
|
|
|
|
|
2014-11-09 17:58:14 +01:00
|
|
|
Status L10n::ReloadChangedFile(const VfsPath& path)
|
|
|
|
{
|
|
|
|
if (!boost::algorithm::starts_with(path.string(), L"l10n/"))
|
|
|
|
return INFO::OK;
|
|
|
|
|
|
|
|
if (path.Extension() != L".po")
|
|
|
|
return INFO::OK;
|
|
|
|
|
|
|
|
// If the file was deleted, ignore it
|
|
|
|
if (!VfsFileExists(path))
|
|
|
|
return INFO::OK;
|
|
|
|
|
2022-10-30 01:41:01 +02:00
|
|
|
std::wstring dictName = GetFallbackToAvailableDictLocale(m_CurrentLocale);
|
|
|
|
if (m_UseLongStrings)
|
2014-11-09 17:58:14 +01:00
|
|
|
dictName = L"long";
|
|
|
|
if (dictName.empty())
|
|
|
|
return INFO::OK;
|
|
|
|
|
|
|
|
// Only the currently used language is loaded, so ignore all others
|
|
|
|
if (path.string().rfind(dictName) == std::string::npos)
|
|
|
|
return INFO::OK;
|
|
|
|
|
2015-01-22 21:36:24 +01:00
|
|
|
LOGMESSAGE("Hotloading translations from '%s'", path.string8());
|
2014-11-09 17:58:14 +01:00
|
|
|
|
|
|
|
CVFSFile file;
|
|
|
|
if (file.Load(g_VFS, path) != PSRETURN_OK)
|
|
|
|
{
|
2015-01-22 21:36:24 +01:00
|
|
|
LOGERROR("Failed to read translations from '%s'", path.string8());
|
2014-11-09 17:58:14 +01:00
|
|
|
return ERR::FAIL;
|
|
|
|
}
|
|
|
|
|
|
|
|
std::string content = file.DecodeUTF8();
|
2020-11-26 23:29:36 +01:00
|
|
|
ReadPoIntoDictionary(content, m_Dictionary.get());
|
2014-11-09 17:58:14 +01:00
|
|
|
|
|
|
|
if (g_GUI)
|
|
|
|
g_GUI->ReloadAllPages();
|
|
|
|
|
|
|
|
return INFO::OK;
|
|
|
|
}
|
|
|
|
|
2014-04-20 22:03:57 +02:00
|
|
|
void L10n::LoadDictionaryForCurrentLocale()
|
|
|
|
{
|
2022-11-04 21:52:28 +01:00
|
|
|
m_Dictionary = std::make_unique<tinygettext::Dictionary>();
|
2014-04-20 22:03:57 +02:00
|
|
|
VfsPaths filenames;
|
2014-04-22 13:38:15 +02:00
|
|
|
|
2022-10-30 01:41:01 +02:00
|
|
|
if (m_UseLongStrings)
|
2014-04-22 13:38:15 +02:00
|
|
|
{
|
|
|
|
if (vfs::GetPathnames(g_VFS, L"l10n/", L"long.*.po", filenames) < 0)
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
2022-10-30 01:41:01 +02:00
|
|
|
std::wstring dictName = GetFallbackToAvailableDictLocale(m_CurrentLocale);
|
2014-05-07 10:14:57 +02:00
|
|
|
if (vfs::GetPathnames(g_VFS, L"l10n/", dictName.append(L".*.po").c_str(), filenames) < 0)
|
|
|
|
{
|
2015-01-22 21:31:30 +01:00
|
|
|
LOGERROR("No files for the dictionary found, but at this point the input should already be validated!");
|
2014-04-20 22:03:57 +02:00
|
|
|
return;
|
2014-05-07 10:14:57 +02:00
|
|
|
}
|
2014-04-20 22:03:57 +02:00
|
|
|
}
|
|
|
|
|
2015-07-30 01:44:05 +02:00
|
|
|
for (const VfsPath& path : filenames)
|
2014-04-20 22:03:57 +02:00
|
|
|
{
|
|
|
|
CVFSFile file;
|
2015-07-30 01:44:05 +02:00
|
|
|
file.Load(g_VFS, path);
|
2014-04-20 22:03:57 +02:00
|
|
|
std::string content = file.DecodeUTF8();
|
2020-11-26 23:29:36 +01:00
|
|
|
ReadPoIntoDictionary(content, m_Dictionary.get());
|
2014-04-20 22:03:57 +02:00
|
|
|
}
|
|
|
|
}
|