mirror of
https://gitlab.com/OpenMW/openmw.git
synced 2025-04-28 21:07:59 +03:00
Lua i18n updates
This commit is contained in:
parent
5a2cafebea
commit
21ffbcc4b4
37 changed files with 839 additions and 1153 deletions
|
@ -1,111 +0,0 @@
|
|||
#include "i18n.hpp"
|
||||
|
||||
#include <components/debug/debuglog.hpp>
|
||||
|
||||
namespace sol
|
||||
{
|
||||
template <>
|
||||
struct is_automagical<LuaUtil::I18nManager::Context> : std::false_type {};
|
||||
}
|
||||
|
||||
namespace LuaUtil
|
||||
{
|
||||
|
||||
void I18nManager::init()
|
||||
{
|
||||
mPreferredLanguages.push_back("en");
|
||||
sol::usertype<Context> ctx = mLua->sol().new_usertype<Context>("I18nContext");
|
||||
ctx[sol::meta_function::call] = &Context::translate;
|
||||
try
|
||||
{
|
||||
mI18nLoader = mLua->loadInternalLib("i18n");
|
||||
sol::set_environment(mLua->newInternalLibEnvironment(), mI18nLoader);
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Log(Debug::Error) << "LuaUtil::I18nManager initialization failed: " << e.what();
|
||||
}
|
||||
}
|
||||
|
||||
void I18nManager::setPreferredLanguages(const std::vector<std::string>& langs)
|
||||
{
|
||||
{
|
||||
Log msg(Debug::Info);
|
||||
msg << "I18n preferred languages:";
|
||||
for (const std::string& l : langs)
|
||||
msg << " " << l;
|
||||
}
|
||||
mPreferredLanguages = langs;
|
||||
for (auto& [_, context] : mContexts)
|
||||
context.updateLang(this);
|
||||
}
|
||||
|
||||
void I18nManager::Context::readLangData(I18nManager* manager, const std::string& lang)
|
||||
{
|
||||
std::string path = "i18n/";
|
||||
path.append(mName);
|
||||
path.append("/");
|
||||
path.append(lang);
|
||||
path.append(".lua");
|
||||
if (!manager->mVFS->exists(path))
|
||||
return;
|
||||
try
|
||||
{
|
||||
sol::protected_function dataFn = manager->mLua->loadFromVFS(path);
|
||||
sol::environment emptyEnv(manager->mLua->sol(), sol::create);
|
||||
sol::set_environment(emptyEnv, dataFn);
|
||||
sol::table data = manager->mLua->newTable();
|
||||
data[lang] = call(dataFn);
|
||||
call(mI18n["load"], data);
|
||||
mLoadedLangs[lang] = true;
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
Log(Debug::Error) << "Can not load " << path << ": " << e.what();
|
||||
}
|
||||
}
|
||||
|
||||
sol::object I18nManager::Context::translate(std::string_view key, const sol::object& data)
|
||||
{
|
||||
sol::object res = call(mI18n["translate"], key, data);
|
||||
if (res != sol::nil)
|
||||
return res;
|
||||
|
||||
// If not found in a language file - register the key itself as a message.
|
||||
std::string composedKey = call(mI18n["getLocale"]).get<std::string>();
|
||||
composedKey.push_back('.');
|
||||
composedKey.append(key);
|
||||
call(mI18n["set"], composedKey, key);
|
||||
return call(mI18n["translate"], key, data);
|
||||
}
|
||||
|
||||
void I18nManager::Context::updateLang(I18nManager* manager)
|
||||
{
|
||||
for (const std::string& lang : manager->mPreferredLanguages)
|
||||
{
|
||||
if (mLoadedLangs[lang] == sol::nil)
|
||||
readLangData(manager, lang);
|
||||
if (mLoadedLangs[lang] != sol::nil)
|
||||
{
|
||||
Log(Debug::Verbose) << "Language file \"i18n/" << mName << "/" << lang << ".lua\" is enabled";
|
||||
call(mI18n["setLocale"], lang);
|
||||
return;
|
||||
}
|
||||
}
|
||||
Log(Debug::Warning) << "No language files for the preferred languages found in \"i18n/" << mName << "\"";
|
||||
}
|
||||
|
||||
sol::object I18nManager::getContext(const std::string& contextName)
|
||||
{
|
||||
if (mI18nLoader == sol::nil)
|
||||
throw std::runtime_error("LuaUtil::I18nManager is not initialized");
|
||||
auto it = mContexts.find(contextName);
|
||||
if (it != mContexts.end())
|
||||
return sol::make_object(mLua->sol(), it->second);
|
||||
Context ctx{contextName, mLua->newTable(), call(mI18nLoader, "i18n.init")};
|
||||
ctx.updateLang(this);
|
||||
mContexts.emplace(contextName, ctx);
|
||||
return sol::make_object(mLua->sol(), ctx);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
#ifndef COMPONENTS_LUA_I18N_H
|
||||
#define COMPONENTS_LUA_I18N_H
|
||||
|
||||
#include "luastate.hpp"
|
||||
|
||||
namespace LuaUtil
|
||||
{
|
||||
|
||||
class I18nManager
|
||||
{
|
||||
public:
|
||||
I18nManager(const VFS::Manager* vfs, LuaState* lua) : mVFS(vfs), mLua(lua) {}
|
||||
void init();
|
||||
|
||||
void setPreferredLanguages(const std::vector<std::string>& langs);
|
||||
const std::vector<std::string>& getPreferredLanguages() const { return mPreferredLanguages; }
|
||||
|
||||
sol::object getContext(const std::string& contextName);
|
||||
|
||||
private:
|
||||
struct Context
|
||||
{
|
||||
std::string mName;
|
||||
sol::table mLoadedLangs;
|
||||
sol::table mI18n;
|
||||
|
||||
void updateLang(I18nManager* manager);
|
||||
void readLangData(I18nManager* manager, const std::string& lang);
|
||||
sol::object translate(std::string_view key, const sol::object& data);
|
||||
};
|
||||
|
||||
const VFS::Manager* mVFS;
|
||||
LuaState* mLua;
|
||||
sol::object mI18nLoader = sol::nil;
|
||||
std::vector<std::string> mPreferredLanguages;
|
||||
std::map<std::string, Context> mContexts;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // COMPONENTS_LUA_I18N_H
|
136
components/lua/l10n.cpp
Normal file
136
components/lua/l10n.cpp
Normal file
|
@ -0,0 +1,136 @@
|
|||
#include "l10n.hpp"
|
||||
|
||||
#include <unicode/errorcode.h>
|
||||
|
||||
#include <components/debug/debuglog.hpp>
|
||||
|
||||
namespace sol
|
||||
{
|
||||
template <>
|
||||
struct is_automagical<LuaUtil::L10nManager::Context> : std::false_type {};
|
||||
}
|
||||
|
||||
namespace LuaUtil
|
||||
{
|
||||
void L10nManager::init()
|
||||
{
|
||||
sol::usertype<Context> ctx = mLua->sol().new_usertype<Context>("L10nContext");
|
||||
ctx[sol::meta_function::call] = &Context::translate;
|
||||
}
|
||||
|
||||
void L10nManager::setPreferredLocales(const std::vector<std::string>& langs)
|
||||
{
|
||||
mPreferredLocales.clear();
|
||||
for (const auto &lang : langs)
|
||||
mPreferredLocales.push_back(icu::Locale(lang.c_str()));
|
||||
{
|
||||
Log msg(Debug::Info);
|
||||
msg << "Preferred locales:";
|
||||
for (const icu::Locale& l : mPreferredLocales)
|
||||
msg << " " << l.getName();
|
||||
}
|
||||
for (auto& [_, context] : mContexts)
|
||||
context.updateLang(this);
|
||||
}
|
||||
|
||||
void L10nManager::Context::readLangData(L10nManager* manager, const icu::Locale& lang)
|
||||
{
|
||||
std::string path = "l10n/";
|
||||
path.append(mName);
|
||||
path.append("/");
|
||||
path.append(lang.getName());
|
||||
path.append(".yaml");
|
||||
if (!manager->mVFS->exists(path))
|
||||
return;
|
||||
|
||||
mMessageBundles->load(*manager->mVFS->get(path), lang, path);
|
||||
}
|
||||
|
||||
std::pair<std::vector<icu::Formattable>, std::vector<icu::UnicodeString>> getICUArgs(std::string_view messageId, const sol::table &table)
|
||||
{
|
||||
std::vector<icu::Formattable> args;
|
||||
std::vector<icu::UnicodeString> argNames;
|
||||
for (auto elem : table)
|
||||
for (auto& [key, value] : table)
|
||||
{
|
||||
// Argument values
|
||||
if (value.is<std::string>())
|
||||
args.push_back(icu::Formattable(value.as<std::string>().c_str()));
|
||||
// Note: While we pass all numbers as doubles, they still seem to be handled appropriately.
|
||||
// Numbers can be forced to be integers using the argType number and argStyle integer
|
||||
// E.g. {var, number, integer}
|
||||
else if (value.is<double>())
|
||||
args.push_back(icu::Formattable(value.as<double>()));
|
||||
else
|
||||
{
|
||||
Log(Debug::Error) << "Unrecognized argument type for key \"" << key.as<std::string>()
|
||||
<< "\" when formatting message \"" << messageId << "\"";
|
||||
}
|
||||
|
||||
// Argument names
|
||||
argNames.push_back(icu::UnicodeString::fromUTF8(key.as<std::string>()));
|
||||
}
|
||||
return std::make_pair(args, argNames);
|
||||
}
|
||||
|
||||
std::string L10nManager::Context::translate(std::string_view key, const sol::object& data)
|
||||
{
|
||||
std::vector<icu::Formattable> args;
|
||||
std::vector<icu::UnicodeString> argNames;
|
||||
|
||||
if (data.is<sol::table>()) {
|
||||
sol::table dataTable = data.as<sol::table>();
|
||||
auto argData = getICUArgs(key, dataTable);
|
||||
args = argData.first;
|
||||
argNames = argData.second;
|
||||
}
|
||||
|
||||
return mMessageBundles->formatMessage(key, argNames, args);
|
||||
}
|
||||
|
||||
void L10nManager::Context::updateLang(L10nManager* manager)
|
||||
{
|
||||
icu::Locale fallbackLocale = mMessageBundles->getFallbackLocale();
|
||||
mMessageBundles->setPreferredLocales(manager->mPreferredLocales);
|
||||
int localeCount = 0;
|
||||
bool fallbackLocaleInPreferred = false;
|
||||
for (const icu::Locale& loc: mMessageBundles->getPreferredLocales())
|
||||
{
|
||||
if (!mMessageBundles->isLoaded(loc))
|
||||
readLangData(manager, loc);
|
||||
if (mMessageBundles->isLoaded(loc))
|
||||
{
|
||||
localeCount++;
|
||||
Log(Debug::Verbose) << "Language file \"l10n/" << mName << "/" << loc.getName() << ".yaml\" is enabled";
|
||||
if (loc == fallbackLocale)
|
||||
fallbackLocaleInPreferred = true;
|
||||
}
|
||||
}
|
||||
if (!mMessageBundles->isLoaded(fallbackLocale))
|
||||
readLangData(manager, fallbackLocale);
|
||||
if (mMessageBundles->isLoaded(fallbackLocale) && !fallbackLocaleInPreferred)
|
||||
Log(Debug::Verbose) << "Fallback language file \"l10n/" << mName << "/" << fallbackLocale.getName() << ".yaml\" is enabled";
|
||||
|
||||
if (localeCount == 0)
|
||||
{
|
||||
Log(Debug::Warning) << "No language files for the preferred languages found in \"l10n/" << mName << "\"";
|
||||
}
|
||||
}
|
||||
|
||||
sol::object L10nManager::getContext(const std::string& contextName, const std::string& fallbackLocaleName)
|
||||
{
|
||||
auto it = mContexts.find(contextName);
|
||||
if (it != mContexts.end())
|
||||
return sol::make_object(mLua->sol(), it->second);
|
||||
icu::Locale fallbackLocale(fallbackLocaleName.c_str());
|
||||
Context ctx{contextName, std::make_shared<l10n::MessageBundles>(mPreferredLocales, fallbackLocale)};
|
||||
{
|
||||
Log msg(Debug::Verbose);
|
||||
msg << "Fallback locale: " << fallbackLocale.getName();
|
||||
}
|
||||
ctx.updateLang(this);
|
||||
mContexts.emplace(contextName, ctx);
|
||||
return sol::make_object(mLua->sol(), ctx);
|
||||
}
|
||||
|
||||
}
|
42
components/lua/l10n.hpp
Normal file
42
components/lua/l10n.hpp
Normal file
|
@ -0,0 +1,42 @@
|
|||
#ifndef COMPONENTS_LUA_I18N_H
|
||||
#define COMPONENTS_LUA_I18N_H
|
||||
|
||||
#include "luastate.hpp"
|
||||
|
||||
#include <components/l10n/messagebundles.hpp>
|
||||
|
||||
namespace LuaUtil
|
||||
{
|
||||
|
||||
class L10nManager
|
||||
{
|
||||
public:
|
||||
L10nManager(const VFS::Manager* vfs, LuaState* lua) : mVFS(vfs), mLua(lua) {}
|
||||
void init();
|
||||
|
||||
void setPreferredLocales(const std::vector<std::string>& locales);
|
||||
const std::vector<icu::Locale>& getPreferredLocales() const { return mPreferredLocales; }
|
||||
|
||||
sol::object getContext(const std::string& contextName, const std::string& fallbackLocale = "en");
|
||||
|
||||
private:
|
||||
struct Context
|
||||
{
|
||||
const std::string mName;
|
||||
// Must be a shared pointer so that sol::make_object copies the pointer, not the data structure.
|
||||
std::shared_ptr<l10n::MessageBundles> mMessageBundles;
|
||||
|
||||
void updateLang(L10nManager* manager);
|
||||
void readLangData(L10nManager* manager, const icu::Locale& lang);
|
||||
std::string translate(std::string_view key, const sol::object& data);
|
||||
};
|
||||
|
||||
const VFS::Manager* mVFS;
|
||||
LuaState* mLua;
|
||||
std::vector<icu::Locale> mPreferredLocales;
|
||||
std::map<std::string, Context> mContexts;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif // COMPONENTS_LUA_I18N_H
|
Loading…
Add table
Add a link
Reference in a new issue