Port of Old MT commit b24e6433df
From: Ekdohibs <nathanael.courant@laposte.net>
Date: Tue, 31 Jan 2017 18:05:03 +0100
master
@@ -631,6 +631,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 | |||
-------------------------------------------------------------------------------- | |||
@@ -152,6 +152,7 @@ Mod directory structure | |||
| | `-- modname_something_else.png | |||
| |-- sounds | |||
| |-- media | |||
| |-- locale | |||
| `-- <custom data> | |||
`-- another | |||
@@ -195,6 +196,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: | |||
@@ -2321,6 +2325,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 | |||
------------------------------ | |||
@@ -461,6 +461,7 @@ set(common_SRCS | |||
terminal_chat_console.cpp | |||
tileanimation.cpp | |||
tool.cpp | |||
translation.cpp | |||
treegen.cpp | |||
version.cpp | |||
voxel.cpp | |||
@@ -623,10 +623,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(); | |||
@@ -636,8 +637,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); | |||
} | |||
} | |||
} | |||
@@ -666,6 +666,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()) | |||
{ | |||
@@ -50,6 +50,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; | |||
@@ -693,8 +694,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" | |||
@@ -252,7 +253,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); | |||
@@ -1650,6 +1651,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; | |||
@@ -3868,7 +3871,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); | |||
@@ -3945,15 +3948,15 @@ 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 != L"") { | |||
infotext += L"\n"; | |||
} | |||
infotext += unescape_enriched(utf8_to_wide( | |||
runData.selected_object->debugInfoText())); | |||
infotext += utf8_to_wide (runData.selected_object->debugInfoText()); | |||
} | |||
if (isLeftPressed()) { | |||
@@ -4490,7 +4493,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; | |||
@@ -4504,7 +4507,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()) { | |||
@@ -566,7 +566,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(); | |||
} | |||
@@ -401,7 +401,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), | |||
@@ -595,7 +595,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, | |||
@@ -729,7 +729,7 @@ void GUIFormSpecMenu::parseTable(parserData* data, const std::string &element) | |||
spec.ftype = f_Table; | |||
for (unsigned int i = 0; i < items.size(); ++i) { | |||
items[i] = unescape_enriched(unescape_string(items[i])); | |||
items[i] = wide_to_utf8(unescape_translate(utf8_to_wide(unescape_string(items[i])))); | |||
} | |||
//now really show table | |||
@@ -801,7 +801,7 @@ void GUIFormSpecMenu::parseTextList(parserData* data, const std::string &element | |||
spec.ftype = f_Table; | |||
for (unsigned int i = 0; i < items.size(); ++i) { | |||
items[i] = unescape_enriched(unescape_string(items[i])); | |||
items[i] = wide_to_utf8(unescape_translate(utf8_to_wide(unescape_string(items[i])))); | |||
} | |||
//now really show list | |||
@@ -872,7 +872,7 @@ void GUIFormSpecMenu::parseDropDown(parserData* data, const std::string &element | |||
} | |||
for (unsigned int i=0; i < items.size(); i++) { | |||
e->addItem(unescape_enriched(unescape_string( | |||
e->addItem(unescape_translate(unescape_string( | |||
utf8_to_wide(items[i]))).c_str()); | |||
} | |||
@@ -931,7 +931,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, | |||
@@ -1003,7 +1003,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, | |||
@@ -1106,7 +1106,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, | |||
@@ -1259,7 +1259,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); | |||
@@ -1446,7 +1446,7 @@ void GUIFormSpecMenu::parseTabHeader(parserData* data, const std::string &elemen | |||
e->setNotClipped(true); | |||
for (unsigned int i = 0; i < buttons.size(); i++) { | |||
e->addTab(unescape_enriched(unescape_string( | |||
e->addTab(unescape_translate(unescape_string( | |||
utf8_to_wide(buttons[i]))).c_str(), -1); | |||
} | |||
@@ -1505,7 +1505,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); | |||
@@ -1623,14 +1623,14 @@ 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; | |||
} else if (parts.size() == 4) { | |||
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; | |||
} | |||
@@ -2658,14 +2658,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; | |||
@@ -210,7 +210,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), | |||
@@ -244,9 +244,9 @@ class GUIFormSpecMenu : public GUIModalMenu | |||
struct TooltipSpec | |||
{ | |||
TooltipSpec() {} | |||
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; } | |||
@@ -35,6 +35,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) | |||
{ | |||
@@ -122,7 +123,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; | |||
@@ -609,6 +609,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 | |||
@@ -629,7 +632,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()); | |||
@@ -2237,6 +2237,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"); | |||
@@ -2259,6 +2260,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()){ | |||
@@ -2318,18 +2321,28 @@ void Server::fillMediaCache() | |||
} | |||
} | |||
void Server::sendMediaAnnouncement(u16 peer_id) | |||
void Server::sendMediaAnnouncement(u16 peer_id, const std::string &lang_code) | |||
{ | |||
verbosestream << "Server: Announcing files to id(" << 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; | |||
} | |||
@@ -404,7 +404,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); | |||
@@ -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; | |||
} | |||
} |
@@ -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> | |||
@@ -747,3 +748,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; | |||
} |
@@ -205,6 +205,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. | |||
@@ -600,6 +650,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. | |||
* | |||
@@ -342,6 +342,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.cpp | |||