Merge branch 'l10n' into 'master'

Separate l10n manager from lua

See merge request OpenMW/openmw!2451
focus_on_focal
psi29a 2 years ago
commit e16c451d08

@ -34,6 +34,8 @@
#include <components/version/version.hpp>
#include <components/l10n/manager.hpp>
#include <components/misc/frameratelimiter.hpp>
#include <components/sceneutil/color.hpp>
@ -391,6 +393,7 @@ OMW::Engine::~Engine()
mStateManager = nullptr;
mLuaWorker = nullptr;
mLuaManager = nullptr;
mL10nManager = nullptr;
mScriptContext = nullptr;
@ -682,6 +685,10 @@ void OMW::Engine::prepareEngine()
mViewer->addEventHandler(mScreenCaptureHandler);
mL10nManager = std::make_unique<l10n::Manager>(mVFS.get());
mL10nManager->setPreferredLocales(Settings::Manager::getStringArray("preferred locales", "General"));
mEnvironment.setL10nManager(*mL10nManager);
mLuaManager = std::make_unique<MWLua::LuaManager>(mVFS.get(), mResDir / "lua_libs");
mEnvironment.setLuaManager(*mLuaManager);
@ -767,7 +774,6 @@ void OMW::Engine::prepareEngine()
mEnvironment.setWorld(*mWorld);
mWindowManager->setStore(mWorld->getStore());
mLuaManager->initL10n();
mWindowManager->initUI();
// Load translation data

@ -111,6 +111,11 @@ namespace MWDialogue
class Journal;
}
namespace l10n
{
class Manager;
}
struct SDL_Window;
namespace OMW
@ -134,6 +139,7 @@ namespace OMW
std::unique_ptr<MWState::StateManager> mStateManager;
std::unique_ptr<MWLua::LuaManager> mLuaManager;
std::unique_ptr<MWLua::Worker> mLuaWorker;
std::unique_ptr<l10n::Manager> mL10nManager;
MWBase::Environment mEnvironment;
ToUTF8::FromType mEncoding;
std::unique_ptr<ToUTF8::Utf8Encoder> mEncoder;

@ -10,6 +10,11 @@ namespace Resource
class ResourceSystem;
}
namespace l10n
{
class Manager;
}
namespace MWBase
{
class World;
@ -42,6 +47,7 @@ namespace MWBase
StateManager* mStateManager = nullptr;
LuaManager* mLuaManager = nullptr;
Resource::ResourceSystem* mResourceSystem = nullptr;
l10n::Manager* mL10nManager = nullptr;
float mFrameRateLimit = 0;
float mFrameDuration = 0;
@ -76,6 +82,8 @@ namespace MWBase
void setResourceSystem(Resource::ResourceSystem& value) { mResourceSystem = &value; }
void setL10nManager(l10n::Manager& value) { mL10nManager = &value; }
Misc::NotNullPtr<World> getWorld() const { return mWorld; }
Misc::NotNullPtr<SoundManager> getSoundManager() const { return mSoundManager; }
@ -98,6 +106,8 @@ namespace MWBase
Misc::NotNullPtr<Resource::ResourceSystem> getResourceSystem() const { return mResourceSystem; }
Misc::NotNullPtr<l10n::Manager> getL10nManager() const { return mL10nManager; }
float getFrameRateLimit() const { return mFrameRateLimit; }
void setFrameRateLimit(float value) { mFrameRateLimit = value; }

@ -34,7 +34,6 @@ namespace MWBase
public:
virtual ~LuaManager() = default;
virtual std::string translate(const std::string& contextName, const std::string& key) = 0;
virtual void newGameStarted() = 0;
virtual void gameLoaded() = 0;
virtual void registerObject(const MWWorld::Ptr& ptr) = 0;

@ -48,10 +48,11 @@
#include <components/misc/frameratelimiter.hpp>
#include <components/l10n/manager.hpp>
#include <components/lua_ui/util.hpp>
#include "../mwbase/inputmanager.hpp"
#include "../mwbase/luamanager.hpp"
#include "../mwbase/soundmanager.hpp"
#include "../mwbase/statemanager.hpp"
#include "../mwbase/world.hpp"
@ -1079,13 +1080,12 @@ namespace MWGui
std::vector<std::string> split;
Misc::StringUtils::split(tag, split, ":");
// TODO: LocalizationManager should not be a part of lua
const auto& luaManager = MWBase::Environment::get().getLuaManager();
l10n::Manager& l10nManager = *MWBase::Environment::get().getL10nManager();
// If a key has a "Context:KeyName" format, use YAML to translate data
if (split.size() == 2 && luaManager != nullptr)
if (split.size() == 2)
{
_result = luaManager->translate(split[0], split[1]);
_result = l10nManager.getContext(split[0])->formatMessage(split[1], {}, {});
return;
}

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

@ -58,12 +58,7 @@ namespace MWLua
{ std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer) });
};
addTimeBindings(api, context, false);
api["l10n"] = [l10n = context.mL10n](const std::string& context, const sol::object& fallbackLocale) {
if (fallbackLocale == sol::nil)
return l10n->getContext(context);
else
return l10n->getContext(context, fallbackLocale.as<std::string>());
};
api["l10n"] = LuaUtil::initL10nLoader(lua->sol(), MWBase::Environment::get().getL10nManager());
const MWWorld::Store<ESM::GameSetting>* gmst
= &MWBase::Environment::get().getWorld()->getStore().get<ESM::GameSetting>();
api["getGMST"] = [lua = context.mLua, gmst](const std::string& setting) -> sol::object {

@ -14,6 +14,8 @@
#include <components/settings/settings.hpp>
#include <components/l10n/manager.hpp>
#include <components/lua/utilpackage.hpp>
#include <components/lua_ui/util.hpp>
@ -38,7 +40,6 @@ namespace MWLua
LuaManager::LuaManager(const VFS::Manager* vfs, const std::filesystem::path& libsDir)
: mLua(vfs, &mConfiguration)
, mUiResourceManager(vfs)
, mL10n(vfs, &mLua)
{
Log(Debug::Info) << "Lua version: " << LuaUtil::getLuaVersion();
mLua.addInternalLibSearchPath(libsDir);
@ -60,19 +61,12 @@ namespace MWLua
mGlobalScripts.setAutoStartConf(mConfiguration.getGlobalConf());
}
void LuaManager::initL10n()
{
mL10n.init();
mL10n.setPreferredLocales(Settings::Manager::getStringArray("preferred locales", "General"));
}
void LuaManager::init()
{
Context context;
context.mIsGlobal = true;
context.mLuaManager = this;
context.mLua = &mLua;
context.mL10n = &mL10n;
context.mWorldView = &mWorldView;
context.mLocalEventQueue = &mLocalEvents;
context.mGlobalEventQueue = &mGlobalEvents;
@ -109,11 +103,6 @@ namespace MWLua
mInitialized = true;
}
std::string LuaManager::translate(const std::string& contextName, const std::string& key)
{
return mL10n.translate(contextName, key);
}
void LuaManager::loadPermanentStorage(const std::filesystem::path& userConfigPath)
{
const auto globalPath = userConfigPath / "global_storage.bin";
@ -516,9 +505,9 @@ namespace MWLua
LuaUi::clearUserInterface();
MWBase::Environment::get().getWindowManager()->setConsoleMode("");
MWBase::Environment::get().getL10nManager()->dropCache();
mUiResourceManager.clear();
mLua.dropScriptCache();
mL10n.clear();
initConfiguration();
{ // Reload global scripts

@ -4,7 +4,6 @@
#include <map>
#include <set>
#include <components/lua/l10n.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/storage.hpp>
@ -29,9 +28,6 @@ namespace MWLua
public:
LuaManager(const VFS::Manager* vfs, const std::filesystem::path& libsDir);
// Called by engine.cpp before UI setup.
void initL10n();
// Called by engine.cpp when the environment is fully initialized.
void init();
@ -42,8 +38,6 @@ namespace MWLua
// thread (in parallel with osg Cull). Can not use scene graph.
void update();
std::string translate(const std::string& contextName, const std::string& key) override;
// Called by engine.cpp from the main thread. Can use scene graph.
void synchronizedUpdate();
@ -140,7 +134,6 @@ namespace MWLua
LuaUtil::ScriptsConfiguration mConfiguration;
LuaUtil::LuaState mLua;
LuaUi::ResourceManager mUiResourceManager;
LuaUtil::L10nManager mL10n;
sol::table mNearbyPackage;
sol::table mUserInterfacePackage;
sol::table mCameraPackage;

@ -3,6 +3,7 @@
#include <components/files/fixedpath.hpp>
#include <components/l10n/manager.hpp>
#include <components/lua/l10n.hpp>
#include <components/lua/luastate.hpp>
@ -84,20 +85,21 @@ you_have_arrows: "Arrows count: {count}"
internal::CaptureStdout();
LuaUtil::LuaState lua{ mVFS.get(), &mCfg };
sol::state& l = lua.sol();
LuaUtil::L10nManager l10n(mVFS.get(), &lua);
l10n.init();
l10n.setPreferredLocales({ "de", "en" });
l10n::Manager l10nManager(mVFS.get());
l10nManager.setPreferredLocales({ "de", "en" });
EXPECT_THAT(internal::GetCapturedStdout(), "Preferred locales: de en\n");
l["l10n"] = LuaUtil::initL10nLoader(l, &l10nManager);
internal::CaptureStdout();
l["t1"] = l10n.getContext("Test1");
l.safe_script("t1 = l10n('Test1')");
EXPECT_THAT(internal::GetCapturedStdout(),
"Fallback locale: en\n"
"Language file \"l10n/Test1/de.yaml\" is enabled\n"
"Language file \"l10n/Test1/en.yaml\" is enabled\n");
internal::CaptureStdout();
l["t2"] = l10n.getContext("Test2");
l.safe_script("t2 = l10n('Test2')");
{
std::string output = internal::GetCapturedStdout();
EXPECT_THAT(output, HasSubstr("Language file \"l10n/Test2/en.yaml\" is enabled"));
@ -111,7 +113,7 @@ you_have_arrows: "Arrows count: {count}"
EXPECT_EQ(get<std::string>(l, "t2('you_have_arrows', {count=3})"), "Arrows count: 3");
internal::CaptureStdout();
l10n.setPreferredLocales({ "en", "de" });
l10nManager.setPreferredLocales({ "en", "de" });
EXPECT_THAT(internal::GetCapturedStdout(),
"Preferred locales: en de\n"
"Language file \"l10n/Test1/en.yaml\" is enabled\n"
@ -140,7 +142,7 @@ you_have_arrows: "Arrows count: {count}"
EXPECT_EQ(get<std::string>(l, "t1('{unknown_arg}')"), "{unknown_arg}");
EXPECT_EQ(get<std::string>(l, "t1('{num, integer}', {num=1})"), "{num, integer}");
// Doesn't give a valid currency symbol with `en`. Not that openmw is designed for real world currency.
l10n.setPreferredLocales({ "en-US", "de" });
l10nManager.setPreferredLocales({ "en-US", "de" });
EXPECT_EQ(get<std::string>(l, "t1('currency', {money=10000.10})"), "You have $10,000.10");
// Note: Not defined in English localisation file, so we fall back to the German before falling back to the key
EXPECT_EQ(get<std::string>(l, "t1('Hello {name}!', {name='World'})"), "Hallo World!");
@ -149,7 +151,7 @@ you_have_arrows: "Arrows count: {count}"
// Test that locales with variants and country codes fall back to more generic locales
internal::CaptureStdout();
l10n.setPreferredLocales({ "en-GB-oed", "de" });
l10nManager.setPreferredLocales({ "en-GB-oed", "de" });
EXPECT_THAT(internal::GetCapturedStdout(),
"Preferred locales: en_GB_OED de\n"
"Language file \"l10n/Test1/en.yaml\" is enabled\n"
@ -158,8 +160,8 @@ you_have_arrows: "Arrows count: {count}"
EXPECT_EQ(get<std::string>(l, "t2('you_have_arrows', {count=3})"), "Arrows count: 3");
// Test setting fallback language
l["t3"] = l10n.getContext("Test3", "de");
l10n.setPreferredLocales({ "en" });
l.safe_script("t3 = l10n('Test3', 'de')");
l10nManager.setPreferredLocales({ "en" });
EXPECT_EQ(get<std::string>(l, "t3('Hello {name}!', {name='World'})"), "Hallo World!");
}

@ -33,7 +33,7 @@ add_component_dir (lua
)
add_component_dir (l10n
messagebundles
messagebundles manager
)
add_component_dir (settings

@ -0,0 +1,95 @@
#include "manager.hpp"
#include <unicode/errorcode.h>
#include <components/debug/debuglog.hpp>
#include <components/vfs/manager.hpp>
namespace l10n
{
void Manager::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& [key, context] : mCache)
updateContext(key.first, *context);
}
void Manager::readLangData(const std::string& name, MessageBundles& ctx, const icu::Locale& lang)
{
std::string path = "l10n/";
path.append(name);
path.append("/");
path.append(lang.getName());
path.append(".yaml");
if (!mVFS->exists(path))
return;
ctx.load(*mVFS->get(path), lang, path);
}
void Manager::updateContext(const std::string& name, MessageBundles& ctx)
{
icu::Locale fallbackLocale = ctx.getFallbackLocale();
ctx.setPreferredLocales(mPreferredLocales);
int localeCount = 0;
bool fallbackLocaleInPreferred = false;
for (const icu::Locale& loc : ctx.getPreferredLocales())
{
if (!ctx.isLoaded(loc))
readLangData(name, ctx, loc);
if (ctx.isLoaded(loc))
{
localeCount++;
Log(Debug::Verbose) << "Language file \"l10n/" << name << "/" << loc.getName() << ".yaml\" is enabled";
if (loc == ctx.getFallbackLocale())
fallbackLocaleInPreferred = true;
}
}
if (!ctx.isLoaded(ctx.getFallbackLocale()))
readLangData(name, ctx, ctx.getFallbackLocale());
if (ctx.isLoaded(ctx.getFallbackLocale()) && !fallbackLocaleInPreferred)
Log(Debug::Verbose) << "Fallback language file \"l10n/" << name << "/" << ctx.getFallbackLocale().getName()
<< ".yaml\" is enabled";
if (localeCount == 0)
{
Log(Debug::Warning) << "No language files for the preferred languages found in \"l10n/" << name << "\"";
}
}
std::shared_ptr<const MessageBundles> Manager::getContext(
const std::string& contextName, const std::string& fallbackLocaleName)
{
std::pair<std::string, std::string> key(contextName, fallbackLocaleName);
auto it = mCache.find(key);
if (it != mCache.end())
return it->second;
auto allowedChar = [](char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_';
};
bool valid = !contextName.empty();
for (char c : contextName)
valid = valid && allowedChar(c);
if (!valid)
throw std::runtime_error(std::string("Invalid l10n context name: ") + contextName);
icu::Locale fallbackLocale(fallbackLocaleName.c_str());
std::shared_ptr<MessageBundles> ctx = std::make_shared<MessageBundles>(mPreferredLocales, fallbackLocale);
{
Log msg(Debug::Verbose);
msg << "Fallback locale: " << fallbackLocale.getName();
}
updateContext(contextName, *ctx);
mCache.emplace(key, ctx);
return ctx;
}
}

@ -0,0 +1,40 @@
#ifndef COMPONENTS_L10N_MANAGER_H
#define COMPONENTS_L10N_MANAGER_H
#include <components/l10n/messagebundles.hpp>
namespace VFS
{
class Manager;
}
namespace l10n
{
class Manager
{
public:
Manager(const VFS::Manager* vfs)
: mVFS(vfs)
{
}
void dropCache() { mCache.clear(); }
void setPreferredLocales(const std::vector<std::string>& locales);
const std::vector<icu::Locale>& getPreferredLocales() const { return mPreferredLocales; }
std::shared_ptr<const MessageBundles> getContext(
const std::string& contextName, const std::string& fallbackLocale = "en");
private:
void readLangData(const std::string& name, MessageBundles& ctx, const icu::Locale& lang);
void updateContext(const std::string& name, MessageBundles& ctx);
const VFS::Manager* mVFS;
std::vector<icu::Locale> mPreferredLocales;
std::map<std::pair<std::string, std::string>, std::shared_ptr<MessageBundles>> mCache;
};
}
#endif // COMPONENTS_L10N_MANAGER_H

@ -1,65 +1,18 @@
#include "l10n.hpp"
#include <unicode/errorcode.h>
#include <components/debug/debuglog.hpp>
#include <components/vfs/manager.hpp>
#include <components/l10n/manager.hpp>
namespace sol
namespace
{
template <>
struct is_automagical<LuaUtil::L10nManager::Context> : std::false_type
struct L10nContext
{
std::shared_ptr<const l10n::MessageBundles> mData;
};
}
namespace LuaUtil
{
void L10nManager::init()
{
sol::usertype<Context> ctx = mLua->sol().new_usertype<Context>("L10nContext");
ctx[sol::meta_function::call] = &Context::translate;
}
std::string L10nManager::translate(const std::string& contextName, const std::string& key)
{
Context& ctx = getContext(contextName).as<Context>();
return ctx.translate(key, sol::nil);
}
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)
void getICUArgs(std::string_view messageId, const sol::table& table, std::vector<icu::UnicodeString>& argNames,
std::vector<icu::Formattable>& args)
{
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& [key, value] : table)
{
// Argument values
@ -80,77 +33,37 @@ namespace LuaUtil
const auto str = key.as<std::string>();
argNames.push_back(icu::UnicodeString::fromUTF8(icu::StringPiece(str.data(), str.size())));
}
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)
namespace sol
{
template <>
struct is_automagical<L10nContext> : std::false_type
{
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)
namespace LuaUtil
{
sol::function initL10nLoader(sol::state& lua, l10n::Manager* manager)
{
auto it = mContexts.find(contextName);
if (it != mContexts.end())
return sol::make_object(mLua->sol(), it->second);
auto allowedChar = [](char c) {
return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_';
sol::usertype<L10nContext> ctxDef = lua.new_usertype<L10nContext>("L10nContext");
ctxDef[sol::meta_function::call]
= [](const L10nContext& ctx, std::string_view key, sol::optional<sol::table> args) {
std::vector<icu::Formattable> argValues;
std::vector<icu::UnicodeString> argNames;
if (args)
getICUArgs(key, *args, argNames, argValues);
return ctx.mData->formatMessage(key, argNames, argValues);
};
bool valid = !contextName.empty();
for (char c : contextName)
valid = valid && allowedChar(c);
if (!valid)
throw std::runtime_error(std::string("Invalid l10n context name: ") + contextName);
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);
}
return sol::make_object(
lua, [manager](const std::string& contextName, sol::optional<std::string> fallbackLocale) {
if (fallbackLocale)
return L10nContext{ manager->getContext(contextName, *fallbackLocale) };
else
return L10nContext{ manager->getContext(contextName) };
});
}
}

@ -1,53 +1,16 @@
#ifndef COMPONENTS_LUA_I18N_H
#define COMPONENTS_LUA_I18N_H
#ifndef COMPONENTS_LUA_L10N_H
#define COMPONENTS_LUA_L10N_H
#include "luastate.hpp"
#include <sol/sol.hpp>
#include <components/l10n/messagebundles.hpp>
namespace VFS
namespace l10n
{
class Manager;
}
namespace LuaUtil
{
class L10nManager
{
public:
L10nManager(const VFS::Manager* vfs, LuaState* lua)
: mVFS(vfs)
, mLua(lua)
{
}
void init();
void clear() { mContexts.clear(); }
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");
std::string translate(const std::string& contextName, const std::string& key);
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;
};
sol::function initL10nLoader(sol::state& lua, l10n::Manager* manager);
}
#endif // COMPONENTS_LUA_I18N_H
#endif // COMPONENTS_LUA_L10N_H

Loading…
Cancel
Save