Yes! I have been waiting for this for a very long time, now it's finally here. Thank you very much, you are my hero! :D
Now I have taken some time to test and review this.
Overall, it seems to work. I have found 2 issues with the documentation and a major issue with the translated strings themselves.
One crucial missing feature at the moment are tools for automatic string extraction and updating. These are extremely important, I definitely do not want to extract every string manually, let alone update them.
In the long run, the switch to Gettext should be done for various reasons.
TEST RESULTS
I have performed a couple of simple functional tests:
- Simple pure ASCII strings:
PASSED
- Translated string with Unicode chars:
PASSED
- Original string with lots of weird Unicode chars in it:
PASSED
- String with 1 parameter in it:
PASSED
- String with equals sign:
PASSED
- String with at sign:
PASSED
- String with newline:
PASSED, but misleading documentation (see below)
- Nested translated strings:
PASSED
- String with 9 parameters in it, but the translation had them in reverse order:
PASSED
- Performing string operations on translated strings:
FAILED (see below)
Spoiler
Mod name: test_translate
init.lua
Code: Select all
local S = minetest.get_translator("test_translate")
VAR = S("Hello @1", S("World"))
VARTAB = {}
for i=1, #VAR do
local char = string.sub(VAR, i, i)
table.insert(VARTAB, char)
end
-- This makes the REAL string visible, char-by-char. Use the luacmd mod and
-- do “/lua print(REALVAR)” in console to reveal.
REALVAR = table.concat(VARTAB, ":")
minetest.register_node("test_translate:node", {
description = S("Purple Cube"),
tiles = { "default_dirt.png^[colorize:#FF00FF:127" },
groups = {dig_immediate=3},
is_ground_content = false,
on_construct = function(pos)
local meta = minetest.get_meta(pos)
meta:set_string("formspec",
"size[9,9]tablecolumns[text]table[0,0;5,5;list;"..
minetest.formspec_escape(VAR)..","..
minetest.formspec_escape(S("Apple"))..","..
minetest.formspec_escape(S("Banana"))..";"..
"1]")
end,
})
minetest.register_craftitem("test_translate:item", {
description = S("@1 Item", S("Purple")),
inventory_image = "default_dirt.png^[colorize:#FF00FF:127",
wield_image = "default_dirt.png^[colorize:#FF00FF:127",
groups = {dig_immediate=3},
is_ground_content = false,
})
minetest.register_craftitem("test_translate:hello", {
description = VAR,
inventory_image = "default_dirt.png^[colorize:#FF00FF:127",
wield_image = "default_dirt.png^[colorize:#FF00FF:127",
groups = {dig_immediate=3},
is_ground_content = false,
})
minetest.register_craftitem("test_translate:space", {
description = S(" Whitespace Test "),
inventory_image = "default_dirt.png^[colorize:#FF00FF:127",
wield_image = "default_dirt.png^[colorize:#FF00FF:127",
groups = {dig_immediate=3},
is_ground_content = false,
})
minetest.register_craftitem("test_translate:newline", {
description = S("Newline\nTest"),
inventory_image = "default_dirt.png^[colorize:#FF00FF:127",
wield_image = "default_dirt.png^[colorize:#FF00FF:127",
groups = {dig_immediate=3},
is_ground_content = false,
})
minetest.register_craftitem("test_translate:equals", {
description = S("Equals = Test"),
inventory_image = "default_dirt.png^[colorize:#FF00FF:127",
wield_image = "default_dirt.png^[colorize:#FF00FF:127",
groups = {dig_immediate=3},
is_ground_content = false,
})
minetest.register_craftitem("test_translate:email", {
description = S("example@@example.org"),
inventory_image = "default_dirt.png^[colorize:#FF00FF:127",
wield_image = "default_dirt.png^[colorize:#FF00FF:127",
groups = {dig_immediate=3},
is_ground_content = false,
})
minetest.register_craftitem("test_translate:129", {
description = S("@1 @2 @3 @4 @5 @6 @7 @8 @9", S("one"), S("two"), S("three"), S("four"), S("five"), S("six"), S("seven"), S("eight"), S("nine")),
inventory_image = "default_dirt.png^[colorize:#FF00FF:127",
wield_image = "default_dirt.png^[colorize:#FF00FF:127",
groups = {dig_immediate=3},
is_ground_content = false,
})
locale/test_translate.de.tr
Code: Select all
# textdomain: test_translate
Purple Cube=Violetter Würfel
@1 Item=@1 Gegenstand
Purple=Violetter
Hello @1=Hallo @1
World=Welt
Whitespace Test = Leerzeichentest
Newline@
Test=Neuzeilen-@
Test
Equals @= Test=Gleichheits @= Test
example@@example.org=Beispiel@@example.org
@1 @2 @3 @4 @5 @6 @7 @8 @9=@9 @8 @7 @6 @5 @4 @3 @2 @1
one=eins
two=zwei
three=drei
four=vier
five=fünf
six=sechs
seven=sieben
eight=acht
nine=neun
DOCUMENTATION
Documentation is mostly fine. I found 2 minor issues.
Issue 1:
Documentation made me think that for a newline I have literally type “@\n” into the text. I later realized that I must replace “\n” with a *real* newline character. This is pretty confusing and also makes the translation file look a bit messy.
Issue 2:
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 is a bad example and should be replaced. This example encourages a poor coding practise. It makes an invalid assumption about languages, namely that the word “Red” will always have the exact same translation in all contexts. This assumption already fails when the word “red” needs to be inflected. Here's are some translations into German:
red = rot
red carpet = rot
er Teppich
red wool = rot
e Wolle
red car = rot
es Auto
Therefore, the safest way to translate item names is to simply always put the full name into minetest.translate, even if it means more typing for you.
I suggest to use a different example which uses e.g. a number, which is much more common.
Read
https://www.gnu.org/software/gettext/ma ... ng-Strings for some good principles to follow. Although this does not use Gettext (yet), the principles in this document very much apply to this translation system as well.
OTHER COMMENTS
The file name suffix “.tr” is a poor choise IMO. This suffix is already used by Qt translation files and those have a very different format (XML-based).
What about “.mtf” (for “Minetest Translation File”)?
STRING OPERATIONS FAIL
I have made a troubling observation. Apparently you can no longer trust basic operations on strings returned by minetest.translate.
Playing around with luacmd:
/lua print(#"Hello")
5
/lua S = minetest.get_translator()
/lua b=S("Hello")
/lua print(b)
Hello
/lua print(#b)
9
In other words, the “#” operator returns the incorrect length for “Hello” after it went through the translator. WTF?
Looking further into this, I found why this is happening:
Code: Select all
S = minetest.get_translator("test_translate") -- I created a test mod with this name
VAR = S("Hello @1", S("World"))
VARTAB = {}
for i=1, #VAR do
local char = string.sub(VAR, i, i)
table.insert(VARTAB, char)
end
REALVAR = table.concat(VARTAB, ":")
I had the feeling that the print operator is lying to me. So I want to look at each character. This code basically steps through each character in VAR and prints it out, so I can reveal the true string, but separated with colons.
Removing the colons then revealed the true string:
Code: Select all
(T@test_translate)Hello F(T@test_translate)WorldEEE
This is troubling. This proofs that the “#” is not useful on translated strings, and a lot of other string operations like string.sub, string.gsub, etc. apparently operate on the strange string above, instead of just “Hello World”.
This will probably mess up a lot of assumptions modders can usually make about strings in Lua. Scary.
Is this intentional? Are there other operations I cannot do on “translated” strings (e.g. strings returned by minetest.translate)?
Nothing of this is documented.
GETTEXT
Any yes, I agree that the switch to Gettext should be eventually made. There are many reasons, like a mature and WORKING toolchain for string extraction and updating (VERY important), a ton of 3rd party tools, translators are used to it, Minetest uses it for the engine, potential Weblate support, plural support, and, and, and …