Add clientside translations.

This commit is contained in:
Ekdohibs 2017-01-31 18:05:03 +01:00
parent b28af0ed07
commit b24e6433df
21 changed files with 629 additions and 46 deletions

@ -680,6 +680,44 @@ function core.strip_colors(str)
return (str:gsub(ESCAPE_CHAR .. "%([bc]@[^)]+%)", ""))
end
function core.translate(textdomain, str, ...)
local start_seq
if textdomain == "" then
start_seq = ESCAPE_CHAR .. "T"
else
start_seq = ESCAPE_CHAR .. "(T@" .. textdomain .. ")"
end
local arg = {n=select('#', ...), ...}
local end_seq = ESCAPE_CHAR .. "E"
local arg_index = 1
local translated = str:gsub("@(.)", function(matched)
local c = string.byte(matched)
if string.byte("1") <= c and c <= string.byte("9") then
local a = c - string.byte("0")
if a ~= arg_index then
error("Escape sequences in string given to core.translate " ..
"are not in the correct order: got @" .. matched ..
"but expected @" .. tostring(arg_index))
end
if a > arg.n then
error("Not enough arguments provided to core.translate")
end
arg_index = arg_index + 1
return ESCAPE_CHAR .. "F" .. arg[a] .. ESCAPE_CHAR .. "E"
else
return matched
end
end)
if arg_index < arg.n + 1 then
error("Too many arguments provided to core.translate")
end
return start_seq .. translated .. end_seq
end
function core.get_translator(textdomain)
return function(str, ...) return core.translate(textdomain or "", str, ...) end
end
--------------------------------------------------------------------------------
-- Returns the exact coordinate of a pointed surface
--------------------------------------------------------------------------------

@ -139,6 +139,7 @@ Mod directory structure
| | `-- modname_something_else.png
| |-- sounds
| |-- media
| |-- locale
| `-- <custom data>
`-- another
@ -182,6 +183,9 @@ Models for entities or meshnodes.
Media files (textures, sounds, whatever) that will be transferred to the
client and will be available for use by the mod.
### `locale`
Translation files for the clients. (See `Translations`)
Naming convention for registered textual names
----------------------------------------------
Registered names should generally be in this format:
@ -2152,6 +2156,68 @@ Helper functions
* `minetest.pointed_thing_to_face_pos(placer, pointed_thing)`: returns a position
* returns the exact position on the surface of a pointed node
Translations
------------
Texts can be translated client-side with the help of `minetest.translate` and translation files.
### Translating a string
Two functions are provided to translate strings: `minetest.translate` and `minetest.get_translator`.
* `minetest.get_translator(textdomain)` is a simple wrapper around `minetest.translate`, and
`minetest.get_translator(textdomain)(str, ...)` is equivalent to `minetest.translate(textdomain, str, ...)`.
It is intended to be used in the following way, so that it avoids verbose repetitions of `minetest.translate`:
local S = minetest.get_translator(textdomain)
S(str, ...)
As an extra commodity, if `textdomain` is nil, it is assumed to be "" instead.
* `minetest.translate(textdomain, str, ...)` translates the string `str` with the given `textdomain`
for disambiguation. The textdomain must match the textdomain specified in the translation file in order
to get the string translated. This can be used so that a string is translated differently in different contexts.
It is advised to use the name of the mod as textdomain whenever possible, to avoid clashes with other mods.
This function must be given a number of arguments equal to the number of arguments the translated string expects.
Arguments are literal strings -- they will not be translated, so if you want them to be, they need to come as
outputs of `minetest.translate` as well.
For instance, suppose we want to translate "@1 Wool" with "@1" being replaced by the translation of "Red".
We can do the following:
local S = minetest.get_translator()
S("@1 Wool", S("Red"))
This will be displayed as "Red Wool" on old clients and on clients that do not have localization enabled.
However, if we have for instance a translation file named `wool.fr.tr` containing the following:
@1 Wool=Laine @1
Red=Rouge
this will be displayed as "Laine Rouge" on clients with a French locale.
### Translation file format
A translation file has the suffix `.[lang].tr`, where `[lang]` is the language it corresponds to.
The file should be a text file, with the following format:
* Lines beginning with `# textdomain:` (the space is significant) can be used to specify the text
domain of all following translations in the file.
* All other empty lines or lines beginning with `#` are ignored.
* Other lines should be in the format `original=translated`. Both `original` and `translated` can
contain escape sequences beginning with `@` to insert arguments, literal `@`, `=` or newline
(See ### Escapes below). There must be no extraneous whitespace around the `=` or at the beginning
or the end of the line.
### Escapes
Strings that need to be translated can contain several escapes, preceded by `@`.
* `@@` acts as a literal `@`.
* `@n`, where `n` is a digit between 1 and 9, is an argument for the translated string that will be inlined
when translation. Due to how translations are implemented, the original translation string **must** have
its arguments in increasing order, without gaps or repetitions, starting from 1.
* `@=` acts as a literal `=`. It is not required in strings given to `minetest.translate`, but is in translation
files to avoid begin confused with the `=` separating the original from the translation.
* `@\n` (where the `\n` is a literal newline) acts as a literal newline. As with `@=`, this escape is not required
in strings given to `minetest.translate`, but is in translation files.
`minetest` namespace reference
------------------------------

@ -450,6 +450,7 @@ set(common_SRCS
terminal_chat_console.cpp
tileanimation.cpp
tool.cpp
translation.cpp
treegen.cpp
version.cpp
voxel.cpp

@ -620,10 +620,11 @@ void Camera::drawNametags()
f32 transformed_pos[4] = { pos.X, pos.Y, pos.Z, 1.0f };
trans.multiplyWith1x4Matrix(transformed_pos);
if (transformed_pos[3] > 0) {
std::string nametag_colorless = unescape_enriched(nametag->nametag_text);
std::wstring nametag_colorless =
unescape_translate(utf8_to_wide(nametag->nametag_text));
core::dimension2d<u32> textsize =
g_fontengine->getFont()->getDimension(
utf8_to_wide(nametag_colorless).c_str());
nametag_colorless.c_str());
f32 zDiv = transformed_pos[3] == 0.0f ? 1.0f :
core::reciprocal(transformed_pos[3]);
v2u32 screensize = RenderingEngine::get_video_driver()->getScreenSize();
@ -633,8 +634,9 @@ void Camera::drawNametags()
screen_pos.Y = screensize.Y *
(0.5 - transformed_pos[1] * zDiv * 0.5) - textsize.Height / 2;
core::rect<s32> size(0, 0, textsize.Width, textsize.Height);
g_fontengine->getFont()->draw(utf8_to_wide(nametag->nametag_text).c_str(),
size + screen_pos, nametag->nametag_color);
g_fontengine->getFont()->draw(
translate_string(utf8_to_wide(nametag->nametag_text)).c_str(),
size + screen_pos, nametag->nametag_color);
}
}
}

@ -650,6 +650,7 @@ ChatBackend::ChatBackend():
void ChatBackend::addMessage(std::wstring name, std::wstring text)
{
// Note: A message may consist of multiple lines, for example the MOTD.
text = translate_string(text);
WStrfnd fnd(text);
while (!fnd.at_end())
{

@ -51,6 +51,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "script/scripting_client.h"
#include "game.h"
#include "chatmessage.h"
#include "translation.h"
extern gui::IGUIEnvironment* guienv;
@ -684,8 +685,19 @@ bool Client::loadMedia(const std::string &data, const std::string &filename)
return true;
}
errorstream<<"Client: Don't know how to load file \""
<<filename<<"\""<<std::endl;
const char *translate_ext[] = {
".tr", NULL
};
name = removeStringEnd(filename, translate_ext);
if (!name.empty()) {
verbosestream << "Client: Loading translation: "
<< "\"" << filename << "\"" << std::endl;
g_translations->loadTranslation(data);
return true;
}
errorstream << "Client: Don't know how to load file \""
<< filename << "\"" << std::endl;
return false;
}

@ -58,6 +58,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "sky.h"
#include "subgame.h"
#include "tool.h"
#include "translation.h"
#include "util/basic_macros.h"
#include "util/directiontables.h"
#include "util/pointedthing.h"
@ -242,7 +243,7 @@ void update_profiler_gui(gui::IGUIStaticText *guitext_profiler, FontEngine *fe,
std::ostringstream os(std::ios_base::binary);
g_profiler->printPage(os, show_profiler, show_profiler_max);
std::wstring text = utf8_to_wide(os.str());
std::wstring text = translate_string(utf8_to_wide(os.str()));
setStaticText(guitext_profiler, text.c_str());
guitext_profiler->setVisible(true);
@ -1619,6 +1620,8 @@ bool Game::startup(bool *kill,
m_invert_mouse = g_settings->getBool("invert_mouse");
m_first_loop_after_window_activation = true;
g_translations->clear();
if (!init(map_dir, address, port, gamespec))
return false;
@ -3781,7 +3784,7 @@ void Game::handlePointingAtNode(const PointedThing &pointed,
NodeMetadata *meta = map.getNodeMetadata(nodepos);
if (meta) {
infotext = unescape_enriched(utf8_to_wide(meta->getString("infotext")));
infotext = unescape_translate(utf8_to_wide(meta->getString("infotext")));
} else {
MapNode n = map.getNodeNoEx(nodepos);
@ -3858,15 +3861,14 @@ void Game::handlePointingAtNode(const PointedThing &pointed,
void Game::handlePointingAtObject(const PointedThing &pointed, const ItemStack &playeritem,
const v3f &player_position, bool show_debug)
{
infotext = unescape_enriched(
infotext = unescape_translate(
utf8_to_wide(runData.selected_object->infoText()));
if (show_debug) {
if (!infotext.empty()) {
infotext += L"\n";
}
infotext += unescape_enriched(utf8_to_wide(
runData.selected_object->debugInfoText()));
infotext += utf8_to_wide(runData.selected_object->debugInfoText());
}
if (isLeftPressed()) {
@ -4399,7 +4401,7 @@ void Game::updateGui(const RunStats &stats, f32 dtime, const CameraOrientation &
guitext3->setRelativePosition(rect);
}
setStaticText(guitext_info, infotext.c_str());
setStaticText(guitext_info, translate_string(infotext).c_str());
guitext_info->setVisible(flags.show_hud && g_menumgr.menuCount() == 0);
float statustext_time_max = 1.5;
@ -4413,7 +4415,7 @@ void Game::updateGui(const RunStats &stats, f32 dtime, const CameraOrientation &
}
}
setStaticText(guitext_status, m_statustext.c_str());
setStaticText(guitext_status, translate_string(m_statustext).c_str());
guitext_status->setVisible(!m_statustext.empty());
if (!m_statustext.empty()) {

@ -547,7 +547,7 @@ bool GUIEngine::downloadFile(const std::string &url, const std::string &target)
/******************************************************************************/
void GUIEngine::setTopleftText(const std::string &text)
{
m_toplefttext = utf8_to_wide(text);
m_toplefttext = translate_string(utf8_to_wide(text));
updateTopLeftTextSize();
}

@ -398,7 +398,7 @@ void GUIFormSpecMenu::parseCheckbox(parserData* data, const std::string &element
if (selected == "true")
fselected = true;
std::wstring wlabel = utf8_to_wide(unescape_string(label));
std::wstring wlabel = translate_string(utf8_to_wide(unescape_string(label)));
core::rect<s32> rect = core::rect<s32>(
pos.X, pos.Y + ((imgsize.Y/2) - m_btn_height),
@ -594,7 +594,7 @@ void GUIFormSpecMenu::parseButton(parserData* data, const std::string &element,
if(!data->explicit_size)
warningstream<<"invalid use of button without a size[] element"<<std::endl;
std::wstring wlabel = utf8_to_wide(unescape_string(label));
std::wstring wlabel = translate_string(utf8_to_wide(unescape_string(label)));
FieldSpec spec(
name,
@ -728,7 +728,7 @@ void GUIFormSpecMenu::parseTable(parserData* data, const std::string &element)
spec.ftype = f_Table;
for (std::string &item : items) {
item = unescape_enriched(unescape_string(item));
item = wide_to_utf8(unescape_translate(utf8_to_wide(unescape_string(item))));
}
//now really show table
@ -799,7 +799,7 @@ void GUIFormSpecMenu::parseTextList(parserData* data, const std::string &element
spec.ftype = f_Table;
for (std::string &item : items) {
item = unescape_enriched(unescape_string(item));
item = wide_to_utf8(unescape_translate(utf8_to_wide(unescape_string(item))));
}
//now really show list
@ -869,7 +869,7 @@ void GUIFormSpecMenu::parseDropDown(parserData* data, const std::string &element
}
for (const std::string &item : items) {
e->addItem(unescape_enriched(unescape_string(
e->addItem(unescape_translate(unescape_string(
utf8_to_wide(item))).c_str());
}
@ -927,7 +927,7 @@ void GUIFormSpecMenu::parsePwdField(parserData* data, const std::string &element
core::rect<s32> rect = core::rect<s32>(pos.X, pos.Y, pos.X+geom.X, pos.Y+geom.Y);
std::wstring wlabel = utf8_to_wide(unescape_string(label));
std::wstring wlabel = translate_string(utf8_to_wide(unescape_string(label)));
FieldSpec spec(
name,
@ -999,7 +999,7 @@ void GUIFormSpecMenu::parseSimpleField(parserData* data,
default_val = m_form_src->resolveText(default_val);
std::wstring wlabel = utf8_to_wide(unescape_string(label));
std::wstring wlabel = translate_string(utf8_to_wide(unescape_string(label)));
FieldSpec spec(
name,
@ -1099,7 +1099,7 @@ void GUIFormSpecMenu::parseTextArea(parserData* data, std::vector<std::string>&
default_val = m_form_src->resolveText(default_val);
std::wstring wlabel = utf8_to_wide(unescape_string(label));
std::wstring wlabel = translate_string(utf8_to_wide(unescape_string(label)));
FieldSpec spec(
name,
@ -1249,7 +1249,7 @@ void GUIFormSpecMenu::parseVertLabel(parserData* data, const std::string &elemen
((parts.size() > 2) && (m_formspec_version > FORMSPEC_API_VERSION)))
{
std::vector<std::string> v_pos = split(parts[0],',');
std::wstring text = unescape_enriched(
std::wstring text = unescape_translate(
unescape_string(utf8_to_wide(parts[1])));
MY_CHECKPOS("vertlabel",1);
@ -1436,7 +1436,7 @@ void GUIFormSpecMenu::parseTabHeader(parserData* data, const std::string &elemen
e->setNotClipped(true);
for (const std::string &button : buttons) {
e->addTab(unescape_enriched(unescape_string(
e->addTab(unescape_translate(unescape_string(
utf8_to_wide(button))).c_str(), -1);
}
@ -1495,7 +1495,7 @@ void GUIFormSpecMenu::parseItemImageButton(parserData* data, const std::string &
item.deSerialize(item_name, idef);
m_tooltips[name] =
TooltipSpec(item.getDefinition(idef).description,
TooltipSpec(utf8_to_wide(item.getDefinition(idef).description),
m_default_tooltip_bgcolor,
m_default_tooltip_color);
@ -1613,7 +1613,7 @@ void GUIFormSpecMenu::parseTooltip(parserData* data, const std::string &element)
std::vector<std::string> parts = split(element,';');
if (parts.size() == 2) {
std::string name = parts[0];
m_tooltips[name] = TooltipSpec(unescape_string(parts[1]),
m_tooltips[name] = TooltipSpec(utf8_to_wide(unescape_string(parts[1])),
m_default_tooltip_bgcolor, m_default_tooltip_color);
return;
}
@ -1622,7 +1622,7 @@ void GUIFormSpecMenu::parseTooltip(parserData* data, const std::string &element)
std::string name = parts[0];
video::SColor tmp_color1, tmp_color2;
if ( parseColorString(parts[2], tmp_color1, false) && parseColorString(parts[3], tmp_color2, false) ) {
m_tooltips[name] = TooltipSpec(unescape_string(parts[1]),
m_tooltips[name] = TooltipSpec(utf8_to_wide(unescape_string(parts[1])),
tmp_color1, tmp_color2);
return;
}
@ -2632,14 +2632,15 @@ void GUIFormSpecMenu::drawMenu()
void GUIFormSpecMenu::showTooltip(const std::wstring &text,
const irr::video::SColor &color, const irr::video::SColor &bgcolor)
{
const std::wstring ntext = translate_string(text);
m_tooltip_element->setOverrideColor(color);
m_tooltip_element->setBackgroundColor(bgcolor);
setStaticText(m_tooltip_element, text.c_str());
setStaticText(m_tooltip_element, ntext.c_str());
// Tooltip size and offset
s32 tooltip_width = m_tooltip_element->getTextWidth() + m_btn_height;
#if (IRRLICHT_VERSION_MAJOR <= 1 && IRRLICHT_VERSION_MINOR <= 8 && IRRLICHT_VERSION_REVISION < 2) || USE_FREETYPE == 1
std::vector<std::wstring> text_rows = str_split(text, L'\n');
std::vector<std::wstring> text_rows = str_split(ntext, L'\n');
s32 tooltip_height = m_tooltip_element->getTextHeight() * text_rows.size() + 5;
#else
s32 tooltip_height = m_tooltip_element->getTextHeight() + 5;

@ -203,7 +203,7 @@ class GUIFormSpecMenu : public GUIModalMenu
const std::wstring &default_text, int id) :
fname(name),
flabel(label),
fdefault(unescape_enriched(default_text)),
fdefault(unescape_enriched(translate_string(default_text))),
fid(id),
send(false),
ftype(f_Unknown),
@ -237,10 +237,9 @@ class GUIFormSpecMenu : public GUIModalMenu
struct TooltipSpec
{
TooltipSpec() = default;
TooltipSpec(const std::string &a_tooltip, irr::video::SColor a_bgcolor,
TooltipSpec(const std::wstring &a_tooltip, irr::video::SColor a_bgcolor,
irr::video::SColor a_color):
tooltip(utf8_to_wide(a_tooltip)),
tooltip(translate_string(a_tooltip)),
bgcolor(a_bgcolor),
color(a_color)
{

@ -317,7 +317,7 @@ void Hud::drawLuaElements(const v3s16 &camera_offset)
(e->number >> 8) & 0xFF,
(e->number >> 0) & 0xFF);
core::rect<s32> size(0, 0, e->scale.X, text_height * e->scale.Y);
std::wstring text = unescape_enriched(utf8_to_wide(e->text));
std::wstring text = unescape_translate(utf8_to_wide(e->text));
core::dimension2d<u32> textsize = font->getDimension(text.c_str());
v2s32 offset((e->align.X - 1.0) * (textsize.Width / 2),
(e->align.Y - 1.0) * (textsize.Height / 2));
@ -354,11 +354,11 @@ void Hud::drawLuaElements(const v3s16 &camera_offset)
(e->number >> 8) & 0xFF,
(e->number >> 0) & 0xFF);
core::rect<s32> size(0, 0, 200, 2 * text_height);
std::wstring text = unescape_enriched(utf8_to_wide(e->name));
std::wstring text = unescape_translate(utf8_to_wide(e->name));
font->draw(text.c_str(), size + pos, color);
std::ostringstream os;
os << distance << e->text;
text = unescape_enriched(utf8_to_wide(os.str()));
text = unescape_translate(utf8_to_wide(os.str()));
pos.Y += text_height;
font->draw(text.c_str(), size + pos, color);
break; }

@ -36,6 +36,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "util/serialize.h"
#include "util/srp.h"
#include "tileanimation.h"
#include "gettext.h"
void Client::handleCommand_Deprecated(NetworkPacket* pkt)
{
@ -123,7 +124,12 @@ void Client::handleCommand_AuthAccept(NetworkPacket* pkt)
<< m_recommended_send_interval<<std::endl;
// Reply to server
NetworkPacket resp_pkt(TOSERVER_INIT2, 0);
std::string lang = gettext("LANG_CODE");
if (lang == "LANG_CODE")
lang = "";
NetworkPacket resp_pkt(TOSERVER_INIT2, sizeof(u16) + lang.size());
resp_pkt << lang;
Send(&resp_pkt);
m_state = LC_Init;

@ -612,6 +612,9 @@ void Server::handleCommand_Init2(NetworkPacket* pkt)
m_clients.event(pkt->getPeerId(), CSE_GotInit2);
u16 protocol_version = m_clients.getProtocolVersion(pkt->getPeerId());
std::string lang;
if (pkt->getSize() > 0)
*pkt >> lang;
/*
Send some initialization data
@ -632,7 +635,7 @@ void Server::handleCommand_Init2(NetworkPacket* pkt)
m_clients.event(pkt->getPeerId(), CSE_SetDefinitionsSent);
// Send media announcement
sendMediaAnnouncement(pkt->getPeerId());
sendMediaAnnouncement(pkt->getPeerId(), lang);
// Send detached inventories
sendDetachedInventories(pkt->getPeerId());

@ -2293,6 +2293,7 @@ void Server::fillMediaCache()
paths.push_back(mod.path + DIR_DELIM + "sounds");
paths.push_back(mod.path + DIR_DELIM + "media");
paths.push_back(mod.path + DIR_DELIM + "models");
paths.push_back(mod.path + DIR_DELIM + "locale");
}
paths.push_back(porting::path_user + DIR_DELIM + "textures" + DIR_DELIM + "server");
@ -2315,6 +2316,8 @@ void Server::fillMediaCache()
".pcx", ".ppm", ".psd", ".wal", ".rgb",
".ogg",
".x", ".b3d", ".md2", ".obj",
// Custom translation file format
".tr",
NULL
};
if (removeStringEnd(filename, supported_ext).empty()){
@ -2374,7 +2377,7 @@ void Server::fillMediaCache()
}
}
void Server::sendMediaAnnouncement(u16 peer_id)
void Server::sendMediaAnnouncement(u16 peer_id, const std::string &lang_code)
{
DSTACK(FUNCTION_NAME);
@ -2382,12 +2385,22 @@ void Server::sendMediaAnnouncement(u16 peer_id)
<< std::endl;
// Make packet
std::ostringstream os(std::ios_base::binary);
NetworkPacket pkt(TOCLIENT_ANNOUNCE_MEDIA, 0, peer_id);
pkt << (u16) m_media.size();
u16 media_sent = 0;
std::string lang_suffix;
lang_suffix.append(".").append(lang_code).append(".tr");
for (const auto &i : m_media) {
if (str_ends_with(i.first, ".tr") && !str_ends_with(i.first, lang_suffix))
continue;
media_sent++;
}
pkt << media_sent;
for (const auto &i : m_media) {
if (str_ends_with(i.first, ".tr") && !str_ends_with(i.first, lang_suffix))
continue;
pkt << i.first << i.second.sha1_digest;
}

@ -405,7 +405,7 @@ private:
void SendBlocks(float dtime);
void fillMediaCache();
void sendMediaAnnouncement(u16 peer_id);
void sendMediaAnnouncement(u16 peer_id, const std::string &lang_code);
void sendRequestedMedia(u16 peer_id,
const std::vector<std::string> &tosend);

146
src/translation.cpp Normal file

@ -0,0 +1,146 @@
/*
Minetest
Copyright (C) 2017 Nore, Nathanaël Courant <nore@mesecons.net>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#include "translation.h"
#include "log.h"
#include "util/string.h"
static Translations main_translations;
Translations *g_translations = &main_translations;
Translations::~Translations()
{
clear();
}
void Translations::clear()
{
m_translations.clear();
}
const std::wstring &Translations::getTranslation(
const std::wstring &textdomain, const std::wstring &s)
{
std::wstring key = textdomain + L"|" + s;
try {
return m_translations.at(key);
} catch (std::out_of_range) {
warningstream << "Translations: can't find translation for string \""
<< wide_to_utf8(s) << "\" in textdomain \""
<< wide_to_utf8(textdomain) << "\"" << std::endl;
// Silence that warning in the future
m_translations[key] = s;
return s;
}
}
void Translations::loadTranslation(const std::string &data)
{
std::istringstream is(data);
std::wstring textdomain;
std::string line;
while (is.good()) {
std::getline(is, line);
if (str_starts_with(line, "# textdomain:")) {
textdomain = utf8_to_wide(trim(str_split(line, ':')[1]));
}
if (line.empty() || line[0] == '#')
continue;
std::wstring wline = utf8_to_wide(line);
if (wline.empty())
continue;
// Read line
// '=' marks the key-value pair, but may be escaped by an '@'.
// '\n' may also be escaped by '@'.
// All other escapes are preserved.
size_t i = 0;
std::wostringstream word1, word2;
while (i < wline.length() && wline[i] != L'=') {
if (wline[i] == L'@') {
if (i + 1 < wline.length()) {
if (wline[i + 1] == L'=') {
word1.put(L'=');
} else {
word1.put(L'@');
word1.put(wline[i + 1]);
}
i += 2;
} else {
// End of line, go to the next one.
word1.put(L'\n');
if (!is.good()) {
break;
}
i = 0;
std::getline(is, line);
wline = utf8_to_wide(line);
}
} else {
word1.put(wline[i]);
i++;
}
}
if (i == wline.length()) {
errorstream << "Malformed translation line \"" << line << "\""
<< std::endl;
continue;
}
i++;
while (i < wline.length()) {
if (wline[i] == L'@') {
if (i + 1 < wline.length()) {
if (wline[i + 1] == L'=') {
word2.put(L'=');
} else {
word2.put(L'@');
word2.put(wline[i + 1]);
}
i += 2;
} else {
// End of line, go to the next one.
word2.put(L'\n');
if (!is.good()) {
break;
}
i = 0;
std::getline(is, line);
wline = utf8_to_wide(line);
}
} else {
word2.put(wline[i]);
i++;
}
}
std::wstring oword1 = word1.str(), oword2 = word2.str();
if (oword2.empty()) {
oword2 = oword1;
errorstream << "Ignoring empty translation for \""
<< wide_to_utf8(oword1) << "\"" << std::endl;
}
m_translations[textdomain + L"|" + oword1] = oword2;
}
}

42
src/translation.h Normal file

@ -0,0 +1,42 @@
/*
Minetest
Copyright (C) 2017 Nore, Nathanaël Courant <nore@mesecons.net>
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as published by
the Free Software Foundation; either version 2.1 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Lesser General Public License for more details.
You should have received a copy of the GNU Lesser General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*/
#pragma once
#include <unordered_map>
#include <string>
class Translations;
extern Translations *g_translations;
class Translations
{
public:
Translations() = default;
~Translations();
void loadTranslation(const std::string &data);
void clear();
const std::wstring &getTranslation(
const std::wstring &textdomain, const std::wstring &s);
private:
std::unordered_map<std::wstring, std::wstring> m_translations;
};

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

@ -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 <algorithm>
#include <sstream>
@ -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<std::wstring> 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<std::wstring> 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<std::wstring> 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;
}

@ -203,6 +203,56 @@ inline bool str_starts_with(const std::basic_string<T> &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 <typename T>
inline bool str_ends_with(const std::basic_string<T> &str,
const std::basic_string<T> &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 <typename T>
inline bool str_ends_with(const std::basic_string<T> &str,
const T *suffix,
bool case_insensitive = false)
{
return str_ends_with(str, std::basic_string<T>(suffix),
case_insensitive);
}
/**
* Splits a string into its component parts separated by the character
* \p delimiter.
@ -598,6 +648,12 @@ std::vector<std::basic_string<T> > split(const std::basic_string<T> &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.
*

@ -325,6 +325,7 @@ src/tileanimation.cpp
src/tool.cpp
src/tool.h
src/touchscreengui.cpp
src/translation.cpp
src/treegen.cpp
src/treegen.h
src/unittest/test_areastore.cpp