Initial i18n/l10n support
This commit is contained in:
parent
c940a2bb89
commit
4726f55c3e
@ -555,7 +555,7 @@ if GetOption('ignore-updates'):
|
|||||||
|
|
||||||
|
|
||||||
#Generate list of sources to compile
|
#Generate list of sources to compile
|
||||||
sources = Glob("src/*.cpp") + Glob("src/*/*.cpp") + Glob("src/*/*/*.cpp") + Glob("data/*.cpp")
|
sources = Glob("src/*.cpp") + Glob("src/*/*.cpp") + Glob("src/*/*/*.cpp") + Glob("data/*.cpp") + Glob("data/localization/*.cpp")
|
||||||
if not GetOption('nolua') and not GetOption('renderer') and not GetOption('font'):
|
if not GetOption('nolua') and not GetOption('renderer') and not GetOption('font'):
|
||||||
sources += Glob("src/lua/socket/*.c") + Glob("src/lua/LuaCompat.c")
|
sources += Glob("src/lua/socket/*.c") + Glob("src/lua/LuaCompat.c")
|
||||||
|
|
||||||
|
10
data/localization/EN.cpp
Normal file
10
data/localization/EN.cpp
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#include "common/Localization.h"
|
||||||
|
#include "common/Internationalization.h"
|
||||||
|
|
||||||
|
Locale LocaleEN {
|
||||||
|
[]{ return String("English"); },
|
||||||
|
[]
|
||||||
|
{
|
||||||
|
using i18n::translation;
|
||||||
|
}
|
||||||
|
};
|
10
data/localization/List.h
Normal file
10
data/localization/List.h
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "common/Localization.h"
|
||||||
|
|
||||||
|
extern Locale LocaleEN;
|
||||||
|
|
||||||
|
const std::vector<Locale *> locales =
|
||||||
|
{
|
||||||
|
&LocaleEN,
|
||||||
|
};
|
@ -32,6 +32,7 @@
|
|||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
#include "localization/List.h"
|
||||||
#include "Format.h"
|
#include "Format.h"
|
||||||
#include "Misc.h"
|
#include "Misc.h"
|
||||||
|
|
||||||
@ -672,6 +673,11 @@ int main(int argc, char * argv[])
|
|||||||
else
|
else
|
||||||
ChdirToDataDirectory();
|
ChdirToDataDirectory();
|
||||||
|
|
||||||
|
String localeName = Client::Ref().GetPrefString("Locale", "");
|
||||||
|
for(Locale *locale : locales)
|
||||||
|
if(locale->GetName() == localeName)
|
||||||
|
locale->Set();
|
||||||
|
|
||||||
scale = Client::Ref().GetPrefInteger("Scale", 1);
|
scale = Client::Ref().GetPrefInteger("Scale", 1);
|
||||||
resizable = Client::Ref().GetPrefBool("Resizable", false);
|
resizable = Client::Ref().GetPrefBool("Resizable", false);
|
||||||
fullscreen = Client::Ref().GetPrefBool("Fullscreen", false);
|
fullscreen = Client::Ref().GetPrefBool("Fullscreen", false);
|
||||||
|
31
src/common/Internationalization.cpp
Normal file
31
src/common/Internationalization.cpp
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
#include "Internationalization.h"
|
||||||
|
|
||||||
|
namespace i18n
|
||||||
|
{
|
||||||
|
// These are not global variables so that it is possible to use them during
|
||||||
|
// dynamic initialization of other global variables
|
||||||
|
static std::map<LiteralPtr, CanonicalPtr> &literalCanonicalization()
|
||||||
|
{
|
||||||
|
static std::map<LiteralPtr, CanonicalPtr> literalCanonicalization{};
|
||||||
|
return literalCanonicalization;
|
||||||
|
}
|
||||||
|
|
||||||
|
static std::map<ByteString, CanonicalPtr> &canonicalization()
|
||||||
|
{
|
||||||
|
static std::map<ByteString, CanonicalPtr> canonicalization{};
|
||||||
|
return canonicalization;
|
||||||
|
}
|
||||||
|
|
||||||
|
CanonicalPtr Canonicalize(LiteralPtr str)
|
||||||
|
{
|
||||||
|
auto it = literalCanonicalization().find(str);
|
||||||
|
if(it == literalCanonicalization().end())
|
||||||
|
{
|
||||||
|
CanonicalPtr can = canonicalization().insert(std::make_pair(str, str)).first->second;
|
||||||
|
literalCanonicalization().insert(std::make_pair(str, can));
|
||||||
|
return can;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
return it->second;
|
||||||
|
}
|
||||||
|
}
|
133
src/common/Internationalization.h
Normal file
133
src/common/Internationalization.h
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
#include "String.h"
|
||||||
|
|
||||||
|
/*
|
||||||
|
We handle internationalization by maintaining a map from "key" strings, to
|
||||||
|
localized versions of those strings. The "keys" are strings in English, the
|
||||||
|
default language. At application startup this map is populated by
|
||||||
|
translations based on current settings. In theory this map could be updated
|
||||||
|
on the fly at runtime but various GUI interfaces will "cache" translations
|
||||||
|
of the strings that they use and working around this is tricky, thus we
|
||||||
|
require an application restart to change the language.
|
||||||
|
|
||||||
|
For performance reasons we rely on the assumption that we only need to
|
||||||
|
translate strings that are actual compile-time string literal constants.
|
||||||
|
Such literals are very easily equated by their pointer value: a pointer to a
|
||||||
|
string literal will always point to that literal and nothing else. The
|
||||||
|
LiteralPtr type is a pointer with this assumption.
|
||||||
|
|
||||||
|
However two instances of the same string literal might not have the same
|
||||||
|
pointer (in particular if the literal appears in different compilation
|
||||||
|
units; albeit depends on the compiler and compiler options). We thus
|
||||||
|
introduce another layer of indirection: a map from string literal pointers
|
||||||
|
to a "canonical" pointer for that string literal. Really it's just an
|
||||||
|
arbitrarily chosen literal pointer for that string. Invariant: given
|
||||||
|
CanonicalPtr c, LiteralPtr l, if !strcmp(l, c) then Canonicalize(l) == c.
|
||||||
|
|
||||||
|
Sometimes several pieces of strings are assembled into a larger string,
|
||||||
|
possibly with data inbetween, e.g.:
|
||||||
|
String::Build("Page ", page, " of ", total)
|
||||||
|
In the C world we could've gotten away with using a key with placeholders
|
||||||
|
such as "Page %d of %d" and then the translation would include the format
|
||||||
|
specifiers as well and it would all work out. Note that it is a bad idea to
|
||||||
|
introduce "Page" and "of" as separate keys as they don't make much sense on
|
||||||
|
their own. Our solution is to allow keys to be a *sequence* of English
|
||||||
|
strings that are intended to be used together, and the translation is a
|
||||||
|
sequence of the same length. In the above example our key would be
|
||||||
|
{"Page ", " of "}. We will then obtain a translation which would be a pair
|
||||||
|
of strings that could be fed into String::Build.
|
||||||
|
|
||||||
|
Sometimes the same English string is used in multiple places in different
|
||||||
|
contexts, and in a different language the different uses might require
|
||||||
|
different translations, e.g. "Login" could be a verb, or a noun. To remedy
|
||||||
|
this we can add a string to the key that would indicate the context, even
|
||||||
|
though the context part is never shown to the user, e.g.
|
||||||
|
{"Login", "Authenticate"} vs {"Login", "Username"}.
|
||||||
|
|
||||||
|
This interface boils down to a user-defined string literal operator""_i18n
|
||||||
|
that lets you write "Foo"_i18n which would look up translation for the key
|
||||||
|
"Foo" at runtime; and the function i18nMulti(...) which takes multiple keys
|
||||||
|
and returns an array of the same size with the translation of the entire
|
||||||
|
sequence.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace i18n
|
||||||
|
{
|
||||||
|
using LiteralPtr = char const *;
|
||||||
|
using CanonicalPtr = char const *;
|
||||||
|
|
||||||
|
CanonicalPtr Canonicalize(LiteralPtr str);
|
||||||
|
|
||||||
|
template<size_t n> struct TranslationMap
|
||||||
|
{
|
||||||
|
// This is not just a static field so that it is possible to use it
|
||||||
|
// during dynamic initialization of global variables
|
||||||
|
static std::map<std::array<CanonicalPtr, n>, std::array<String, n>> &Map()
|
||||||
|
{
|
||||||
|
static std::map<std::array<CanonicalPtr, n>, std::array<String, n>> map{};
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
template<size_t N> String &translation(char const (&str)[N])
|
||||||
|
{
|
||||||
|
return TranslationMap<1>::Map()[{Canonicalize(str)}][0];
|
||||||
|
}
|
||||||
|
|
||||||
|
template<size_t n> inline std::array<String, n> &multiTranslation(std::array<CanonicalPtr, n> &cans, size_t)
|
||||||
|
{
|
||||||
|
return TranslationMap<n>::Map()[cans];
|
||||||
|
}
|
||||||
|
|
||||||
|
template<size_t n, size_t N, typename... Ts> std::array<String, n> &multiTranslation(std::array<CanonicalPtr, n> &cans, size_t i, char const (&lit)[N], Ts&&... args)
|
||||||
|
{
|
||||||
|
cans[i] = Canonicalize(lit);
|
||||||
|
return multiTranslation(cans, i + 1, std::forward<Ts>(args)...);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename... Ts> std::array<String, sizeof...(Ts)> &translation(Ts&&... args)
|
||||||
|
{
|
||||||
|
std::array<CanonicalPtr, sizeof...(Ts)> strs;
|
||||||
|
return multiTranslation(strs, 0, std::forward<Ts>(args)...);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<size_t n> inline std::array<String, n> getTranslation(std::array<LiteralPtr, n> strs)
|
||||||
|
{
|
||||||
|
std::array<CanonicalPtr, n> cans;
|
||||||
|
for(size_t i = 0; i < n; i++)
|
||||||
|
cans[i] = Canonicalize(strs[i]);
|
||||||
|
auto it = TranslationMap<n>::Map().find(cans);
|
||||||
|
if(it != TranslationMap<n>::Map().end())
|
||||||
|
return it->second;
|
||||||
|
std::array<String, n> defs;
|
||||||
|
for(size_t i = 0; i < n; i++)
|
||||||
|
defs[i] = ByteString(strs[i]).FromAscii();
|
||||||
|
return defs;
|
||||||
|
}
|
||||||
|
|
||||||
|
template<size_t n> inline std::array<String, n> getMultiTranslation(std::array<LiteralPtr, n> &strs, size_t)
|
||||||
|
{
|
||||||
|
return getTranslation<n>(strs);
|
||||||
|
}
|
||||||
|
|
||||||
|
template<size_t n, size_t N, typename... Ts> std::array<String, n> getMultiTranslation(std::array<LiteralPtr, n> &strs, size_t i, char const (&lit)[N], Ts&&... args)
|
||||||
|
{
|
||||||
|
strs[i] = lit;
|
||||||
|
return getMultiTranslation(strs, i + 1, std::forward<Ts>(args)...);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline String operator""_i18n(char const *str, size_t)
|
||||||
|
{
|
||||||
|
return i18n::getTranslation<1>({str})[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
template<typename... Ts> std::array<String, sizeof...(Ts)> i18nMulti(Ts&&... args)
|
||||||
|
{
|
||||||
|
std::array<i18n::LiteralPtr, sizeof...(Ts)> strs;
|
||||||
|
return i18n::getMultiTranslation(strs, 0, std::forward<Ts>(args)...);
|
||||||
|
}
|
16
src/common/Localization.h
Normal file
16
src/common/Localization.h
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <functional>
|
||||||
|
|
||||||
|
#include "String.h"
|
||||||
|
|
||||||
|
class Locale
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
// The name of the language this locale is for, readable in both the native
|
||||||
|
// language and in English;
|
||||||
|
std::function<String()> GetName;
|
||||||
|
|
||||||
|
// Populate the translations map.
|
||||||
|
std::function<void()> Set;
|
||||||
|
};
|
@ -683,3 +683,4 @@ template<typename... Ts> String String::Build(Ts&&... args)
|
|||||||
}
|
}
|
||||||
|
|
||||||
#include "common/Format.h"
|
#include "common/Format.h"
|
||||||
|
#include "common/Internationalization.h"
|
||||||
|
Reference in New Issue
Block a user