1
0
mirror of https://gitlab.com/OpenMW/openmw.git synced 2025-03-20 10:20:50 +00:00

Merge branch 'lua_i18n' into 'master'

Lua i18n

Closes #6504

See merge request OpenMW/openmw!1520
This commit is contained in:
psi29a 2022-01-03 09:14:45 +00:00
commit b0e2820340
27 changed files with 1198 additions and 26 deletions

View File

@ -737,7 +737,7 @@ void OMW::Engine::prepareEngine (Settings::Manager & settings)
mViewer->addEventHandler(mScreenCaptureHandler);
mLuaManager = new MWLua::LuaManager(mVFS.get());
mLuaManager = new MWLua::LuaManager(mVFS.get(), (mResDir / "lua_libs").string());
mEnvironment.setLuaManager(mLuaManager);
// Create input and UI first to set up a bootstrapping environment for

View File

@ -7,6 +7,7 @@ namespace LuaUtil
{
class LuaState;
class UserdataSerializer;
class I18nManager;
}
namespace MWLua
@ -20,6 +21,7 @@ namespace MWLua
LuaManager* mLuaManager;
LuaUtil::LuaState* mLua;
LuaUtil::UserdataSerializer* mSerializer;
LuaUtil::I18nManager* mI18n;
WorldView* mWorldView;
LocalEventQueue* mLocalEventQueue;
GlobalEventQueue* mGlobalEventQueue;

View File

@ -1,6 +1,7 @@
#include "luabindings.hpp"
#include <components/lua/luastate.hpp>
#include <components/lua/i18n.hpp>
#include <components/queries/luabindings.hpp>
#include "../mwbase/environment.hpp"
@ -25,7 +26,7 @@ namespace MWLua
{
auto* lua = context.mLua;
sol::table api(lua->sol(), sol::create);
api["API_REVISION"] = 11;
api["API_REVISION"] = 12;
api["quit"] = [lua]()
{
Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback();
@ -64,6 +65,7 @@ namespace MWLua
{"CarriedLeft", MWWorld::InventoryStore::Slot_CarriedLeft},
{"Ammunition", MWWorld::InventoryStore::Slot_Ammunition}
}));
api["i18n"] = [i18n=context.mI18n](const std::string& context) { return i18n->getContext(context); };
return LuaUtil::makeReadOnly(api);
}

View File

@ -6,6 +6,8 @@
#include <components/esm/esmwriter.hpp>
#include <components/esm/luascripts.hpp>
#include <components/settings/settings.hpp>
#include <components/lua/utilpackage.hpp>
#include "../mwbase/windowmanager.hpp"
@ -20,9 +22,10 @@
namespace MWLua
{
LuaManager::LuaManager(const VFS::Manager* vfs) : mLua(vfs, &mConfiguration)
LuaManager::LuaManager(const VFS::Manager* vfs, const std::string& libsDir) : mLua(vfs, &mConfiguration), mI18n(vfs, &mLua)
{
Log(Debug::Info) << "Lua version: " << LuaUtil::getLuaVersion();
mLua.addInternalLibSearchPath(libsDir);
mGlobalSerializer = createUserdataSerializer(false, mWorldView.getObjectRegistry());
mLocalSerializer = createUserdataSerializer(true, mWorldView.getObjectRegistry());
@ -46,6 +49,7 @@ namespace MWLua
context.mIsGlobal = true;
context.mLuaManager = this;
context.mLua = &mLua;
context.mI18n = &mI18n;
context.mWorldView = &mWorldView;
context.mLocalEventQueue = &mLocalEvents;
context.mGlobalEventQueue = &mGlobalEvents;
@ -55,6 +59,11 @@ namespace MWLua
localContext.mIsGlobal = false;
localContext.mSerializer = mLocalSerializer.get();
mI18n.init();
std::vector<std::string> preferredLanguages;
Misc::StringUtils::split(Settings::Manager::getString("i18n preferred languages", "Lua"), preferredLanguages, ", ");
mI18n.setPreferredLanguages(preferredLanguages);
initObjectBindingsForGlobalScripts(context);
initCellBindingsForGlobalScripts(context);
initObjectBindingsForLocalScripts(localContext);

View File

@ -5,6 +5,7 @@
#include <set>
#include <components/lua/luastate.hpp>
#include <components/lua/i18n.hpp>
#include "../mwbase/luamanager.hpp"
@ -22,7 +23,7 @@ namespace MWLua
class LuaManager : public MWBase::LuaManager
{
public:
LuaManager(const VFS::Manager* vfs);
LuaManager(const VFS::Manager* vfs, const std::string& libsDir);
// Called by engine.cpp when the environment is fully initialized.
void init();
@ -91,6 +92,7 @@ namespace MWLua
bool mGlobalScriptsStarted = false;
LuaUtil::ScriptsConfiguration mConfiguration;
LuaUtil::LuaState mLua;
LuaUtil::I18nManager mI18n;
sol::table mNearbyPackage;
sol::table mUserInterfacePackage;
sol::table mCameraPackage;

View File

@ -23,6 +23,7 @@ if (GTEST_FOUND AND GMOCK_FOUND)
lua/test_serialization.cpp
lua/test_querypackage.cpp
lua/test_configuration.cpp
lua/test_i18n.cpp
lua/test_ui_content.cpp

View File

@ -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");
}
}

View File

@ -106,7 +106,7 @@ return {
}
EXPECT_EQ(LuaUtil::call(script["useCounter"]).get<int>(), 45);
EXPECT_ERROR(LuaUtil::call(script["incorrectRequire"]), "Resource 'counter.lua' not found");
EXPECT_ERROR(LuaUtil::call(script["incorrectRequire"]), "module not found: counter");
}
TEST_F(LuaStateTest, ReadOnly)
@ -161,7 +161,7 @@ return {
sol::table script2 = lua.runInNewSandbox("bbb/tests.lua", "", {{"test.api", api2}});
EXPECT_ERROR(LuaUtil::call(script1["sqr"], 3), "Resource 'sqrlib.lua' not found");
EXPECT_ERROR(LuaUtil::call(script1["sqr"], 3), "module not found: sqrlib");
EXPECT_EQ(LuaUtil::call(script2["sqr"], 3).get<int>(), 9);
EXPECT_EQ(LuaUtil::call(script1["apiName"]).get<std::string>(), "api1");

View File

@ -10,12 +10,6 @@ namespace
{
using namespace testing;
template <typename T>
T get(sol::state& lua, std::string luaCode)
{
return lua.safe_script("return " + luaCode).get<T>();
}
std::string getAsString(sol::state& lua, std::string luaCode)
{
return LuaUtil::toString(lua.safe_script("return " + luaCode));

View File

@ -2,6 +2,7 @@
#define LUA_TESTING_UTIL_H
#include <sstream>
#include <sol/sol.hpp>
#include <components/vfs/archive.hpp>
#include <components/vfs/manager.hpp>
@ -9,6 +10,12 @@
namespace
{
template <typename T>
T get(sol::state& lua, const std::string& luaCode)
{
return lua.safe_script("return " + luaCode).get<T>();
}
class TestFile : public VFS::File
{
public:

View File

@ -29,7 +29,7 @@ endif (GIT_CHECKOUT)
# source files
add_component_dir (lua
luastate scriptscontainer utilpackage serialization configuration
luastate scriptscontainer utilpackage serialization configuration i18n
)
add_component_dir (settings
@ -160,7 +160,7 @@ add_component_dir (fallback
add_component_dir (queries
query luabindings
)
add_component_dir (lua_ui
widget widgetlist element layers content
text textedit window

108
components/lua/i18n.cpp Normal file
View File

@ -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);
}
}

41
components/lua/i18n.hpp Normal file
View File

@ -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

View File

@ -4,17 +4,44 @@
#include <luajit.h>
#endif // NO_LUAJIT
#include <filesystem>
#include <components/debug/debuglog.hpp>
namespace LuaUtil
{
static std::string packageNameToPath(std::string_view packageName)
static std::string packageNameToVfsPath(std::string_view packageName, const VFS::Manager* vfs)
{
std::string res(packageName);
std::replace(res.begin(), res.end(), '.', '/');
res.append(".lua");
return res;
std::string path(packageName);
std::replace(path.begin(), path.end(), '.', '/');
std::string pathWithInit = path + "/init.lua";
path.append(".lua");
if (vfs->exists(path))
return path;
else if (vfs->exists(pathWithInit))
return pathWithInit;
else
throw std::runtime_error("module not found: " + std::string(packageName));
}
static std::string packageNameToPath(std::string_view packageName, const std::vector<std::string>& searchDirs)
{
std::string path(packageName);
std::replace(path.begin(), path.end(), '.', '/');
std::string pathWithInit = path + "/init.lua";
path.append(".lua");
for (const std::string& dir : searchDirs)
{
std::filesystem::path base(dir);
std::filesystem::path p1 = base / path;
if (std::filesystem::exists(p1))
return p1.string();
std::filesystem::path p2 = base / pathWithInit;
if (std::filesystem::exists(p2))
return p2.string();
}
throw std::runtime_error("module not found: " + std::string(packageName));
}
static const std::string safeFunctions[] = {
@ -28,7 +55,7 @@ namespace LuaUtil
sol::lib::string, sol::lib::table, sol::lib::debug);
mLua["math"]["randomseed"](static_cast<unsigned>(std::time(nullptr)));
mLua["math"]["randomseed"] = sol::nil;
mLua["math"]["randomseed"] = []{};
mLua["writeToLog"] = [](std::string_view s) { Log(Debug::Level::Info) << s; };
mLua.script(R"(printToLog = function(name, ...)
@ -105,7 +132,7 @@ namespace LuaUtil
const std::string& path, const std::string& namePrefix,
const std::map<std::string, sol::object>& packages, const sol::object& hiddenData)
{
sol::protected_function script = loadScript(path);
sol::protected_function script = loadScriptAndCache(path);
sol::environment env(mLua, sol::create, mSandboxEnv);
std::string envName = namePrefix + "[" + path + "]:";
@ -122,9 +149,9 @@ namespace LuaUtil
sol::object package = packages[packageName];
if (package == sol::nil)
{
sol::protected_function packageLoader = loadScript(packageNameToPath(packageName));
sol::protected_function packageLoader = loadScriptAndCache(packageNameToVfsPath(packageName, mVFS));
sol::set_environment(env, packageLoader);
package = throwIfError(packageLoader());
package = call(packageLoader, packageName);
if (!package.is<sol::table>())
throw std::runtime_error("Lua package must return a table.");
packages[packageName] = package;
@ -138,6 +165,24 @@ namespace LuaUtil
return call(script);
}
sol::environment LuaState::newInternalLibEnvironment()
{
sol::environment env(mLua, sol::create, mSandboxEnv);
sol::table loaded(mLua, sol::create);
for (const std::string& s : safePackages)
loaded[s] = mSandboxEnv[s];
env["require"] = [this, loaded, env](const std::string& module) mutable
{
if (loaded[module] != sol::nil)
return loaded[module];
sol::protected_function initializer = loadInternalLib(module);
sol::set_environment(env, initializer);
loaded[module] = call(initializer, module);
return loaded[module];
};
return env;
}
sol::protected_function_result LuaState::throwIfError(sol::protected_function_result&& res)
{
if (!res.valid() && static_cast<int>(res.get_type()) == LUA_TSTRING)
@ -146,17 +191,31 @@ namespace LuaUtil
return std::move(res);
}
sol::function LuaState::loadScript(const std::string& path)
sol::function LuaState::loadScriptAndCache(const std::string& path)
{
auto iter = mCompiledScripts.find(path);
if (iter != mCompiledScripts.end())
return mLua.load(iter->second.as_string_view(), path, sol::load_mode::binary);
sol::function res = loadFromVFS(path);
mCompiledScripts[path] = res.dump();
return res;
}
sol::function LuaState::loadFromVFS(const std::string& path)
{
std::string fileContent(std::istreambuf_iterator<char>(*mVFS->get(path)), {});
sol::load_result res = mLua.load(fileContent, path, sol::load_mode::text);
if (!res.valid())
throw std::runtime_error("Lua error: " + res.get<std::string>());
mCompiledScripts[path] = res.get<sol::function>().dump();
return res;
}
sol::function LuaState::loadInternalLib(std::string_view libName)
{
std::string path = packageNameToPath(libName, mLibSearchPaths);
sol::load_result res = mLua.load_file(path, sol::load_mode::text);
if (!res.valid())
throw std::runtime_error("Lua error: " + res.get<std::string>());
return res;
}

View File

@ -76,12 +76,18 @@ namespace LuaUtil
const ScriptsConfiguration& getConfiguration() const { return *mConf; }
// Load internal Lua library. All libraries are loaded in one sandbox and shouldn't be exposed to scripts directly.
void addInternalLibSearchPath(const std::string& path) { mLibSearchPaths.push_back(path); }
sol::function loadInternalLib(std::string_view libName);
sol::function loadFromVFS(const std::string& path);
sol::environment newInternalLibEnvironment();
private:
static sol::protected_function_result throwIfError(sol::protected_function_result&&);
template <typename... Args>
friend sol::protected_function_result call(const sol::protected_function& fn, Args&&... args);
sol::function loadScript(const std::string& path);
sol::function loadScriptAndCache(const std::string& path);
sol::state mLua;
const ScriptsConfiguration* mConf;
@ -89,6 +95,7 @@ namespace LuaUtil
std::map<std::string, sol::bytecode> mCompiledScripts;
std::map<std::string, sol::object> mCommonPackages;
const VFS::Manager* mVFS;
std::vector<std::string> mLibSearchPaths;
};
// Should be used for every call of every Lua function.

View File

@ -27,3 +27,14 @@ Values >1 are not yet supported.
This setting can only be configured by editing the settings configuration file.
i18n preferred languages
------------------------
:Type: string
:Default: en
List of the preferred languages separated by comma.
For example "de,en" means German as the first prority and English as a fallback.
This setting can only be configured by editing the settings configuration file.

17
extern/i18n.lua/CMakeLists.txt vendored Normal file
View File

@ -0,0 +1,17 @@
if (NOT DEFINED OPENMW_RESOURCES_ROOT)
message( FATAL_ERROR "OPENMW_RESOURCES_ROOT is not set" )
endif()
# Copy resource files into the build directory
set(SDIR ${CMAKE_CURRENT_SOURCE_DIR})
set(DDIRRELATIVE resources/lua_libs/i18n)
set(I18N_LUA_FILES
i18n/init.lua
i18n/interpolate.lua
i18n/plural.lua
i18n/variants.lua
i18n/version.lua
)
copy_all_resource_files(${CMAKE_CURRENT_SOURCE_DIR} ${OPENMW_RESOURCES_ROOT} ${DDIRRELATIVE} "${I18N_LUA_FILES}")

22
extern/i18n.lua/LICENSE vendored Normal file
View File

@ -0,0 +1,22 @@
MIT License Terms
=================
Copyright (c) 2012 Enrique García Cota.
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.

164
extern/i18n.lua/README.md vendored Normal file
View File

@ -0,0 +1,164 @@
i18n.lua
========
[![Build Status](https://travis-ci.org/kikito/i18n.lua.png?branch=master)](https://travis-ci.org/kikito/i18n.lua)
A very complete i18n lib for Lua
Description
===========
``` lua
i18n = require 'i18n'
-- loading stuff
i18n.set('en.welcome', 'welcome to this program')
i18n.load({
en = {
good_bye = "good-bye!",
age_msg = "your age is %{age}.",
phone_msg = {
one = "you have one new message.",
other = "you have %{count} new messages."
}
}
})
i18n.loadFile('path/to/your/project/i18n/de.lua') -- load German language file
i18n.loadFile('path/to/your/project/i18n/fr.lua') -- load French language file
… -- section 'using language files' below describes structure of files
-- setting the translation context
i18n.setLocale('en') -- English is the default locale anyway
-- getting translations
i18n.translate('welcome') -- Welcome to this program
i18n('welcome') -- Welcome to this program
i18n('age_msg', {age = 18}) -- Your age is 18.
i18n('phone_msg', {count = 1}) -- You have one new message.
i18n('phone_msg', {count = 2}) -- You have 2 new messages.
i18n('good_bye') -- Good-bye!
```
Interpolation
=============
You can interpolate variables in 3 different ways:
``` lua
-- the most usual one
i18n.set('variables', 'Interpolating variables: %{name} %{age}')
i18n('variables', {name='john', 'age'=10}) -- Interpolating variables: john 10
i18n.set('lua', 'Traditional Lua way: %d %s')
i18n('lua', {1, 'message'}) -- Traditional Lua way: 1 message
i18n.set('combined', 'Combined: %<name>.q %<age>.d %<age>.o')
i18n('combined', {name='john', 'age'=10}) -- Combined: john 10 12k
```
Pluralization
=============
This lib implements the [unicode.org plural rules](http://cldr.unicode.org/index/cldr-spec/plural-rules). Just set the locale you want to use and it will deduce the appropiate pluralization rules:
``` lua
i18n = require 'i18n'
i18n.load({
en = {
msg = {
one = "one message",
other = "%{count} messages"
}
},
ru = {
msg = {
one = "1 сообщение",
few = "%{count} сообщения",
many = "%{count} сообщений",
other = "%{count} сообщения"
}
}
})
i18n('msg', {count = 1}) -- one message
i18n.setLocale('ru')
i18n('msg', {count = 5}) -- 5 сообщений
```
The appropiate rule is chosen by finding the 'root' of the locale used: for example if the current locale is 'fr-CA', the 'fr' rules will be applied.
If the provided functions are not enough (i.e. invented languages) it's possible to specify a custom pluralization function in the second parameter of setLocale. This function must return 'one', 'few', 'other', etc given a number.
Fallbacks
=========
When a value is not found, the lib has several fallback mechanisms:
* First, it will look in the current locale's parents. For example, if the locale was set to 'en-US' and the key 'msg' was not found there, it will be looked over in 'en'.
* Second, if the value is not found in the locale ancestry, a 'fallback locale' (by default: 'en') can be used. If the fallback locale has any parents, they will be looked over too.
* Third, if all the locales have failed, but there is a param called 'default' on the provided data, it will be used.
* Otherwise the translation will return nil.
The parents of a locale are found by splitting the locale by its hyphens. Other separation characters (spaces, underscores, etc) are not supported.
Using language files
====================
It might be a good idea to store each translation in a different file. This is supported via the 'i18n.loadFile' directive:
``` lua
i18n.loadFile('path/to/your/project/i18n/de.lua') -- German translation
i18n.loadFile('path/to/your/project/i18n/en.lua') -- English translation
i18n.loadFile('path/to/your/project/i18n/fr.lua') -- French translation
```
The German language file 'de.lua' should read:
``` lua
return {
de = {
good_bye = "Auf Wiedersehen!",
age_msg = "Ihr Alter beträgt %{age}.",
phone_msg = {
one = "Sie haben eine neue Nachricht.",
other = "Sie haben %{count} neue Nachrichten."
}
}
}
```
If desired, you can also store all translations in one single file (eg. 'translations.lua'):
``` lua
return {
de = {
good_bye = "Auf Wiedersehen!",
age_msg = "Ihr Alter beträgt %{age}.",
phone_msg = {
one = "Sie haben eine neue Nachricht.",
other = "Sie haben %{count} neue Nachrichten."
}
},
fr = {
good_bye = "Au revoir !",
age_msg = "Vous avez %{age} ans.",
phone_msg = {
one = "Vous avez une noveau message.",
other = "Vous avez %{count} noveaux messages."
}
},
}
```
Specs
=====
This project uses [busted](https://github.com/Olivine-Labs/busted) for its specs. If you want to run the specs, you will have to install it first. Then just execute the following from the root inspect folder:
busted

188
extern/i18n.lua/i18n/init.lua vendored Normal file
View File

@ -0,0 +1,188 @@
local i18n = {}
local store
local locale
local pluralizeFunction
local defaultLocale = 'en'
local fallbackLocale = defaultLocale
local currentFilePath = (...):gsub("%.init$","")
local plural = require(currentFilePath .. '.plural')
local interpolate = require(currentFilePath .. '.interpolate')
local variants = require(currentFilePath .. '.variants')
local version = require(currentFilePath .. '.version')
i18n.plural, i18n.interpolate, i18n.variants, i18n.version, i18n._VERSION =
plural, interpolate, variants, version, version
-- private stuff
local function dotSplit(str)
local fields, length = {},0
str:gsub("[^%.]+", function(c)
length = length + 1
fields[length] = c
end)
return fields, length
end
local function isPluralTable(t)
return type(t) == 'table' and type(t.other) == 'string'
end
local function isPresent(str)
return type(str) == 'string' and #str > 0
end
local function assertPresent(functionName, paramName, value)
if isPresent(value) then return end
local msg = "i18n.%s requires a non-empty string on its %s. Got %s (a %s value)."
error(msg:format(functionName, paramName, tostring(value), type(value)))
end
local function assertPresentOrPlural(functionName, paramName, value)
if isPresent(value) or isPluralTable(value) then return end
local msg = "i18n.%s requires a non-empty string or plural-form table on its %s. Got %s (a %s value)."
error(msg:format(functionName, paramName, tostring(value), type(value)))
end
local function assertPresentOrTable(functionName, paramName, value)
if isPresent(value) or type(value) == 'table' then return end
local msg = "i18n.%s requires a non-empty string or table on its %s. Got %s (a %s value)."
error(msg:format(functionName, paramName, tostring(value), type(value)))
end
local function assertFunctionOrNil(functionName, paramName, value)
if value == nil or type(value) == 'function' then return end
local msg = "i18n.%s requires a function (or nil) on param %s. Got %s (a %s value)."
error(msg:format(functionName, paramName, tostring(value), type(value)))
end
local function defaultPluralizeFunction(count)
return plural.get(variants.root(i18n.getLocale()), count)
end
local function pluralize(t, data)
assertPresentOrPlural('interpolatePluralTable', 't', t)
data = data or {}
local count = data.count or 1
local plural_form = pluralizeFunction(count)
return t[plural_form]
end
local function treatNode(node, data)
if type(node) == 'string' then
return interpolate(node, data)
elseif isPluralTable(node) then
return interpolate(pluralize(node, data), data)
end
return node
end
local function recursiveLoad(currentContext, data)
local composedKey
for k,v in pairs(data) do
composedKey = (currentContext and (currentContext .. '.') or "") .. tostring(k)
assertPresent('load', composedKey, k)
assertPresentOrTable('load', composedKey, v)
if type(v) == 'string' then
i18n.set(composedKey, v)
else
recursiveLoad(composedKey, v)
end
end
end
local function localizedTranslate(key, loc, data)
local path, length = dotSplit(loc .. "." .. key)
local node = store
for i=1, length do
node = node[path[i]]
if not node then return nil end
end
return treatNode(node, data)
end
-- public interface
function i18n.set(key, value)
assertPresent('set', 'key', key)
assertPresentOrPlural('set', 'value', value)
local path, length = dotSplit(key)
local node = store
for i=1, length-1 do
key = path[i]
node[key] = node[key] or {}
node = node[key]
end
local lastKey = path[length]
node[lastKey] = value
end
function i18n.translate(key, data)
assertPresent('translate', 'key', key)
data = data or {}
local usedLocale = data.locale or locale
local fallbacks = variants.fallbacks(usedLocale, fallbackLocale)
for i=1, #fallbacks do
local value = localizedTranslate(key, fallbacks[i], data)
if value then return value end
end
return data.default
end
function i18n.setLocale(newLocale, newPluralizeFunction)
assertPresent('setLocale', 'newLocale', newLocale)
assertFunctionOrNil('setLocale', 'newPluralizeFunction', newPluralizeFunction)
locale = newLocale
pluralizeFunction = newPluralizeFunction or defaultPluralizeFunction
end
function i18n.setFallbackLocale(newFallbackLocale)
assertPresent('setFallbackLocale', 'newFallbackLocale', newFallbackLocale)
fallbackLocale = newFallbackLocale
end
function i18n.getFallbackLocale()
return fallbackLocale
end
function i18n.getLocale()
return locale
end
function i18n.reset()
store = {}
plural.reset()
i18n.setLocale(defaultLocale)
i18n.setFallbackLocale(defaultLocale)
end
function i18n.load(data)
recursiveLoad(nil, data)
end
function i18n.loadFile(path)
local chunk = assert(loadfile(path))
local data = chunk()
i18n.load(data)
end
setmetatable(i18n, {__call = function(_, ...) return i18n.translate(...) end})
i18n.reset()
return i18n

60
extern/i18n.lua/i18n/interpolate.lua vendored Normal file
View File

@ -0,0 +1,60 @@
local unpack = unpack or table.unpack -- lua 5.2 compat
local FORMAT_CHARS = { c=1, d=1, E=1, e=1, f=1, g=1, G=1, i=1, o=1, u=1, X=1, x=1, s=1, q=1, ['%']=1 }
-- matches a string of type %{age}
local function interpolateValue(string, variables)
return string:gsub("(.?)%%{%s*(.-)%s*}",
function (previous, key)
if previous == "%" then
return
else
return previous .. tostring(variables[key])
end
end)
end
-- matches a string of type %<age>.d
local function interpolateField(string, variables)
return string:gsub("(.?)%%<%s*(.-)%s*>%.([cdEefgGiouXxsq])",
function (previous, key, format)
if previous == "%" then
return
else
return previous .. string.format("%" .. format, variables[key] or "nil")
end
end)
end
local function escapePercentages(string)
return string:gsub("(%%)(.?)", function(_, char)
if FORMAT_CHARS[char] then
return "%" .. char
else
return "%%" .. char
end
end)
end
local function unescapePercentages(string)
return string:gsub("(%%%%)(.?)", function(_, char)
if FORMAT_CHARS[char] then
return "%" .. char
else
return "%%" .. char
end
end)
end
local function interpolate(pattern, variables)
variables = variables or {}
local result = pattern
result = interpolateValue(result, variables)
result = interpolateField(result, variables)
result = escapePercentages(result)
result = string.format(result, unpack(variables))
result = unescapePercentages(result)
return result
end
return interpolate

280
extern/i18n.lua/i18n/plural.lua vendored Normal file
View File

@ -0,0 +1,280 @@
local plural = {}
local defaultFunction = nil
-- helper functions
local function assertPresentString(functionName, paramName, value)
if type(value) ~= 'string' or #value == 0 then
local msg = "Expected param %s of function %s to be a string, but got %s (a value of type %s) instead"
error(msg:format(paramName, functionName, tostring(value), type(value)))
end
end
local function assertNumber(functionName, paramName, value)
if type(value) ~= 'number' then
local msg = "Expected param %s of function %s to be a number, but got %s (a value of type %s) instead"
error(msg:format(paramName, functionName, tostring(value), type(value)))
end
end
-- transforms "foo bar baz" into {'foo','bar','baz'}
local function words(str)
local result, length = {}, 0
str:gsub("%S+", function(word)
length = length + 1
result[length] = word
end)
return result
end
local function isInteger(n)
return n == math.floor(n)
end
local function between(value, min, max)
return value >= min and value <= max
end
local function inside(v, list)
for i=1, #list do
if v == list[i] then return true end
end
return false
end
-- pluralization functions
local pluralization = {}
local f1 = function(n)
return n == 1 and "one" or "other"
end
pluralization[f1] = words([[
af asa bem bez bg bn brx ca cgg chr da de dv ee el
en eo es et eu fi fo fur fy gl gsw gu ha haw he is
it jmc kaj kcg kk kl ksb ku lb lg mas ml mn mr nah
nb nd ne nl nn no nr ny nyn om or pa pap ps pt rm
rof rwk saq seh sn so sq ss ssy st sv sw syr ta te
teo tig tk tn ts ur ve vun wae xh xog zu
]])
local f2 = function(n)
return (n == 0 or n == 1) and "one" or "other"
end
pluralization[f2] = words("ak am bh fil guw hi ln mg nso ti tl wa")
local f3 = function(n)
if not isInteger(n) then return 'other' end
return (n == 0 and "zero") or
(n == 1 and "one") or
(n == 2 and "two") or
(between(n % 100, 3, 10) and "few") or
(between(n % 100, 11, 99) and "many") or
"other"
end
pluralization[f3] = {'ar'}
local f4 = function()
return "other"
end
pluralization[f4] = words([[
az bm bo dz fa hu id ig ii ja jv ka kde kea km kn
ko lo ms my root sah ses sg th to tr vi wo yo zh
]])
local f5 = function(n)
if not isInteger(n) then return 'other' end
local n_10, n_100 = n % 10, n % 100
return (n_10 == 1 and n_100 ~= 11 and 'one') or
(between(n_10, 2, 4) and not between(n_100, 12, 14) and 'few') or
((n_10 == 0 or between(n_10, 5, 9) or between(n_100, 11, 14)) and 'many') or
'other'
end
pluralization[f5] = words('be bs hr ru sh sr uk')
local f6 = function(n)
if not isInteger(n) then return 'other' end
local n_10, n_100 = n % 10, n % 100
return (n_10 == 1 and not inside(n_100, {11,71,91}) and 'one') or
(n_10 == 2 and not inside(n_100, {12,72,92}) and 'two') or
(inside(n_10, {3,4,9}) and
not between(n_100, 10, 19) and
not between(n_100, 70, 79) and
not between(n_100, 90, 99)
and 'few') or
(n ~= 0 and n % 1000000 == 0 and 'many') or
'other'
end
pluralization[f6] = {'br'}
local f7 = function(n)
return (n == 1 and 'one') or
((n == 2 or n == 3 or n == 4) and 'few') or
'other'
end
pluralization[f7] = {'cz', 'sk'}
local f8 = function(n)
return (n == 0 and 'zero') or
(n == 1 and 'one') or
(n == 2 and 'two') or
(n == 3 and 'few') or
(n == 6 and 'many') or
'other'
end
pluralization[f8] = {'cy'}
local f9 = function(n)
return (n >= 0 and n < 2 and 'one') or
'other'
end
pluralization[f9] = {'ff', 'fr', 'kab'}
local f10 = function(n)
return (n == 1 and 'one') or
(n == 2 and 'two') or
((n == 3 or n == 4 or n == 5 or n == 6) and 'few') or
((n == 7 or n == 8 or n == 9 or n == 10) and 'many') or
'other'
end
pluralization[f10] = {'ga'}
local f11 = function(n)
return ((n == 1 or n == 11) and 'one') or
((n == 2 or n == 12) and 'two') or
(isInteger(n) and (between(n, 3, 10) or between(n, 13, 19)) and 'few') or
'other'
end
pluralization[f11] = {'gd'}
local f12 = function(n)
local n_10 = n % 10
return ((n_10 == 1 or n_10 == 2 or n % 20 == 0) and 'one') or
'other'
end
pluralization[f12] = {'gv'}
local f13 = function(n)
return (n == 1 and 'one') or
(n == 2 and 'two') or
'other'
end
pluralization[f13] = words('iu kw naq se sma smi smj smn sms')
local f14 = function(n)
return (n == 0 and 'zero') or
(n == 1 and 'one') or
'other'
end
pluralization[f14] = {'ksh'}
local f15 = function(n)
return (n == 0 and 'zero') or
(n > 0 and n < 2 and 'one') or
'other'
end
pluralization[f15] = {'lag'}
local f16 = function(n)
if not isInteger(n) then return 'other' end
if between(n % 100, 11, 19) then return 'other' end
local n_10 = n % 10
return (n_10 == 1 and 'one') or
(between(n_10, 2, 9) and 'few') or
'other'
end
pluralization[f16] = {'lt'}
local f17 = function(n)
return (n == 0 and 'zero') or
((n % 10 == 1 and n % 100 ~= 11) and 'one') or
'other'
end
pluralization[f17] = {'lv'}
local f18 = function(n)
return((n % 10 == 1 and n ~= 11) and 'one') or
'other'
end
pluralization[f18] = {'mk'}
local f19 = function(n)
return (n == 1 and 'one') or
((n == 0 or
(n ~= 1 and isInteger(n) and between(n % 100, 1, 19)))
and 'few') or
'other'
end
pluralization[f19] = {'mo', 'ro'}
local f20 = function(n)
if n == 1 then return 'one' end
if not isInteger(n) then return 'other' end
local n_100 = n % 100
return ((n == 0 or between(n_100, 2, 10)) and 'few') or
(between(n_100, 11, 19) and 'many') or
'other'
end
pluralization[f20] = {'mt'}
local f21 = function(n)
if n == 1 then return 'one' end
if not isInteger(n) then return 'other' end
local n_10, n_100 = n % 10, n % 100
return ((between(n_10, 2, 4) and not between(n_100, 12, 14)) and 'few') or
((n_10 == 0 or n_10 == 1 or between(n_10, 5, 9) or between(n_100, 12, 14)) and 'many') or
'other'
end
pluralization[f21] = {'pl'}
local f22 = function(n)
return (n == 0 or n == 1) and 'one' or
'other'
end
pluralization[f22] = {'shi'}
local f23 = function(n)
local n_100 = n % 100
return (n_100 == 1 and 'one') or
(n_100 == 2 and 'two') or
((n_100 == 3 or n_100 == 4) and 'few') or
'other'
end
pluralization[f23] = {'sl'}
local f24 = function(n)
return (isInteger(n) and (n == 0 or n == 1 or between(n, 11, 99)) and 'one')
or 'other'
end
pluralization[f24] = {'tzm'}
local pluralizationFunctions = {}
for f,locales in pairs(pluralization) do
for _,locale in ipairs(locales) do
pluralizationFunctions[locale] = f
end
end
-- public interface
function plural.get(locale, n)
assertPresentString('i18n.plural.get', 'locale', locale)
assertNumber('i18n.plural.get', 'n', n)
local f = pluralizationFunctions[locale] or defaultFunction
return f(math.abs(n))
end
function plural.setDefaultFunction(f)
defaultFunction = f
end
function plural.reset()
defaultFunction = pluralizationFunctions['en']
end
plural.reset()
return plural

49
extern/i18n.lua/i18n/variants.lua vendored Normal file
View File

@ -0,0 +1,49 @@
local variants = {}
local function reverse(arr, length)
local result = {}
for i=1, length do result[i] = arr[length-i+1] end
return result, length
end
local function concat(arr1, len1, arr2, len2)
for i = 1, len2 do
arr1[len1 + i] = arr2[i]
end
return arr1, len1 + len2
end
function variants.ancestry(locale)
local result, length, accum = {},0,nil
locale:gsub("[^%-]+", function(c)
length = length + 1
accum = accum and (accum .. '-' .. c) or c
result[length] = accum
end)
return reverse(result, length)
end
function variants.isParent(parent, child)
return not not child:match("^".. parent .. "%-")
end
function variants.root(locale)
return locale:match("[^%-]+")
end
function variants.fallbacks(locale, fallbackLocale)
if locale == fallbackLocale or
variants.isParent(fallbackLocale, locale) then
return variants.ancestry(locale)
end
if variants.isParent(locale, fallbackLocale) then
return variants.ancestry(fallbackLocale)
end
local ancestry1, length1 = variants.ancestry(locale)
local ancestry2, length2 = variants.ancestry(fallbackLocale)
return concat(ancestry1, length1, ancestry2, length2)
end
return variants

1
extern/i18n.lua/i18n/version.lua vendored Normal file
View File

@ -0,0 +1 @@
return '0.9.2'

View File

@ -3,3 +3,4 @@ add_subdirectory(shaders)
add_subdirectory(vfs)
add_subdirectory(builtin_scripts)
add_subdirectory(lua_api)
add_subdirectory(../extern/i18n.lua ${CMAKE_CURRENT_BINARY_DIR}/files)

View File

@ -37,6 +37,39 @@
-- @function [parent=#core] isWorldPaused
-- @return #boolean
-------------------------------------------------------------------------------
-- Return i18n formatting function for the given context.
-- It is based on `i18n.lua` library.
-- Language files should be stored in VFS as `i18n/<ContextName>/<Lang>.lua`.
-- See https://github.com/kikito/i18n.lua for format details.
-- @function [parent=#core] i18n
-- @param #string context I18n context; recommended to use the name of the mod.
-- @return #function
-- @usage
-- -- DataFiles/i18n/MyMod/en.lua
-- return {
-- good_morning = 'Good morning.',
-- you_have_arrows = {
-- one = 'You have one arrow.',
-- other = 'You have %{count} arrows.',
-- },
-- }
-- @usage
-- -- DataFiles/i18n/MyMod/de.lua
-- return {
-- good_morning = "Guten Morgen.",
-- you_have_arrows = {
-- one = "Du hast ein Pfeil.",
-- other = "Du hast %{count} Pfeile.",
-- },
-- ["Hello %{name}!"] = "Hallo %{name}!",
-- }
-- @usage
-- local myMsg = core.i18n('MyMod')
-- print( myMsg('good_morning') )
-- print( myMsg('you_have_arrows', {count=5}) )
-- print( myMsg('Hello %{name}!', {name='World'}) )
-------------------------------------------------------------------------------
-- @type OBJECT_TYPE

View File

@ -1123,3 +1123,7 @@ lua debug = false
# If zero, Lua scripts are processed in the main thread.
lua num threads = 1
# List of the preferred languages separated by comma.
# For example "de,en" means German as the first prority and English as a fallback.
i18n preferred languages = en