mirror of https://github.com/OpenMW/openmw.git
Lua i18n updates
parent
5a2cafebea
commit
21ffbcc4b4
@ -1,110 +0,0 @@
|
|||||||
#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,157 @@
|
|||||||
|
#include "gmock/gmock.h"
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <components/files/fixedpath.hpp>
|
||||||
|
|
||||||
|
#include <components/lua/luastate.hpp>
|
||||||
|
#include <components/lua/l10n.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(
|
||||||
|
good_morning: "Good morning."
|
||||||
|
you_have_arrows: |-
|
||||||
|
{count, plural,
|
||||||
|
=0{You have no arrows.}
|
||||||
|
one{You have one arrow.}
|
||||||
|
other{You have {count} arrows.}
|
||||||
|
}
|
||||||
|
pc_must_come: |-
|
||||||
|
{PCGender, select,
|
||||||
|
male {He is}
|
||||||
|
female {She is}
|
||||||
|
other {They are}
|
||||||
|
} coming with us.
|
||||||
|
quest_completion: "The quest is {done, number, percent} complete."
|
||||||
|
ordinal: "You came in {num, ordinal} place."
|
||||||
|
spellout: "There {num, plural, one{is {num, spellout} thing} other{are {num, spellout} things}}."
|
||||||
|
duration: "It took {num, duration}"
|
||||||
|
numbers: "{int} and {double, number, integer} are integers, but {double} is a double"
|
||||||
|
rounding: "{value, number, :: .00}"
|
||||||
|
)X");
|
||||||
|
|
||||||
|
TestFile test1De(R"X(
|
||||||
|
good_morning: "Guten Morgen."
|
||||||
|
you_have_arrows: |-
|
||||||
|
{count, plural,
|
||||||
|
one{Du hast ein Pfeil.}
|
||||||
|
other{Du hast {count} Pfeile.}
|
||||||
|
}
|
||||||
|
"Hello {name}!": "Hallo {name}!"
|
||||||
|
)X");
|
||||||
|
|
||||||
|
TestFile test1EnUS(R"X(
|
||||||
|
currency: "You have {money, number, currency}"
|
||||||
|
)X");
|
||||||
|
|
||||||
|
TestFile test2En(R"X(
|
||||||
|
good_morning: "Morning!"
|
||||||
|
you_have_arrows: "Arrows count: {count}"
|
||||||
|
)X");
|
||||||
|
|
||||||
|
struct LuaL10nTest : Test
|
||||||
|
{
|
||||||
|
std::unique_ptr<VFS::Manager> mVFS = createTestVFS({
|
||||||
|
{"l10n/Test1/en.yaml", &test1En},
|
||||||
|
{"l10n/Test1/en_US.yaml", &test1EnUS},
|
||||||
|
{"l10n/Test1/de.yaml", &test1De},
|
||||||
|
{"l10n/Test2/en.yaml", &test2En},
|
||||||
|
{"l10n/Test3/en.yaml", &test1En},
|
||||||
|
{"l10n/Test3/de.yaml", &test1De},
|
||||||
|
});
|
||||||
|
|
||||||
|
LuaUtil::ScriptsConfiguration mCfg;
|
||||||
|
};
|
||||||
|
|
||||||
|
TEST_F(LuaL10nTest, L10n)
|
||||||
|
{
|
||||||
|
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"});
|
||||||
|
EXPECT_THAT(internal::GetCapturedStdout(), "Preferred locales: de en\n");
|
||||||
|
|
||||||
|
internal::CaptureStdout();
|
||||||
|
l["t1"] = l10n.getContext("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");
|
||||||
|
{
|
||||||
|
std::string output = internal::GetCapturedStdout();
|
||||||
|
EXPECT_THAT(output, HasSubstr("Language file \"l10n/Test2/en.yaml\" 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();
|
||||||
|
l10n.setPreferredLocales({"en", "de"});
|
||||||
|
EXPECT_THAT(internal::GetCapturedStdout(),
|
||||||
|
"Preferred locales: en de\n"
|
||||||
|
"Language file \"l10n/Test1/en.yaml\" is enabled\n"
|
||||||
|
"Language file \"l10n/Test1/de.yaml\" is enabled\n"
|
||||||
|
"Language file \"l10n/Test2/en.yaml\" 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('pc_must_come', {PCGender=\"male\"})"), "He is coming with us.");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t1('pc_must_come', {PCGender=\"female\"})"), "She is coming with us.");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t1('pc_must_come', {PCGender=\"blah\"})"), "They are coming with us.");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t1('pc_must_come', {PCGender=\"other\"})"), "They are coming with us.");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t1('quest_completion', {done=0.1})"), "The quest is 10% complete.");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t1('quest_completion', {done=1})"), "The quest is 100% complete.");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t1('ordinal', {num=1})"), "You came in 1st place.");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t1('ordinal', {num=100})"), "You came in 100th place.");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t1('spellout', {num=1})"), "There is one thing.");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t1('spellout', {num=100})"), "There are one hundred things.");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t1('duration', {num=100})"), "It took 1:40");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t1('numbers', {int=123, double=123.456})"), "123 and 123 are integers, but 123.456 is a double");
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t1('rounding', {value=123.456789})"), "123.46");
|
||||||
|
// Check that failed messages display the key instead of an empty string
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t1('{mismatched_braces')"), "{mismatched_braces");
|
||||||
|
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"});
|
||||||
|
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!");
|
||||||
|
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");
|
||||||
|
|
||||||
|
// Test that locales with variants and country codes fall back to more generic locales
|
||||||
|
internal::CaptureStdout();
|
||||||
|
l10n.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"
|
||||||
|
"Language file \"l10n/Test1/de.yaml\" is enabled\n"
|
||||||
|
"Language file \"l10n/Test2/en.yaml\" is enabled\n");
|
||||||
|
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"});
|
||||||
|
EXPECT_EQ(get<std::string>(l, "t3('Hello {name}!', {name='World'})"), "Hallo World!");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,167 @@
|
|||||||
|
#include "messagebundles.hpp"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <unicode/errorcode.h>
|
||||||
|
#include <unicode/calendar.h>
|
||||||
|
#include <yaml-cpp/yaml.h>
|
||||||
|
|
||||||
|
#include <components/debug/debuglog.hpp>
|
||||||
|
|
||||||
|
namespace l10n
|
||||||
|
{
|
||||||
|
MessageBundles::MessageBundles(const std::vector<icu::Locale> &preferredLocales, icu::Locale &fallbackLocale) :
|
||||||
|
mFallbackLocale(fallbackLocale)
|
||||||
|
{
|
||||||
|
setPreferredLocales(preferredLocales);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MessageBundles::setPreferredLocales(const std::vector<icu::Locale> &preferredLocales)
|
||||||
|
{
|
||||||
|
mPreferredLocales.clear();
|
||||||
|
mPreferredLocaleStrings.clear();
|
||||||
|
for (const icu::Locale &loc: preferredLocales)
|
||||||
|
{
|
||||||
|
mPreferredLocales.push_back(loc);
|
||||||
|
mPreferredLocaleStrings.push_back(loc.getName());
|
||||||
|
// Try without variant or country if they are specified, starting with the most specific
|
||||||
|
if (strcmp(loc.getVariant(), "") != 0)
|
||||||
|
{
|
||||||
|
icu::Locale withoutVariant(loc.getLanguage(), loc.getCountry());
|
||||||
|
mPreferredLocales.push_back(withoutVariant);
|
||||||
|
mPreferredLocaleStrings.push_back(withoutVariant.getName());
|
||||||
|
}
|
||||||
|
if (strcmp(loc.getCountry(), "") != 0)
|
||||||
|
{
|
||||||
|
icu::Locale withoutCountry(loc.getLanguage());
|
||||||
|
mPreferredLocales.push_back(withoutCountry);
|
||||||
|
mPreferredLocaleStrings.push_back(withoutCountry.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string getErrorText(const UParseError &parseError)
|
||||||
|
{
|
||||||
|
icu::UnicodeString preContext(parseError.preContext), postContext(parseError.postContext);
|
||||||
|
std::string parseErrorString;
|
||||||
|
preContext.toUTF8String(parseErrorString);
|
||||||
|
postContext.toUTF8String(parseErrorString);
|
||||||
|
return parseErrorString;
|
||||||
|
}
|
||||||
|
|
||||||
|
static bool checkSuccess(const icu::ErrorCode &status, const std::string &message, const UParseError parseError = UParseError())
|
||||||
|
{
|
||||||
|
if (status.isFailure())
|
||||||
|
{
|
||||||
|
std::string errorText = getErrorText(parseError);
|
||||||
|
if (errorText.size())
|
||||||
|
{
|
||||||
|
Log(Debug::Error) << message << ": " << status.errorName() << " in \"" << errorText << "\"";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Log(Debug::Error) << message << ": " << status.errorName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return status.isSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
void MessageBundles::load(std::istream &input, const icu::Locale& lang, const std::string &path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
YAML::Node data = YAML::Load(input);
|
||||||
|
std::string localeName = lang.getName();
|
||||||
|
for (const auto& it: data)
|
||||||
|
{
|
||||||
|
std::string key = it.first.as<std::string>();
|
||||||
|
std::string value = it.second.as<std::string>();
|
||||||
|
icu::UnicodeString pattern = icu::UnicodeString::fromUTF8(value);
|
||||||
|
icu::ErrorCode status;
|
||||||
|
UParseError parseError;
|
||||||
|
icu::MessageFormat message(pattern, lang, parseError, status);
|
||||||
|
if (checkSuccess(status, std::string("Failed to create message ")
|
||||||
|
+ key + " for locale " + lang.getName(), parseError))
|
||||||
|
{
|
||||||
|
mBundles[localeName].insert(std::make_pair(key, message));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (std::exception& e)
|
||||||
|
{
|
||||||
|
Log(Debug::Error) << "Can not load " << path << ": " << e.what();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const icu::MessageFormat * MessageBundles::findMessage(std::string_view key, const std::string &localeName) const
|
||||||
|
{
|
||||||
|
auto iter = mBundles.find(localeName);
|
||||||
|
if (iter != mBundles.end())
|
||||||
|
{
|
||||||
|
auto message = iter->second.find(key.data());
|
||||||
|
if (message != iter->second.end())
|
||||||
|
{
|
||||||
|
return &(message->second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string MessageBundles::formatMessage(std::string_view key, const std::map<std::string, icu::Formattable> &args) const
|
||||||
|
{
|
||||||
|
std::vector<icu::UnicodeString> argNames;
|
||||||
|
std::vector<icu::Formattable> argValues;
|
||||||
|
for (auto& [key, value] : args)
|
||||||
|
{
|
||||||
|
argNames.push_back(icu::UnicodeString::fromUTF8(key));
|
||||||
|
argValues.push_back(value);
|
||||||
|
}
|
||||||
|
return formatMessage(key, argNames, argValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string MessageBundles::formatMessage(std::string_view key, const std::vector<icu::UnicodeString> &argNames, const std::vector<icu::Formattable> &args) const
|
||||||
|
{
|
||||||
|
icu::UnicodeString result;
|
||||||
|
std::string resultString;
|
||||||
|
icu::ErrorCode success;
|
||||||
|
|
||||||
|
const icu::MessageFormat *message = nullptr;
|
||||||
|
for (auto &loc: mPreferredLocaleStrings)
|
||||||
|
{
|
||||||
|
message = findMessage(key, loc);
|
||||||
|
if (message)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// If no requested locales included the message, try the fallback locale
|
||||||
|
if (!message)
|
||||||
|
message = findMessage(key, mFallbackLocale.getName());
|
||||||
|
|
||||||
|
if (message)
|
||||||
|
{
|
||||||
|
if (args.size() > 0 && argNames.size() > 0)
|
||||||
|
message->format(&argNames[0], &args[0], args.size(), result, success);
|
||||||
|
else
|
||||||
|
message->format(nullptr, nullptr, args.size(), result, success);
|
||||||
|
checkSuccess(success, std::string("Failed to format message ") + key.data());
|
||||||
|
result.toUTF8String(resultString);
|
||||||
|
return resultString;
|
||||||
|
}
|
||||||
|
icu::Locale defaultLocale(NULL);
|
||||||
|
if (mPreferredLocales.size() > 0)
|
||||||
|
{
|
||||||
|
defaultLocale = mPreferredLocales[0];
|
||||||
|
}
|
||||||
|
UParseError parseError;
|
||||||
|
icu::MessageFormat defaultMessage(icu::UnicodeString::fromUTF8(key), defaultLocale, parseError, success);
|
||||||
|
if (!checkSuccess(success, std::string("Failed to create message ") + key.data(), parseError))
|
||||||
|
// If we can't parse the key as a pattern, just return the key
|
||||||
|
return std::string(key);
|
||||||
|
|
||||||
|
if (args.size() > 0 && argNames.size() > 0)
|
||||||
|
defaultMessage.format(&argNames[0], &args[0], args.size(), result, success);
|
||||||
|
else
|
||||||
|
defaultMessage.format(nullptr, nullptr, args.size(), result, success);
|
||||||
|
checkSuccess(success, std::string("Failed to format message ") + key.data());
|
||||||
|
result.toUTF8String(resultString);
|
||||||
|
return resultString;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
#ifndef COMPONENTS_L10N_MESSAGEBUNDLES_H
|
||||||
|
#define COMPONENTS_L10N_MESSAGEBUNDLES_H
|
||||||
|
|
||||||
|
#include <string_view>
|
||||||
|
#include <map>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <unicode/locid.h>
|
||||||
|
#include <unicode/msgfmt.h>
|
||||||
|
|
||||||
|
namespace l10n
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @brief A collection of Message Bundles
|
||||||
|
*
|
||||||
|
* Class handling localised message storage and lookup, including fallback locales when messages are missing.
|
||||||
|
*
|
||||||
|
* If no fallback locale is provided (or a message fails to be found), the key will be formatted instead,
|
||||||
|
* or returned verbatim if formatting fails.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
class MessageBundles
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
/* @brief Constructs an empty MessageBundles
|
||||||
|
*
|
||||||
|
* @param preferredLocales user-requested locales, in order of priority
|
||||||
|
* Each locale will be checked when looking up messages, in case some resource files are incomplete.
|
||||||
|
* For each locale which contains a country code or a variant, the locales obtained by removing first
|
||||||
|
* the variant, then the country code, will also be checked before moving on to the next locale in the list.
|
||||||
|
* @param fallbackLocale the fallback locale which should be used if messages cannot be found for the user
|
||||||
|
* preferred locales
|
||||||
|
*/
|
||||||
|
MessageBundles(const std::vector<icu::Locale> &preferredLocales, icu::Locale &fallbackLocale);
|
||||||
|
std::string formatMessage(std::string_view key, const std::map<std::string, icu::Formattable> &args) const;
|
||||||
|
std::string formatMessage(std::string_view key, const std::vector<icu::UnicodeString> &argNames, const std::vector<icu::Formattable> &args) const;
|
||||||
|
void setPreferredLocales(const std::vector<icu::Locale> &preferredLocales);
|
||||||
|
const std::vector<icu::Locale> & getPreferredLocales() const { return mPreferredLocales; }
|
||||||
|
void load(std::istream &input, const icu::Locale &lang, const std::string &path);
|
||||||
|
bool isLoaded(icu::Locale loc) const { return mBundles.find(loc.getName()) != mBundles.end(); }
|
||||||
|
const icu::Locale & getFallbackLocale() const { return mFallbackLocale; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
// icu::Locale isn't hashable (or comparable), so we use the string form instead, which is canonicalized
|
||||||
|
std::unordered_map<std::string, std::unordered_map<std::string, icu::MessageFormat>> mBundles;
|
||||||
|
const icu::Locale mFallbackLocale;
|
||||||
|
std::vector<std::string> mPreferredLocaleStrings;
|
||||||
|
std::vector<icu::Locale> mPreferredLocales;
|
||||||
|
const icu::MessageFormat * findMessage(std::string_view key, const std::string &localeName) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#endif // COMPONENTS_L10N_MESSAGEBUNDLES_H
|
@ -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
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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
|
@ -1,17 +0,0 @@
|
|||||||
if (NOT DEFINED OPENMW_RESOURCES_ROOT)
|
|
||||||
return()
|
|
||||||
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}")
|
|
@ -1,22 +0,0 @@
|
|||||||
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.
|
|
@ -1,164 +0,0 @@
|
|||||||
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
|
|
@ -1,188 +0,0 @@
|
|||||||
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
|
|
@ -1,60 +0,0 @@
|
|||||||
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
|
|
@ -1,280 +0,0 @@
|
|||||||
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
|
|
@ -1,49 +0,0 @@
|
|||||||
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 +0,0 @@
|
|||||||
return '0.9.2'
|
|
@ -1,42 +0,0 @@
|
|||||||
-- source: https://en.uesp.net/wiki/Lore:Calendar
|
|
||||||
|
|
||||||
return {
|
|
||||||
month1 = "Morning Star",
|
|
||||||
month2 = "Sun's Dawn",
|
|
||||||
month3 = "First Seed",
|
|
||||||
month4 = "Rain's Hand",
|
|
||||||
month5 = "Second Seed",
|
|
||||||
month6 = "Midyear",
|
|
||||||
month7 = "Sun's Height",
|
|
||||||
month8 = "Last Seed",
|
|
||||||
month9 = "Hearthfire",
|
|
||||||
month10 = "Frostfall",
|
|
||||||
month11 = "Sun's Dusk",
|
|
||||||
month12 = "Evening Star",
|
|
||||||
|
|
||||||
-- The variant of month names in the context "day X of month Y".
|
|
||||||
-- In English it is the same, but some languages require a different form.
|
|
||||||
monthInGenitive1 = "Morning Star",
|
|
||||||
monthInGenitive2 = "Sun's Dawn",
|
|
||||||
monthInGenitive3 = "First Seed",
|
|
||||||
monthInGenitive4 = "Rain's Hand",
|
|
||||||
monthInGenitive5 = "Second Seed",
|
|
||||||
monthInGenitive6 = "Midyear",
|
|
||||||
monthInGenitive7 = "Sun's Height",
|
|
||||||
monthInGenitive8 = "Last Seed",
|
|
||||||
monthInGenitive9 = "Hearthfire",
|
|
||||||
monthInGenitive10 = "Frostfall",
|
|
||||||
monthInGenitive11 = "Sun's Dusk",
|
|
||||||
monthInGenitive12 = "Evening Star",
|
|
||||||
|
|
||||||
dateFormat = "day %{day} of %{monthInGenitive} %{year}",
|
|
||||||
|
|
||||||
weekday1 = "Sundas",
|
|
||||||
weekday2 = "Morndas",
|
|
||||||
weekday3 = "Tirdas",
|
|
||||||
weekday4 = "Middas",
|
|
||||||
weekday5 = "Turdas",
|
|
||||||
weekday6 = "Fredas",
|
|
||||||
weekday7 = "Loredas",
|
|
||||||
}
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
|||||||
|
# source: https://en.uesp.net/wiki/Lore:Calendar
|
||||||
|
|
||||||
|
month1: "Morning Star"
|
||||||
|
month2: "Sun's Dawn"
|
||||||
|
month3: "First Seed"
|
||||||
|
month4: "Rain's Hand"
|
||||||
|
month5: "Second Seed"
|
||||||
|
month6: "Midyear"
|
||||||
|
month7: "Sun's Height"
|
||||||
|
month8: "Last Seed"
|
||||||
|
month9: "Hearthfire"
|
||||||
|
month10: "Frostfall"
|
||||||
|
month11: "Sun's Dusk"
|
||||||
|
month12: "Evening Star"
|
||||||
|
|
||||||
|
# The variant of month names in the context "day X of month Y".
|
||||||
|
# In English it is the same, but some languages require a different form.
|
||||||
|
monthInGenitive1: "Morning Star"
|
||||||
|
monthInGenitive2: "Sun's Dawn"
|
||||||
|
monthInGenitive3: "First Seed"
|
||||||
|
monthInGenitive4: "Rain's Hand"
|
||||||
|
monthInGenitive5: "Second Seed"
|
||||||
|
monthInGenitive6: "Midyear"
|
||||||
|
monthInGenitive7: "Sun's Height"
|
||||||
|
monthInGenitive8: "Last Seed"
|
||||||
|
monthInGenitive9: "Hearthfire"
|
||||||
|
monthInGenitive10: "Frostfall"
|
||||||
|
monthInGenitive11: "Sun's Dusk"
|
||||||
|
monthInGenitive12: "Evening Star"
|
||||||
|
|
||||||
|
dateFormat: "day {day} of {monthInGenitive} {year}"
|
||||||
|
|
||||||
|
weekday1: "Sundas"
|
||||||
|
weekday2: "Morndas"
|
||||||
|
weekday3: "Tirdas"
|
||||||
|
weekday4: "Middas"
|
||||||
|
weekday5: "Turdas"
|
||||||
|
weekday6: "Fredas"
|
||||||
|
weekday7: "Loredas"
|
Loading…
Reference in New Issue