Skip to content

Commit

Permalink
Add support for translating content titles and descriptions (#12208)
Browse files Browse the repository at this point in the history
  • Loading branch information
rubenwardy committed Feb 24, 2024
1 parent 57de599 commit b4be483
Show file tree
Hide file tree
Showing 12 changed files with 252 additions and 47 deletions.
27 changes: 27 additions & 0 deletions builtin/mainmenu/content/pkgmgr.lua
Expand Up @@ -150,6 +150,8 @@ function pkgmgr.get_mods(path, virtual_path, listing, modpack)
toadd.virtual_path = mod_virtual_path
toadd.type = "mod"

pkgmgr.update_translations({ toadd })

-- Check modpack.txt
-- Note: modpack.conf is already checked above
local modpackfile = io.open(mod_path .. DIR_DELIM .. "modpack.txt")
Expand Down Expand Up @@ -189,6 +191,8 @@ function pkgmgr.get_texture_packs()
load_texture_packs(txtpath_system, retval)
end

pkgmgr.update_translations(retval)

table.sort(retval, function(a, b)
return a.title:lower() < b.title:lower()
end)
Expand Down Expand Up @@ -775,6 +779,29 @@ function pkgmgr.update_gamelist()
table.sort(pkgmgr.games, function(a, b)
return a.title:lower() < b.title:lower()
end)
pkgmgr.update_translations(pkgmgr.games)
end

--------------------------------------------------------------------------------
function pkgmgr.update_translations(list)
for _, item in ipairs(list) do
local info = core.get_content_info(item.path)
assert(info.path)
assert(info.textdomain)

assert(not item.is_translated)
item.is_translated = true

if info.title and info.title ~= "" then
item.title = core.get_content_translation(info.path, info.textdomain,
core.translate(info.textdomain, info.title))
end

if info.description and info.description ~= "" then
item.description = core.get_content_translation(info.path, info.textdomain,
core.translate(info.textdomain, info.description))
end
end
end

--------------------------------------------------------------------------------
Expand Down
7 changes: 4 additions & 3 deletions builtin/mainmenu/tab_content.lua
Expand Up @@ -114,12 +114,13 @@ local function get_formspec(tabview, name, tabdata)
modscreenshot = defaulttexturedir .. "no_screenshot.png"
end

local info = core.get_content_info(selected_pkg.path)
local desc = fgettext("No package description available")
if info.description and info.description:trim() ~= "" then
desc = core.formspec_escape(info.description)
if selected_pkg.description and selected_pkg.description:trim() ~= "" then
desc = core.formspec_escape(selected_pkg.description)
end

local info = core.get_content_info(selected_pkg.path)

local title_and_name
if selected_pkg.type == "game" then
title_and_name = selected_pkg.name
Expand Down
66 changes: 55 additions & 11 deletions doc/lua_api.md
Expand Up @@ -61,7 +61,8 @@ The game directory can contain the following files:
* `game.conf`, with the following keys:
* `title`: Required, a human-readable title to address the game, e.g. `title = Minetest Game`.
* `name`: (Deprecated) same as title.
* `description`: Short description to be shown in the content tab
* `description`: Short description to be shown in the content tab.
See [Translating content meta](#translating-content-meta).
* `allowed_mapgens = <comma-separated mapgens>`
e.g. `allowed_mapgens = v5,v6,flat`
Mapgens not in this list are removed from the list of mapgens for the
Expand All @@ -87,10 +88,11 @@ The game directory can contain the following files:
`enable_damage`, `creative_mode`, `enable_server`.
* `map_persistent`: Specifies whether newly created worlds should use
a persistent map backend. Defaults to `true` (= "sqlite3")
* `author`: The author of the game. It only appears when downloaded from
ContentDB.
* `author`: The author's ContentDB username.
* `release`: Ignore this: Should only ever be set by ContentDB, as it is
an internal ID used to track versions.
* `textdomain`: Textdomain used to translate description. Defaults to game id.
See [Translating content meta](#translating-content-meta).
* `minetest.conf`:
Used to set default settings when running this game.
* `settingtypes.txt`:
Expand Down Expand Up @@ -156,13 +158,14 @@ The file is a key-value store of modpack details.

* `name`: The modpack name. Allows Minetest to determine the modpack name even
if the folder is wrongly named.
* `title`: A human-readable title to address the modpack. See [Translating content meta](#translating-content-meta).
* `description`: Description of mod to be shown in the Mods tab of the main
menu.
* `author`: The author of the modpack. It only appears when downloaded from
ContentDB.
menu. See [Translating content meta](#translating-content-meta).
* `author`: The author's ContentDB username.
* `release`: Ignore this: Should only ever be set by ContentDB, as it is an
internal ID used to track versions.
* `title`: A human-readable title to address the modpack.
* `textdomain`: Textdomain used to translate title and description. Defaults to modpack name.
See [Translating content meta](#translating-content-meta).

Note: to support 0.4.x, please also create an empty modpack.txt file.

Expand Down Expand Up @@ -201,17 +204,18 @@ A `Settings` file that provides meta information about the mod.

* `name`: The mod name. Allows Minetest to determine the mod name even if the
folder is wrongly named.
* `title`: A human-readable title to address the mod. See [Translating content meta](#translating-content-meta).
* `description`: Description of mod to be shown in the Mods tab of the main
menu.
menu. See [Translating content meta](#translating-content-meta).
* `depends`: A comma separated list of dependencies. These are mods that must be
loaded before this mod.
* `optional_depends`: A comma separated list of optional dependencies.
Like a dependency, but no error if the mod doesn't exist.
* `author`: The author of the mod. It only appears when downloaded from
ContentDB.
* `author`: The author's ContentDB username.
* `release`: Ignore this: Should only ever be set by ContentDB, as it is an
internal ID used to track versions.
* `title`: A human-readable title to address the mod.
* `textdomain`: Textdomain used to translate title and description. Defaults to modname.
See [Translating content meta](#translating-content-meta).

### `screenshot.png`

Expand Down Expand Up @@ -4135,6 +4139,46 @@ the table returned by `minetest.get_player_information(name)`.
IMPORTANT: This functionality should only be used for sorting, filtering or similar purposes.
You do not need to use this to get translated strings to show up on the client.

Translating content meta
------------------------

You can translate content meta, such as `title` and `description`, by placing
translations in a `locale/DOMAIN.LANG.tr` file. The textdomain defaults to the
content name, but can be customised using `textdomain` in the content's .conf.

### Mods and Texture Packs

Say you have a mod called `mymod` with a short description in mod.conf:

```
description = This is the short description
```

Minetest will look for translations in the `mymod` textdomain as there's no
textdomain specified in mod.conf. For example, `mymod/locale/mymod.fr.tr`:

```
# textdomain:mymod
This is the short description=Voici la description succincte
```

### Games and Modpacks

For games and modpacks, Minetest will look for the textdomain in all mods.

Say you have a game called `mygame` with the following game.conf:

```
description = This is the game's short description
textdomain = mygame
```

Minetest will then look for the textdomain `mygame` in all mods, for example,
`mygame/mods/anymod/locale/mygame.fr.tr`. Note that it is still recommended that your
textdomain match the mod name, but this isn't required.



Perlin noise
============

Expand Down
8 changes: 8 additions & 0 deletions doc/menu_lua_api.md
Expand Up @@ -323,6 +323,7 @@ Package - content which is downloadable from the content db, may or may not be i
description = "description",
author = "author",
path = "path/to/content",
textdomain = "textdomain", -- textdomain to translate title / description with
depends = {"mod", "names"}, -- mods only
optional_depends = {"mod", "names"}, -- mods only
}
Expand All @@ -340,6 +341,13 @@ Package - content which is downloadable from the content db, may or may not be i
error_message = "", -- message or nil
}
```
* `core.get_content_translation(path, domain, string)`
* Translates `string` using `domain` in content directory at `path`.
* Textdomains will be found by looking through all locale folders.
* String should contain translation markup from `core.translate(textdomain, ...)`.
* Ex: `core.get_content_translation("mods/mymod", "mymod", core.translate("mymod", "Hello World"))`
will translate "Hello World" into the current user's language
using `mods/mymod/locale/mymod.fr.tr`.

Logging
-------
Expand Down
11 changes: 9 additions & 2 deletions doc/texture_packs.md
Expand Up @@ -25,8 +25,14 @@ texture pack. The name must not be “base”.
### `texture_pack.conf`
A key-value config file with the following keys:

* `title` - human readable title
* `name`: The texture pack name. Allows Minetest to determine the texture pack name even if
the folder is wrongly named.
* `title` - human-readable title
* `description` - short description, shown in the content tab
* `author`: The author's ContentDB username.
* `textdomain`: Textdomain used to translate title and description.
Defaults to the texture pack name.
See [Translating content meta](lua_api.md#translating-content-meta).

### `description.txt`
**Deprecated**, you should use texture_pack.conf instead.
Expand Down Expand Up @@ -205,7 +211,8 @@ Here are targets you can choose from:
Nodes support all targets, but other items only support 'inventory'
and 'wield'.

¹ : `N` is an integer [0,255]. Sets align_style = "world" and scale = N on the tile, refer to lua_api.md for details.
¹ : `N` is an integer [0,255]. Sets align_style = "world" and scale = N on the tile,
refer to lua_api.md for details.

### Using the special targets

Expand Down
61 changes: 35 additions & 26 deletions src/content/content.cpp
Expand Up @@ -24,68 +24,59 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "filesys.h"
#include "settings.h"

enum ContentType
ContentType getContentType(const std::string &path)
{
ECT_UNKNOWN,
ECT_MOD,
ECT_MODPACK,
ECT_GAME,
ECT_TXP
};

ContentType getContentType(const ContentSpec &spec)
{
std::ifstream modpack_is((spec.path + DIR_DELIM + "modpack.txt").c_str());
std::ifstream modpack_is((path + DIR_DELIM + "modpack.txt").c_str());
if (modpack_is.good()) {
modpack_is.close();
return ECT_MODPACK;
return ContentType::MODPACK;
}

std::ifstream modpack2_is((spec.path + DIR_DELIM + "modpack.conf").c_str());
std::ifstream modpack2_is((path + DIR_DELIM + "modpack.conf").c_str());
if (modpack2_is.good()) {
modpack2_is.close();
return ECT_MODPACK;
return ContentType::MODPACK;
}

std::ifstream init_is((spec.path + DIR_DELIM + "init.lua").c_str());
std::ifstream init_is((path + DIR_DELIM + "init.lua").c_str());
if (init_is.good()) {
init_is.close();
return ECT_MOD;
return ContentType::MOD;
}

std::ifstream game_is((spec.path + DIR_DELIM + "game.conf").c_str());
std::ifstream game_is((path + DIR_DELIM + "game.conf").c_str());
if (game_is.good()) {
game_is.close();
return ECT_GAME;
return ContentType::GAME;
}

std::ifstream txp_is((spec.path + DIR_DELIM + "texture_pack.conf").c_str());
std::ifstream txp_is((path + DIR_DELIM + "texture_pack.conf").c_str());
if (txp_is.good()) {
txp_is.close();
return ECT_TXP;
return ContentType::TXP;
}

return ECT_UNKNOWN;
return ContentType::UNKNOWN;
}

void parseContentInfo(ContentSpec &spec)
{
std::string conf_path;

switch (getContentType(spec)) {
case ECT_MOD:
switch (getContentType(spec.path)) {
case ContentType::MOD:
spec.type = "mod";
conf_path = spec.path + DIR_DELIM + "mod.conf";
break;
case ECT_MODPACK:
case ContentType::MODPACK:
spec.type = "modpack";
conf_path = spec.path + DIR_DELIM + "modpack.conf";
break;
case ECT_GAME:
case ContentType::GAME:
spec.type = "game";
conf_path = spec.path + DIR_DELIM + "game.conf";
break;
case ECT_TXP:
case ContentType::TXP:
spec.type = "txp";
conf_path = spec.path + DIR_DELIM + "texture_pack.conf";
break;
Expand All @@ -104,6 +95,15 @@ void parseContentInfo(ContentSpec &spec)
if (spec.type != "game" && conf.exists("name"))
spec.name = conf.get("name");

if (conf.exists("title"))
spec.title = conf.get("title");

if (spec.type == "game") {
if (spec.title.empty())
spec.title = spec.name;
spec.name = "";
}

if (conf.exists("description"))
spec.desc = conf.get("description");

Expand All @@ -112,8 +112,17 @@ void parseContentInfo(ContentSpec &spec)

if (conf.exists("release"))
spec.release = conf.getS32("release");

if (conf.exists("textdomain"))
spec.textdomain = conf.get("textdomain");
}

if (spec.name.empty())
spec.name = fs::GetFilenameFromPath(spec.path.c_str());

if (spec.textdomain.empty())
spec.textdomain = spec.name;

if (spec.desc.empty()) {
std::ifstream is((spec.path + DIR_DELIM + "description.txt").c_str());
spec.desc = std::string((std::istreambuf_iterator<char>(is)),
Expand Down
13 changes: 13 additions & 0 deletions src/content/content.h
Expand Up @@ -22,6 +22,16 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "convert_json.h"
#include "irrlichttypes.h"

enum class ContentType
{
UNKNOWN,
MOD,
MODPACK,
GAME,
TXP
};


struct ContentSpec
{
std::string type;
Expand All @@ -37,6 +47,9 @@ struct ContentSpec
/// Short description
std::string desc;
std::string path;
std::string textdomain;
};


ContentType getContentType(const std::string &path);
void parseContentInfo(ContentSpec &spec);

0 comments on commit b4be483

Please sign in to comment.