mirror of https://github.com/OpenMW/openmw.git
Use a separate instance of Lua i18n for every context
parent
f91a5499d3
commit
0f246e7365
@ -0,0 +1,110 @@
|
|||||||
|
#include "gmock/gmock.h"
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <components/files/fixedpath.hpp>
|
||||||
|
|
||||||
|
#include <components/lua/luastate.hpp>
|
||||||
|
#include <components/lua/i18n.hpp>
|
||||||
|
|
||||||
|
#include "testing_util.hpp"
|
||||||
|
|
||||||
|
namespace
|
||||||
|
{
|
||||||
|
using namespace testing;
|
||||||
|
|
||||||
|
TestFile invalidScript("not a script");
|
||||||
|
TestFile incorrectScript("return { incorrectSection = {}, engineHandlers = { incorrectHandler = function() end } }");
|
||||||
|
TestFile emptyScript("");
|
||||||
|
|
||||||
|
TestFile test1En(R"X(
|
||||||
|
return {
|
||||||
|
good_morning = "Good morning.",
|
||||||
|
you_have_arrows = {
|
||||||
|
one = "You have one arrow.",
|
||||||
|
other = "You have %{count} arrows.",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)X");
|
||||||
|
|
||||||
|
TestFile test1De(R"X(
|
||||||
|
return {
|
||||||
|
good_morning = "Guten Morgen.",
|
||||||
|
you_have_arrows = {
|
||||||
|
one = "Du hast ein Pfeil.",
|
||||||
|
other = "Du hast %{count} Pfeile.",
|
||||||
|
},
|
||||||
|
["Hello %{name}!"] = "Hallo %{name}!",
|
||||||
|
}
|
||||||
|
)X");
|
||||||
|
|
||||||
|
TestFile test2En(R"X(
|
||||||
|
return {
|
||||||
|
good_morning = "Morning!",
|
||||||
|
you_have_arrows = "Arrows count: %{count}",
|
||||||
|
}
|
||||||
|
)X");
|
||||||
|
|
||||||
|
TestFile invalidTest2De(R"X(
|
||||||
|
require('math')
|
||||||
|
return {}
|
||||||
|
)X");
|
||||||
|
|
||||||
|
struct LuaI18nTest : Test
|
||||||
|
{
|
||||||
|
std::unique_ptr<VFS::Manager> mVFS = createTestVFS({
|
||||||
|
{"i18n/Test1/en.lua", &test1En},
|
||||||
|
{"i18n/Test1/de.lua", &test1De},
|
||||||
|
{"i18n/Test2/en.lua", &test2En},
|
||||||
|
{"i18n/Test2/de.lua", &invalidTest2De},
|
||||||
|
});
|
||||||
|
|
||||||
|
LuaUtil::ScriptsConfiguration mCfg;
|
||||||
|
std::string mLibsPath = (Files::TargetPathType("openmw_test_suite").getLocalPath() / "resources" / "lua_libs").string();
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(LuaI18nTest, I18n)
|
||||||
|
{
|
||||||
|
internal::CaptureStdout();
|
||||||
|
LuaUtil::LuaState lua{mVFS.get(), &mCfg};
|
||||||
|
sol::state& l = lua.sol();
|
||||||
|
LuaUtil::I18nManager i18n(mVFS.get(), &lua);
|
||||||
|
lua.addInternalLibSearchPath(mLibsPath);
|
||||||
|
i18n.init();
|
||||||
|
i18n.setPreferredLanguages({"de", "en"});
|
||||||
|
EXPECT_THAT(internal::GetCapturedStdout(), "I18n preferred languages: de en\n");
|
||||||
|
|
||||||
|
internal::CaptureStdout();
|
||||||
|
l["t1"] = i18n.getContext("Test1");
|
||||||
|
EXPECT_THAT(internal::GetCapturedStdout(), "Language file \"i18n/Test1/de.lua\" is enabled\n");
|
||||||
|
|
||||||
|
internal::CaptureStdout();
|
||||||
|
l["t2"] = i18n.getContext("Test2");
|
||||||
|
{
|
||||||
|
std::string output = internal::GetCapturedStdout();
|
||||||
|
EXPECT_THAT(output, HasSubstr("Can not load i18n/Test2/de.lua"));
|
||||||
|
EXPECT_THAT(output, HasSubstr("Language file \"i18n/Test2/en.lua\" is enabled"));
|
||||||
|
}
|
||||||
|
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t1('good_morning')"), "Guten Morgen.");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t1('you_have_arrows', {count=1})"), "Du hast ein Pfeil.");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t1('you_have_arrows', {count=5})"), "Du hast 5 Pfeile.");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t1('Hello %{name}!', {name='World'})"), "Hallo World!");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t2('good_morning')"), "Morning!");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t2('you_have_arrows', {count=3})"), "Arrows count: 3");
|
||||||
|
|
||||||
|
internal::CaptureStdout();
|
||||||
|
i18n.setPreferredLanguages({"en", "de"});
|
||||||
|
EXPECT_THAT(internal::GetCapturedStdout(),
|
||||||
|
"I18n preferred languages: en de\n"
|
||||||
|
"Language file \"i18n/Test1/en.lua\" is enabled\n"
|
||||||
|
"Language file \"i18n/Test2/en.lua\" is enabled\n");
|
||||||
|
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t1('good_morning')"), "Good morning.");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t1('you_have_arrows', {count=1})"), "You have one arrow.");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t1('you_have_arrows', {count=5})"), "You have 5 arrows.");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t1('Hello %{name}!', {name='World'})"), "Hello World!");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t2('good_morning')"), "Morning!");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t2('you_have_arrows', {count=3})"), "Arrows count: 3");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,108 @@
|
|||||||
|
#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");
|
||||||
|
Context ctx{contextName, mLua->newTable(), call(mI18nLoader, "i18n.init")};
|
||||||
|
ctx.updateLang(this);
|
||||||
|
mContexts.emplace(contextName, ctx);
|
||||||
|
return sol::make_object(mLua->sol(), ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
#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
|
Loading…
Reference in New Issue