1
0
Fork 0
mirror of https://github.com/luanti-org/luanti.git synced 2025-08-01 17:38:41 +00:00

Add support for translating content titles and descriptions (#12208)

This commit is contained in:
rubenwardy 2024-02-24 19:13:07 +00:00 committed by GitHub
parent 57de599a29
commit b4be483d3e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 252 additions and 47 deletions

View file

@ -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;
@ -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");
@ -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)),

View file

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

View file

@ -36,6 +36,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "client/guiscalingfilter.h"
#include "irrlicht_changes/static_text.h"
#include "client/tile.h"
#include "content/content.h"
#include "content/mods.h"
#if USE_SOUND
#include "client/sound/sound_openal.h"
@ -204,6 +206,57 @@ GUIEngine::GUIEngine(JoystickController *joystick,
m_menu.reset();
}
/******************************************************************************/
std::string findLocaleFileInMods(const std::string &path, const std::string &filename)
{
std::vector<ModSpec> mods = flattenMods(getModsInPath(path, "root", true));
for (const auto &mod : mods) {
std::string ret = mod.path + DIR_DELIM "locale" DIR_DELIM + filename;
if (fs::PathExists(ret)) {
return ret;
}
}
return "";
}
/******************************************************************************/
Translations *GUIEngine::getContentTranslations(const std::string &path,
const std::string &domain, const std::string &lang_code)
{
if (domain.empty() || lang_code.empty())
return nullptr;
std::string filename = domain + "." + lang_code + ".tr";
std::string key = path + DIR_DELIM "locale" DIR_DELIM + filename;
if (key == m_last_translations_key)
return &m_last_translations;
std::string trans_path = key;
ContentType type = getContentType(path);
if (type == ContentType::GAME)
trans_path = findLocaleFileInMods(path + DIR_DELIM "mods" DIR_DELIM, filename);
else if (type == ContentType::MODPACK)
trans_path = findLocaleFileInMods(path, filename);
// We don't need to search for locale files in a mod, as there's only one `locale` folder.
if (trans_path.empty())
return nullptr;
m_last_translations_key = key;
m_last_translations = {};
std::string data;
if (fs::ReadFile(trans_path, data)) {
m_last_translations.loadTranslation(data);
}
return &m_last_translations;
}
/******************************************************************************/
bool GUIEngine::loadMainMenuScript()
{

View file

@ -28,6 +28,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
#include "client/sound.h"
#include "client/tile.h"
#include "util/enriched_string.h"
#include "translation.h"
/******************************************************************************/
/* Structs and macros */
@ -165,7 +166,22 @@ public:
return m_scriptdir;
}
/**
* Get translations for content
*
* Only loads a single textdomain from the path, as specified by `domain`,
* for performance reasons.
*
* WARNING: Do not store the returned pointer for long as the contents may
* change with the next call to `getContentTranslations`.
* */
Translations *getContentTranslations(const std::string &path,
const std::string &domain, const std::string &lang_code);
private:
std::string m_last_translations_key;
/** Only the most recently used translation set is kept loaded */
Translations m_last_translations;
/** find and run the main menu script */
bool loadMainMenuScript();

View file

@ -363,6 +363,9 @@ int ModApiMainMenu::l_get_content_info(lua_State *L)
lua_pushstring(L, spec.name.c_str());
lua_setfield(L, -2, "name");
lua_pushstring(L, spec.title.c_str());
lua_setfield(L, -2, "title");
lua_pushstring(L, spec.type.c_str());
lua_setfield(L, -2, "type");
@ -383,6 +386,9 @@ int ModApiMainMenu::l_get_content_info(lua_State *L)
lua_pushstring(L, spec.path.c_str());
lua_setfield(L, -2, "path");
lua_pushstring(L, spec.textdomain.c_str());
lua_setfield(L, -2, "textdomain");
if (spec.type == "mod") {
ModSpec spec;
spec.path = path;
@ -432,8 +438,7 @@ int ModApiMainMenu::l_check_mod_configuration(lua_State *L)
// Ignore non-string keys
if (lua_type(L, -2) != LUA_TSTRING) {
throw LuaError(
"Unexpected non-string key in table passed to "
"core.check_mod_configuration");
"Unexpected non-string key in table passed to core.check_mod_configuration");
}
std::string modpath = luaL_checkstring(L, -1);
@ -472,7 +477,6 @@ int ModApiMainMenu::l_check_mod_configuration(lua_State *L)
return 1;
}
lua_newtable(L);
lua_pushboolean(L, modmgr.isConsistent());
@ -500,7 +504,25 @@ int ModApiMainMenu::l_check_mod_configuration(lua_State *L)
index++;
}
lua_setfield(L, -2, "satisfied_mods");
return 1;
}
/******************************************************************************/
int ModApiMainMenu::l_get_content_translation(lua_State *L)
{
GUIEngine* engine = getGuiEngine(L);
sanity_check(engine != NULL);
std::string path = luaL_checkstring(L, 1);
std::string domain = luaL_checkstring(L, 2);
std::string string = luaL_checkstring(L, 3);
std::string lang = gettext("LANG_CODE");
if (lang == "LANG_CODE")
lang = "";
auto *translations = engine->getContentTranslations(path, domain, lang);
string = wide_to_utf8(translate_string(utf8_to_wide(string), translations));
lua_pushstring(L, string.c_str());
return 1;
}
@ -1102,6 +1124,7 @@ void ModApiMainMenu::Initialize(lua_State *L, int top)
API_FCT(get_games);
API_FCT(get_content_info);
API_FCT(check_mod_configuration);
API_FCT(get_content_translation);
API_FCT(start);
API_FCT(close);
API_FCT(show_keys_menu);

View file

@ -84,6 +84,8 @@ private:
static int l_check_mod_configuration(lua_State *L);
static int l_get_content_translation(lua_State *L);
//gui
static int l_show_keys_menu(lua_State *L);

View file

@ -54,6 +54,7 @@ const std::wstring &Translations::getTranslation(
void Translations::loadTranslation(const std::string &data)
{
std::istringstream is(data);
std::string textdomain_narrow;
std::wstring textdomain;
std::string line;
@ -70,7 +71,8 @@ void Translations::loadTranslation(const std::string &data)
<< "\"" << std::endl;
continue;
}
textdomain = utf8_to_wide(trim(parts[1]));
textdomain_narrow = trim(parts[1]);
textdomain = utf8_to_wide(textdomain_narrow);
}
if (line.empty() || line[0] == '#')
continue;
@ -116,7 +118,7 @@ void Translations::loadTranslation(const std::string &data)
if (i == wline.length()) {
errorstream << "Malformed translation line \"" << line << "\""
<< std::endl;
<< " in text domain " << textdomain_narrow << std::endl;
continue;
}
i++;