Skip to content

Commit

Permalink
Add clientside translations.
Browse files Browse the repository at this point in the history
  • Loading branch information
Ekdohibs committed Aug 24, 2017
1 parent b28af0e commit b24e643
Show file tree
Hide file tree
Showing 21 changed files with 629 additions and 46 deletions.
38 changes: 38 additions & 0 deletions builtin/common/misc_helpers.lua
Expand Up @@ -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
--------------------------------------------------------------------------------
Expand Down
66 changes: 66 additions & 0 deletions doc/lua_api.txt
Expand Up @@ -139,6 +139,7 @@ Mod directory structure
| | `-- modname_something_else.png
| |-- sounds
| |-- media
| |-- locale
| `-- <custom data>
`-- another

Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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
------------------------------

Expand Down
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Expand Up @@ -450,6 +450,7 @@ set(common_SRCS
terminal_chat_console.cpp
tileanimation.cpp
tool.cpp
translation.cpp
treegen.cpp
version.cpp
voxel.cpp
Expand Down
10 changes: 6 additions & 4 deletions src/camera.cpp
Expand Up @@ -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();
Expand All @@ -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);
}
}
}
Expand Down
1 change: 1 addition & 0 deletions src/chat.cpp
Expand Up @@ -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())
{
Expand Down
16 changes: 14 additions & 2 deletions src/client.cpp
Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}

Expand Down
16 changes: 9 additions & 7 deletions src/game.cpp
Expand Up @@ -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"
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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()) {
Expand Down Expand Up @@ -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;
Expand All @@ -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()) {
Expand Down
2 changes: 1 addition & 1 deletion src/guiEngine.cpp
Expand Up @@ -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();
}
Expand Down

0 comments on commit b24e643

Please sign in to comment.