From b24e6433df3c3b2926568aff9c0173459e3e8eab Mon Sep 17 00:00:00 2001 From: Ekdohibs Date: Tue, 31 Jan 2017 18:05:03 +0100 Subject: Add clientside translations. --- src/util/enriched_string.cpp | 6 +- src/util/string.cpp | 194 +++++++++++++++++++++++++++++++++++++++++++ src/util/string.h | 56 +++++++++++++ 3 files changed, 253 insertions(+), 3 deletions(-) (limited to 'src/util') diff --git a/src/util/enriched_string.cpp b/src/util/enriched_string.cpp index f79e77d5a..642188a52 100644 --- a/src/util/enriched_string.cpp +++ b/src/util/enriched_string.cpp @@ -36,19 +36,19 @@ EnrichedString::EnrichedString(const std::wstring &string, EnrichedString::EnrichedString(const std::wstring &s, const SColor &color) { clear(); - addAtEnd(s, color); + addAtEnd(translate_string(s), color); } EnrichedString::EnrichedString(const wchar_t *str, const SColor &color) { clear(); - addAtEnd(std::wstring(str), color); + addAtEnd(translate_string(std::wstring(str)), color); } void EnrichedString::operator=(const wchar_t *str) { clear(); - addAtEnd(std::wstring(str), SColor(255, 255, 255, 255)); + addAtEnd(translate_string(std::wstring(str)), SColor(255, 255, 255, 255)); } void EnrichedString::addAtEnd(const std::wstring &s, const SColor &initial_color) diff --git a/src/util/string.cpp b/src/util/string.cpp index fc2a2057f..6335aeafe 100644 --- a/src/util/string.cpp +++ b/src/util/string.cpp @@ -24,6 +24,7 @@ with this program; if not, write to the Free Software Foundation, Inc., #include "hex.h" #include "../porting.h" +#include "../translation.h" #include #include @@ -743,3 +744,196 @@ void str_replace(std::string &str, char from, char to) { std::replace(str.begin(), str.end(), from, to); } + +/* Translated strings have the following format: + * \x1bT marks the beginning of a translated string + * \x1bE marks its end + * + * \x1bF marks the beginning of an argument, and \x1bE its end. + * + * Arguments are *not* translated, as they may contain escape codes. + * Thus, if you want a translated argument, it should be inside \x1bT/\x1bE tags as well. + * + * This representation is chosen so that clients ignoring escape codes will + * see untranslated strings. + * + * For instance, suppose we have a string such as "@1 Wool" with the argument "White" + * The string will be sent as "\x1bT\x1bF\x1bTWhite\x1bE\x1bE Wool\x1bE" + * To translate this string, we extract what is inside \x1bT/\x1bE tags. + * When we notice the \x1bF tag, we recursively extract what is there up to the \x1bE end tag, + * translating it as well. + * We get the argument "White", translated, and create a template string with "@1" instead of it. + * We finally get the template "@1 Wool" that was used in the beginning, which we translate + * before filling it again. + */ + +void translate_all(const std::wstring &s, size_t &i, std::wstring &res); + +void translate_string(const std::wstring &s, const std::wstring &textdomain, + size_t &i, std::wstring &res) { + std::wostringstream output; + std::vector args; + int arg_number = 1; + while (i < s.length()) { + // Not an escape sequence: just add the character. + if (s[i] != '\x1b') { + output.put(s[i]); + // The character is a literal '@'; add it twice + // so that it is not mistaken for an argument. + if (s[i] == L'@') + output.put(L'@'); + ++i; + continue; + } + + // We have an escape sequence: locate it and its data + // It is either a single character, or it begins with '(' + // and extends up to the following ')', with '\' as an escape character. + ++i; + size_t start_index = i; + size_t length; + if (i == s.length()) { + length = 0; + } else if (s[i] == L'(') { + ++i; + ++start_index; + while (i < s.length() && s[i] != L')') { + if (s[i] == L'\\') + ++i; + ++i; + } + length = i - start_index; + ++i; + if (i > s.length()) + i = s.length(); + } else { + ++i; + length = 1; + } + std::wstring escape_sequence(s, start_index, length); + + // The escape sequence is now reconstructed. + std::vector parts = split(escape_sequence, L'@'); + if (parts[0] == L"E") { + // "End of translation" escape sequence. We are done locating the string to translate. + break; + } else if (parts[0] == L"F") { + // "Start of argument" escape sequence. + // Recursively translate the argument, and add it to the argument list. + // Add an "@n" instead of the argument to the template to translate. + if (arg_number >= 10) { + errorstream << "Ignoring too many arguments to translation" << std::endl; + std::wstring arg; + translate_all(s, i, arg); + args.push_back(arg); + continue; + } + output.put(L'@'); + output << arg_number; + ++arg_number; + std::wstring arg; + translate_all(s, i, arg); + args.push_back(arg); + } else { + // This is an escape sequence *inside* the template string to translate itself. + // This should not happen, show an error message. + errorstream << "Ignoring escape sequence '" << wide_to_narrow(escape_sequence) << "' in translation" << std::endl; + } + } + + // Translate the template. + std::wstring toutput = g_translations->getTranslation(textdomain, output.str()); + + // Put back the arguments in the translated template. + std::wostringstream result; + size_t j = 0; + while (j < toutput.length()) { + // Normal character, add it to output and continue. + if (toutput[j] != L'@' || j == toutput.length() - 1) { + result.put(toutput[j]); + ++j; + continue; + } + + ++j; + // Literal escape for '@'. + if (toutput[j] == L'@') { + result.put(L'@'); + ++j; + continue; + } + + // Here we have an argument; get its index and add the translated argument to the output. + int arg_index = toutput[j] - L'1'; + ++j; + result << args[arg_index]; + } + res = result.str(); +} + +void translate_all(const std::wstring &s, size_t &i, std::wstring &res) { + std::wostringstream output; + while (i < s.length()) { + // Not an escape sequence: just add the character. + if (s[i] != '\x1b') { + output.put(s[i]); + ++i; + continue; + } + + // We have an escape sequence: locate it and its data + // It is either a single character, or it begins with '(' + // and extends up to the following ')', with '\' as an escape character. + size_t escape_start = i; + ++i; + size_t start_index = i; + size_t length; + if (i == s.length()) { + length = 0; + } else if (s[i] == L'(') { + ++i; + ++start_index; + while (i < s.length() && s[i] != L')') { + if (s[i] == L'\\') { + ++i; + } + ++i; + } + length = i - start_index; + ++i; + if (i > s.length()) + i = s.length(); + } else { + ++i; + length = 1; + } + std::wstring escape_sequence(s, start_index, length); + + // The escape sequence is now reconstructed. + std::vector parts = split(escape_sequence, L'@'); + if (parts[0] == L"E") { + // "End of argument" escape sequence. Exit. + break; + } else if (parts[0] == L"T") { + // Beginning of translated string. + std::wstring textdomain; + if (parts.size() > 1) + textdomain = parts[1]; + std::wstring translated; + translate_string(s, textdomain, i, translated); + output << translated; + } else { + // Another escape sequence, such as colors. Preserve it. + output << std::wstring(s, escape_start, i - escape_start); + } + } + + res = output.str(); +} + +std::wstring translate_string(const std::wstring &s) { + size_t i = 0; + std::wstring res; + translate_all(s, i, res); + return res; +} diff --git a/src/util/string.h b/src/util/string.h index 122262af8..2840f1192 100644 --- a/src/util/string.h +++ b/src/util/string.h @@ -203,6 +203,56 @@ inline bool str_starts_with(const std::basic_string &str, case_insensitive); } + +/** + * Check whether \p str ends with the string suffix. If \p case_insensitive + * is true then the check is case insensitve (default is false; i.e. case is + * significant). + * + * @param str + * @param suffix + * @param case_insensitive + * @return true if the str begins with suffix + */ +template +inline bool str_ends_with(const std::basic_string &str, + const std::basic_string &suffix, + bool case_insensitive = false) +{ + if (str.size() < suffix.size()) + return false; + + size_t start = str.size() - suffix.size(); + if (!case_insensitive) + return str.compare(start, suffix.size(), suffix) == 0; + + for (size_t i = 0; i < suffix.size(); ++i) + if (tolower(str[start + i]) != tolower(suffix[i])) + return false; + return true; +} + + +/** + * Check whether \p str ends with the string suffix. If \p case_insensitive + * is true then the check is case insensitve (default is false; i.e. case is + * significant). + * + * @param str + * @param suffix + * @param case_insensitive + * @return true if the str begins with suffix + */ +template +inline bool str_ends_with(const std::basic_string &str, + const T *suffix, + bool case_insensitive = false) +{ + return str_ends_with(str, std::basic_string(suffix), + case_insensitive); +} + + /** * Splits a string into its component parts separated by the character * \p delimiter. @@ -598,6 +648,12 @@ std::vector > split(const std::basic_string &s, T delim) return tokens; } +std::wstring translate_string(const std::wstring &s); + +inline std::wstring unescape_translate(const std::wstring &s) { + return unescape_enriched(translate_string(s)); +} + /** * Checks that all characters in \p to_check are a decimal digits. * -- cgit v1.2.3