Lua i18n updates

pull/3226/head
Benjamin Winger 2 years ago committed by Petr Mikheev
parent 5a2cafebea
commit 21ffbcc4b4

@ -13,6 +13,8 @@ brew update --quiet
command -v ccache >/dev/null 2>&1 || brew install ccache
command -v cmake >/dev/null 2>&1 || brew install cmake
command -v qmake >/dev/null 2>&1 || brew install qt@5
command -v pkgdata >/dev/null 2>&1 || brew install icu4c
brew install yaml-cpp
export PATH="/usr/local/opt/qt@5/bin:$PATH" # needed to use qmake in none default path as qt now points to qt6
ccache --version

@ -6,6 +6,20 @@ sed -i s/"NOT FFVER_OK"/"FALSE"/ CMakeLists.txt
mkdir -p build
cd build
# Build a version of ICU for the host so that it can use the tools during the cross-compilation
mkdir -p icu-host-build
cd icu-host-build
if [ -r ../extern/fetched/icu/icu4c/source/configure ]; then
ICU_SOURCE_DIR=../extern/fetched/icu/icu4c/source
else
wget https://github.com/unicode-org/icu/archive/refs/tags/release-70-1.zip
unzip release-70-1.zip
ICU_SOURCE_DIR=./icu-release-70-1/icu4c/source
fi
${ICU_SOURCE_DIR}/configure --disable-tests --disable-samples --disable-icuio --disable-extras CC="ccache gcc" CXX="ccache g++"
make -j $(nproc)
cd ..
cmake \
-DCMAKE_TOOLCHAIN_FILE=/android-ndk-r22/build/cmake/android.toolchain.cmake \
-DANDROID_ABI=arm64-v8a \
@ -26,4 +40,7 @@ cmake \
-DBUILD_BULLETOBJECTTOOL=OFF \
-DOPENMW_USE_SYSTEM_MYGUI=OFF \
-DOPENMW_USE_SYSTEM_SQLITE3=OFF \
-DOPENMW_USE_SYSTEM_YAML_CPP=OFF \
-DOPENMW_USE_SYSTEM_ICU=OFF \
-DOPENMW_ICU_HOST_BUILD_DIR="$(pwd)/icu-host-build" \
..

@ -512,6 +512,8 @@ if ! [ -z $USE_CCACHE ]; then
add_cmake_opts "-DCMAKE_C_COMPILER_LAUNCHER=ccache -DCMAKE_CXX_COMPILER_LAUNCHER=ccache"
fi
ICU_VER="70_1"
echo
echo "==================================="
echo "Starting prebuild on MSVC${MSVC_DISPLAY_YEAR} WIN${BITS}"
@ -598,6 +600,11 @@ if [ -z $SKIP_DOWNLOAD ]; then
git clone -b release-1.10.0 https://github.com/google/googletest.git
fi
fi
# ICU
download "ICU ${ICU_VER/_/.}"\
"https://github.com/unicode-org/icu/releases/download/release-${ICU_VER/_/-}/icu4c-${ICU_VER}-Win${BITS}-MSVC2019.zip" \
"icu4c-${ICU_VER}-Win${BITS}-MSVC2019.zip"
fi
cd .. #/..
@ -1027,6 +1034,24 @@ if [ ! -z $TEST_FRAMEWORK ]; then
fi
cd $DEPS
echo
# ICU
printf "ICU ${ICU_VER/_/.}... "
{
if [ -d ICU ]; then
printf "Exists. "
elif [ -z $SKIP_EXTRACT ]; then
rm -rf ICU
eval 7z x -y icu4c-${ICU_VER}-Win${BITS}-MSVC2019.zip -o$(real_pwd)/ICU $STRIP
fi
export ICU_ROOT="$(real_pwd)/ICU"
add_cmake_opts -DICU_INCLUDE_DIR="${ICU_ROOT}/include" \
-DICU_LIBRARY="${ICU_ROOT}/lib${BITS}/icuuc.lib " \
-DICU_DEBUG=ON
echo Done.
}
echo
cd $DEPS_INSTALL/..
echo
@ -1034,6 +1059,7 @@ echo "Setting up OpenMW build..."
add_cmake_opts -DOPENMW_MP_BUILD=on
add_cmake_opts -DCMAKE_INSTALL_PREFIX="${INSTALL_PREFIX}"
add_cmake_opts -DOPENMW_USE_SYSTEM_SQLITE3=OFF
add_cmake_opts -DOPENMW_USE_SYSTEM_YAML_CPP=OFF
if [ ! -z $CI ]; then
case $STEP in
components )

@ -20,7 +20,7 @@ declare -rA GROUPED_DEPS=(
libavcodec-dev libavformat-dev libavutil-dev libswscale-dev libswresample-dev
libsdl2-dev libqt5opengl5-dev libopenal-dev libunshield-dev libtinyxml-dev
libbullet-dev liblz4-dev libpng-dev libjpeg-dev libluajit-5.1-dev
librecast-dev libsqlite3-dev ca-certificates
librecast-dev libsqlite3-dev ca-certificates libicu-dev libyaml-cpp-dev
"
# These dependencies can alternatively be built and linked statically.

@ -248,6 +248,23 @@ set(USED_OSG_PLUGINS
osgdb_serializers_osg
osgdb_tga)
option(OPENMW_USE_SYSTEM_ICU "Use system ICU library instead of internal. If disabled, requires autotools" ON)
if(OPENMW_USE_SYSTEM_ICU)
find_package(ICU COMPONENTS uc i18n)
endif()
option(OPENMW_USE_SYSTEM_YAML_CPP "Use system yaml-cpp library instead of internal." ON)
if(OPENMW_USE_SYSTEM_YAML_CPP)
set(_yaml_cpp_static_default OFF)
else()
set(_yaml_cpp_static_default ON)
endif()
option(YAML_CPP_STATIC "Link static build of yaml-cpp into the binaries" ${_yaml_cpp_static_default})
if (OPENMW_USE_SYSTEM_YAML_CPP)
find_package(yaml-cpp)
endif()
add_subdirectory(extern)
# Sound setup
@ -442,6 +459,7 @@ include_directories(
${LUA_INCLUDE_DIR}
${SOL_INCLUDE_DIR}
${SOL_CONFIG_DIR}
${ICU_INCLUDE_DIRS}
)
link_directories(${SDL2_LIBRARY_DIRS} ${Boost_LIBRARY_DIRS})

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

@ -1,7 +1,7 @@
#include "luabindings.hpp"
#include <components/lua/luastate.hpp>
#include <components/lua/i18n.hpp>
#include <components/lua/l10n.hpp>
#include "../mwbase/environment.hpp"
#include "../mwbase/statemanager.hpp"
@ -52,7 +52,12 @@ namespace MWLua
context.mGlobalEventQueue->push_back({std::move(eventName), LuaUtil::serialize(eventData, context.mSerializer)});
};
addTimeBindings(api, context, false);
api["i18n"] = [i18n=context.mI18n](const std::string& context) { return i18n->getContext(context); };
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>());
};
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
{

@ -30,7 +30,7 @@ namespace MWLua
LuaManager::LuaManager(const VFS::Manager* vfs, const std::string& libsDir)
: mLua(vfs, &mConfiguration)
, mUiResourceManager(vfs)
, mI18n(vfs, &mLua)
, mL10n(vfs, &mLua)
{
Log(Debug::Info) << "Lua version: " << LuaUtil::getLuaVersion();
mLua.addInternalLibSearchPath(libsDir);
@ -57,7 +57,7 @@ namespace MWLua
context.mIsGlobal = true;
context.mLuaManager = this;
context.mLua = &mLua;
context.mI18n = &mI18n;
context.mL10n = &mL10n;
context.mWorldView = &mWorldView;
context.mLocalEventQueue = &mLocalEvents;
context.mGlobalEventQueue = &mGlobalEvents;
@ -67,10 +67,10 @@ 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);
mL10n.init();
std::vector<std::string> preferredLocales;
Misc::StringUtils::split(Settings::Manager::getString("preferred locales", "General"), preferredLocales, ", ");
mL10n.setPreferredLocales(preferredLocales);
initObjectBindingsForGlobalScripts(context);
initCellBindingsForGlobalScripts(context);

@ -4,7 +4,7 @@
#include <map>
#include <set>
#include <components/lua/i18n.hpp>
#include <components/lua/l10n.hpp>
#include <components/lua/luastate.hpp>
#include <components/lua/storage.hpp>
@ -102,7 +102,7 @@ namespace MWLua
LuaUtil::ScriptsConfiguration mConfiguration;
LuaUtil::LuaState mLua;
LuaUi::ResourceManager mUiResourceManager;
LuaUtil::I18nManager mI18n;
LuaUtil::L10nManager mL10n;
sol::table mNearbyPackage;
sol::table mUserInterfacePackage;
sol::table mCameraPackage;

@ -22,7 +22,7 @@ if (GTEST_FOUND AND GMOCK_FOUND)
lua/test_utilpackage.cpp
lua/test_serialization.cpp
lua/test_configuration.cpp
lua/test_i18n.cpp
lua/test_l10n.cpp
lua/test_storage.cpp
lua/test_ui_content.cpp

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

@ -29,7 +29,11 @@ endif (GIT_CHECKOUT)
# source files
add_component_dir (lua
luastate scriptscontainer utilpackage serialization configuration i18n storage
luastate scriptscontainer utilpackage serialization configuration l10n storage
)
add_component_dir (l10n
messagebundles
)
add_component_dir (settings
@ -395,6 +399,8 @@ target_link_libraries(components
Base64
SQLite::SQLite3
smhasher
${ICU_LIBRARIES}
yaml-cpp
)
target_link_libraries(components ${BULLET_LIBRARIES})

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

@ -68,3 +68,21 @@ notify on saved screenshot
:Default: False
Show message box when screenshot is saved to a file.
preferred locales
-----------------
:Type: string
:Default: en
List of the preferred locales separated by comma.
For example "de,en" means German as the first prority and English as a fallback.
Each locale must consist of a two-letter language code (e.g. "de" or "en") and
can also optionally include a two-letter country code (e.g. "en_US", "fr_CA").
Locales with country codes can match locales without one (e.g. specifying "en_US"
will match "en"), so is recommended that you include the country codes where possible,
since if the country code isn't specified the generic language-code only locale might
refer to any of the country-specific variants.
This setting can only be configured by editing the settings configuration file.

@ -26,15 +26,3 @@ If one, a separate thread is used.
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.

@ -226,3 +226,66 @@ if (BUILD_BENCHMARKS AND NOT OPENMW_USE_SYSTEM_BENCHMARK)
)
FetchContent_MakeAvailableExcludeFromAll(benchmark)
endif()
if (NOT OPENMW_USE_SYSTEM_YAML_CPP)
if(YAML_CPP_STATIC)
set(YAML_BUILD_SHARED_LIBS OFF)
else()
set(YAML_BUILD_SHARED_LIBS ON)
endif()
include(FetchContent)
FetchContent_Declare(yaml-cpp
URL https://github.com/jbeder/yaml-cpp/archive/refs/tags/yaml-cpp-0.7.0.zip
URL_HASH MD5=1e8ca0d6ccf99f3ed9506c1f6937d0ec
SOURCE_DIR fetched/yaml-cpp
)
FetchContent_MakeAvailableExcludeFromAll(yaml-cpp)
endif()
if (NOT OPENMW_USE_SYSTEM_ICU)
if (ANDROID)
# Note: Must be a build directory, not an install root, since the configure script
# looks for a configuration file which does not get installed.
set(OPENMW_ICU_HOST_BUILD_DIR "" CACHE STRING "A pre-built ICU build directory for the host system if cross-compiling")
# We need a host version of ICU so that the tools can be run when building the data library.
set(NDK_STANDARD_ROOT ${CMAKE_ANDROID_NDK}/toolchains/llvm/prebuilt/linux-x86_64)
string(REPLACE "android-" "" ANDROIDVER ${ANDROID_PLATFORM})
set(ICU_ENV
"CC=ccache ${NDK_STANDARD_ROOT}/bin/aarch64-linux-android${ANDROIDVER}-clang"
"CXX=ccache ${NDK_STANDARD_ROOT}/bin/aarch64-linux-android${ANDROIDVER}-clang"
"RANLIB=${NDK_STANDARD_ROOT}/bin/aarch64-linux-android-ranlib"
"AR=${NDK_STANDARD_ROOT}/bin/aarch64-linux-android-ar"
"CPPFLAGS=${ANDROID_COMPILER_FLAGS}"
"LDFLAGS=${ANDROID_LINKER_FLAGS} -lc -lstdc++"
)
# Wants a triple such as aarch64-linux-android, excluding a trailing
# -clang etc.
string(REGEX MATCH "^[^-]\+-[^-]+-[^-]+" ICU_TOOLCHAIN_NAME ${ANDROID_TOOLCHAIN_NAME})
set(ICU_ADDITIONAL_OPTS --host=${ICU_TOOLCHAIN_NAME}${ANDROIDVER} --with-cross-build=${OPENMW_ICU_HOST_BUILD_DIR})
endif()
include(ExternalProject)
ExternalProject_Add(icu
URL https://github.com/unicode-org/icu/archive/refs/tags/release-70-1.zip
URL_HASH MD5=49d5e2e5bab93ae1a4b56e916150544c
SOURCE_DIR fetched/icu
CONFIGURE_COMMAND ${CMAKE_COMMAND} -E env ${ICU_ENV}
<SOURCE_DIR>/icu4c/source/configure --enable-static --disable-shared
--disable-tests --disable-samples --disable-icuio --disable-extras ${ICU_ADDITIONAL_OPTS}
BUILD_COMMAND make
INSTALL_COMMAND ""
)
ExternalProject_Get_Property(icu SOURCE_DIR BINARY_DIR)
set(ICU_INCLUDE_DIRS
${SOURCE_DIR}/icu4c/source/common
${SOURCE_DIR}/icu4c/source/i18n
PARENT_SCOPE
)
foreach(ICULIB data uc i18n)
add_library(ICU::${ICULIB} STATIC IMPORTED GLOBAL)
set_target_properties(ICU::${ICULIB} PROPERTIES IMPORTED_LOCATION
${BINARY_DIR}/lib/${CMAKE_STATIC_LIBRARY_PREFIX}icu${ICULIB}${CMAKE_STATIC_LIBRARY_SUFFIX})
add_dependencies(ICU::${ICULIB} icu)
endforeach()
set(ICU_LIBRARIES ICU::i18n ICU::uc ICU::data PARENT_SCOPE)
endif()

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

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

@ -14,7 +14,7 @@ set(LUA_BUILTIN_FILES
scripts/omw/head_bobbing.lua
scripts/omw/third_person.lua
i18n/Calendar/en.lua
l10n/Calendar/en.yaml
)
foreach (f ${LUA_BUILTIN_FILES})

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

@ -6,7 +6,7 @@
local core = require('openmw.core')
local time = require('openmw_aux.time')
local i18n = core.i18n('Calendar')
local l10n = core.l10n('Calendar')
local monthsDuration = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}
local daysInWeek = 7
@ -31,10 +31,10 @@ local function gameTime(t)
end
local function defaultDateFormat(t)
return i18n('dateFormat', {
return l10n('dateFormat', {
day = t.day,
month = i18n('month' .. t.month),
monthInGenitive = i18n('monthInGenitive' .. t.month),
month = l10n('month' .. t.month),
monthInGenitive = l10n('monthInGenitive' .. t.month),
year = t.year,
})
end
@ -63,8 +63,8 @@ local function formatGameTime(formatStr, timestamp)
if formatStr == '*t' then return t end
local replFn = function(tag)
if tag == '%a' or tag == '%A' then return i18n('weekday' .. t.wday) end
if tag == '%b' or tag == '%B' then return i18n('monthInGenitive' .. t.month) end
if tag == '%a' or tag == '%A' then return l10n('weekday' .. t.wday) end
if tag == '%b' or tag == '%B' then return l10n('monthInGenitive' .. t.month) end
if tag == '%c' then
return string.format('%02d:%02d %s', t.hour, t.min, defaultDateFormat(t))
end
@ -137,7 +137,7 @@ return {
-- @param monthIndex
-- @return #string
monthName = function(m)
return i18n('month' .. ((m-1) % #monthsDuration + 1))
return l10n('month' .. ((m-1) % #monthsDuration + 1))
end,
--- The name of a month in genitive (for English is the same as `monthName`, but in some languages the form can differ).
@ -145,7 +145,7 @@ return {
-- @param monthIndex
-- @return #string
monthNameInGenitive = function(m)
return i18n('monthInGenitive' .. ((m-1) % #monthsDuration + 1))
return l10n('monthInGenitive' .. ((m-1) % #monthsDuration + 1))
end,
--- The name of a weekday
@ -153,7 +153,7 @@ return {
-- @param dayIndex
-- @return #string
weekdayName = function(d)
return i18n('weekday' .. ((d-1) % daysInWeek + 1))
return l10n('weekday' .. ((d-1) % daysInWeek + 1))
end,
}

@ -53,37 +53,71 @@
-- @return #any
---
-- 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 l10n formatting function for the given context.
-- Language files should be stored in VFS as `l10n/<ContextName>/<Locale>.yaml`.
--
-- Locales usually have the form {lang}_{COUNTRY},
-- where {lang} is a lowercase two-letter language code and {COUNTRY} is an uppercase
-- two-letter country code. Capitalization and the separator must have exactly
-- this format for language files to be recognized, but when users request a
-- locale they do not need to match capitalization and can use hyphens instead of
-- underscores.
--
-- Locales may also contain variants and keywords. See https://unicode-org.github.io/icu/userguide/locale/#language-code
-- for full details.
--
-- Messages have the form of ICU MessageFormat strings.
-- See https://unicode-org.github.io/icu/userguide/format_parse/messages/
-- for a guide to MessageFormat, and see
-- https://unicode-org.github.io/icu-docs/apidoc/released/icu4c/classicu_1_1MessageFormat.html
-- for full details of the MessageFormat syntax.
-- @function [parent=#core] l10n
-- @param #string context l10n context; recommended to use the name of the mod.
-- @param #string fallbackLocale The source locale containing the default messages
-- If omitted defaults to "en"
-- @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.',
-- },
-- }
-- # DataFiles/l10n/MyMod/en.yaml
-- good_morning: 'Good morning.'
--
-- you_have_arrows: |- {count, plural,
-- 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}!",
-- }
-- # DataFiles/l10n/MyMod/de.yaml
-- good_morning: "Guten Morgen."
-- you_have_arrows: |- {count, plural,
-- one {Du hast ein Pfeil.}
-- other {Du hast {count} Pfeile.}
-- }
-- "Hello {name}!": "Hallo {name}!"
-- @usage
-- local myMsg = core.i18n('MyMod')
-- # DataFiles/l10n/AdvancedExample/en.yaml
-- # More complicated patterns
-- # select rules can be used to match arbitrary string arguments
-- # The default keyword other must always be provided
-- pc_must_come: {PCGender, select,
-- male {He is}
-- female {She is}
-- other {They are}
-- } coming with us.
-- # Numbers have various formatting options
-- quest_completion: "The quest is {done, number, percent} complete.",
-- # E.g. "You came in 4th place"
-- ordinal: "You came in {num, ordinal} place."
-- # E.g. "There is one thing", "There are one hundred things"
-- spellout: "There {num, plural, one{is {num, spellout} thing} other{are {num, spellout} things}}."
-- numbers: "{int} and {double, number, integer} are integers, but {double} is a double"
-- # Numbers can be formatted with custom patterns
-- # See https://unicode-org.github.io/icu/userguide/format_parse/numbers/skeletons.html#syntax
-- rounding: "{value, number, :: .00}"
-- @usage
-- -- Usage in Lua
-- local myMsg = core.l10n('MyMod', 'en')
-- print( myMsg('good_morning') )
-- print( myMsg('you_have_arrows', {count=5}) )
-- print( myMsg('Hello %{name}!', {name='World'}) )
-- print( myMsg('Hello {name}!', {name='World'}) )
---

@ -396,6 +396,10 @@ texture mipmap = nearest
# Show message box when screenshot is saved to a file.
notify on saved screenshot = false
# List of the preferred languages separated by comma.
# For example "de,en" means German as the first prority and English as a fallback.
preferred locales = en
[Shaders]
# Force rendering with shaders. By default, only bump-mapped objects will use shaders.
@ -1125,8 +1129,3 @@ lua debug = false
# Set the maximum number of threads used for Lua scripts.
# 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

Loading…
Cancel
Save