diff --git a/SConscript b/SConscript index 18abb9d39..4409c18d3 100644 --- a/SConscript +++ b/SConscript @@ -555,7 +555,7 @@ if GetOption('ignore-updates'): #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'): sources += Glob("src/lua/socket/*.c") + Glob("src/lua/LuaCompat.c") diff --git a/data/localization/EN.cpp b/data/localization/EN.cpp new file mode 100644 index 000000000..6090a6fa0 --- /dev/null +++ b/data/localization/EN.cpp @@ -0,0 +1,10 @@ +#include "common/Localization.h" +#include "common/Internationalization.h" + +Locale LocaleEN { + []{ return String("English"); }, + [] + { + using i18n::translation; + } +}; diff --git a/data/localization/List.h b/data/localization/List.h new file mode 100644 index 000000000..03358f3b8 --- /dev/null +++ b/data/localization/List.h @@ -0,0 +1,10 @@ +#pragma once + +#include "common/Localization.h" + +extern Locale LocaleEN; + +const std::vector locales = +{ + &LocaleEN, +}; diff --git a/src/PowderToySDL.cpp b/src/PowderToySDL.cpp index 9807e4aa2..4e7926bb2 100644 --- a/src/PowderToySDL.cpp +++ b/src/PowderToySDL.cpp @@ -32,6 +32,7 @@ #include #endif +#include "localization/List.h" #include "Format.h" #include "Misc.h" @@ -672,6 +673,11 @@ int main(int argc, char * argv[]) else ChdirToDataDirectory(); + String localeName = Client::Ref().GetPrefString("Locale", ""); + for(Locale *locale : locales) + if(locale->GetName() == localeName) + locale->Set(); + scale = Client::Ref().GetPrefInteger("Scale", 1); resizable = Client::Ref().GetPrefBool("Resizable", false); fullscreen = Client::Ref().GetPrefBool("Fullscreen", false); diff --git a/src/common/Internationalization.cpp b/src/common/Internationalization.cpp new file mode 100644 index 000000000..ed7860174 --- /dev/null +++ b/src/common/Internationalization.cpp @@ -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 &literalCanonicalization() + { + static std::map literalCanonicalization{}; + return literalCanonicalization; + } + + static std::map &canonicalization() + { + static std::map 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; + } +} diff --git a/src/common/Internationalization.h b/src/common/Internationalization.h new file mode 100644 index 000000000..77e364f7b --- /dev/null +++ b/src/common/Internationalization.h @@ -0,0 +1,133 @@ +#pragma once + +#include +#include + +#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 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> &Map() + { + static std::map, std::array> map{}; + return map; + } + }; + + template String &translation(char const (&str)[N]) + { + return TranslationMap<1>::Map()[{Canonicalize(str)}][0]; + } + + template inline std::array &multiTranslation(std::array &cans, size_t) + { + return TranslationMap::Map()[cans]; + } + + template std::array &multiTranslation(std::array &cans, size_t i, char const (&lit)[N], Ts&&... args) + { + cans[i] = Canonicalize(lit); + return multiTranslation(cans, i + 1, std::forward(args)...); + } + + template std::array &translation(Ts&&... args) + { + std::array strs; + return multiTranslation(strs, 0, std::forward(args)...); + } + + template inline std::array getTranslation(std::array strs) + { + std::array cans; + for(size_t i = 0; i < n; i++) + cans[i] = Canonicalize(strs[i]); + auto it = TranslationMap::Map().find(cans); + if(it != TranslationMap::Map().end()) + return it->second; + std::array defs; + for(size_t i = 0; i < n; i++) + defs[i] = ByteString(strs[i]).FromAscii(); + return defs; + } + + template inline std::array getMultiTranslation(std::array &strs, size_t) + { + return getTranslation(strs); + } + + template std::array getMultiTranslation(std::array &strs, size_t i, char const (&lit)[N], Ts&&... args) + { + strs[i] = lit; + return getMultiTranslation(strs, i + 1, std::forward(args)...); + } +} + +inline String operator""_i18n(char const *str, size_t) +{ + return i18n::getTranslation<1>({str})[0]; +} + +template std::array i18nMulti(Ts&&... args) +{ + std::array strs; + return i18n::getMultiTranslation(strs, 0, std::forward(args)...); +} diff --git a/src/common/Localization.h b/src/common/Localization.h new file mode 100644 index 000000000..6b1e0bf0c --- /dev/null +++ b/src/common/Localization.h @@ -0,0 +1,16 @@ +#pragma once + +#include + +#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 GetName; + + // Populate the translations map. + std::function Set; +}; diff --git a/src/common/String.h b/src/common/String.h index d0eee7507..d39287fee 100644 --- a/src/common/String.h +++ b/src/common/String.h @@ -683,3 +683,4 @@ template String String::Build(Ts&&... args) } #include "common/Format.h" +#include "common/Internationalization.h"