1
0
mirror of https://github.com/TerryCavanagh/VVVVVV.git synced 2024-06-28 23:48:30 +02:00
VVVVVV/desktop_version/src/LocalizationMaint.cpp
Dav999 6377fd5e73 Make language sync support creating blank language files
Two translators thus far have tried to populate initial language files
by creating a blank folder and then using the in-game sync option. For
example, see #1078.

That is not how the sync option was intended to be used, but it's
really close to getting everything, so I decided to just complete the
support by making sure numbers.xml is copied from English, and making
sure meta.xml is filled in with English text and not text from an
arbitrary language. Also, minor detail on plural form 1 being set to 1
by default if reset, so strings_plural.xml is fully consistent too.
2023-12-15 19:48:55 -08:00

580 lines
18 KiB
C++

#define LOCALIZATIONMAINT_CPP
#include "Localization.h"
#include "LocalizationStorage.h"
#include <tinyxml2.h>
#include "Alloc.h"
#include "FileSystemUtils.h"
#include "Font.h"
#include "Graphics.h"
#include "Script.h"
#include "Vlogging.h"
#include "XMLUtils.h"
namespace loc
{
static void write_max_local(tinyxml2::XMLElement* pElem, uint8_t glyph_w, uint8_t glyph_h)
{
const char* max;
if ((max = pElem->Attribute("max")) != NULL)
{
unsigned short max_w, max_h, max_local_w, max_local_h;
if (parse_max(max, &max_w, &max_h))
{
max_local_w = (max_w*8) / glyph_w;
max_local_h = (max_h*10) / SDL_max(10, glyph_h);
if (max_local_h == 0)
{
max_local_h = 1;
}
char buf[16];
if (max_h == 1)
{
SDL_snprintf(buf, sizeof(buf), "%d", max_local_w);
}
else
{
SDL_snprintf(buf, sizeof(buf), "%d*%d", max_local_w, max_local_h);
}
pElem->SetAttribute("max_local", buf);
}
}
}
static void write_max_local_decl(tinyxml2::XMLDocument* doc, uint8_t glyph_w, uint8_t glyph_h)
{
char buf[16];
SDL_snprintf(buf, sizeof(buf), "%dx%d", glyph_w, glyph_h);
doc->FirstChildElement()->SetAttribute("max_local_for", buf);
}
static void sync_lang_file(const std::string& langcode)
{
/* Update translation files for the given language with new strings from templates.
* This basically takes the (English) templates, fills in existing translations, and saves.
* Any FILESYSTEM_saveTiXml2Document() writes to main lang dir */
vlog_info("Syncing %s with templates...", langcode.c_str());
lang = langcode;
loadtext(false);
uint8_t glyph_w = 8, glyph_h = 8;
font::glyph_dimensions(PR_FONT_IDX(langmeta.font_idx), &glyph_w, &glyph_h);
bool max_local_needed = glyph_w != 8 || glyph_h != 8;
tinyxml2::XMLDocument doc;
tinyxml2::XMLHandle hDoc(&doc);
tinyxml2::XMLElement* pElem;
tinyxml2::XMLElement* subElem;
if (load_lang_doc("meta", doc, "en"))
{
FOR_EACH_XML_ELEMENT(hDoc, pElem)
{
const char* pKey = pElem->Value();
if (SDL_strcmp(pKey, "active") == 0)
pElem->SetText((int) langmeta.active);
else if (SDL_strcmp(pKey, "nativename") == 0)
pElem->SetText(langmeta.nativename.c_str());
else if (SDL_strcmp(pKey, "credit") == 0)
pElem->SetText(langmeta.credit.c_str());
else if (SDL_strcmp(pKey, "action_hint") == 0)
pElem->SetText(langmeta.action_hint.c_str());
else if (SDL_strcmp(pKey, "gamepad_hint") == 0)
pElem->SetText(langmeta.gamepad_hint.c_str());
else if (SDL_strcmp(pKey, "autowordwrap") == 0)
pElem->SetText((int) langmeta.autowordwrap);
else if (SDL_strcmp(pKey, "toupper") == 0)
pElem->SetText((int) langmeta.toupper);
else if (SDL_strcmp(pKey, "toupper_i_dot") == 0)
pElem->SetText((int) langmeta.toupper_i_dot);
else if (SDL_strcmp(pKey, "toupper_lower_escape_char") == 0)
pElem->SetText((int) langmeta.toupper_lower_escape_char);
else if (SDL_strcmp(pKey, "menu_select") == 0)
pElem->SetText(langmeta.menu_select.c_str());
else if (SDL_strcmp(pKey, "menu_select_tight") == 0)
pElem->SetText(langmeta.menu_select_tight.c_str());
else if (SDL_strcmp(pKey, "font") == 0)
pElem->SetText(font::get_main_font_name(langmeta.font_idx));
}
/* This part exists because we want to preserve blank lines between the commented
* options for clarity, so we have to take matters into our own hands. */
for (
tinyxml2::XMLNode* pNode = hDoc.FirstChildElement().FirstChild().ToNode();
pNode != NULL;
pNode = pNode->NextSibling()
)
{
tinyxml2::XMLComment* pCom = pNode->ToComment();
if (pCom != NULL)
{
tinyxml2::XMLNode* pPrevNode = pCom->PreviousSibling();
if (pPrevNode != NULL)
{
doc.FirstChildElement()->InsertAfterChild(pPrevNode, doc.NewText("\n\n "));
}
doc.FirstChildElement()->InsertAfterChild(pCom, doc.NewText("\n "));
}
}
FILESYSTEM_saveTiXml2Document((langcode + "/meta.xml").c_str(), doc);
}
if (load_lang_doc("strings", doc, "en"))
{
if (max_local_needed)
{
write_max_local_decl(&doc, glyph_w, glyph_h);
}
FOR_EACH_XML_ELEMENT(hDoc, pElem)
{
EXPECT_ELEM(pElem, "string");
const char* eng = pElem->Attribute("english");
if (eng != NULL)
{
char textcase = pElem->UnsignedAttribute("case", 0);
const char* tra;
if (textcase == 0)
{
tra = map_lookup_text(map_translation, eng, "");
}
else
{
char* eng_prefixed = add_disambiguator(textcase, eng, NULL);
if (eng_prefixed == NULL)
{
/* Are we out of memory? Stop, don't blank our language files... */
return;
}
/* Note the fallback: if this string used to not be cased and now it is,
* simply fill in the old single variant we already had. */
tra = map_lookup_text(
map_translation,
eng_prefixed,
map_lookup_text(map_translation, eng, "")
);
VVV_free(eng_prefixed);
}
pElem->SetAttribute("translation", tra);
}
if (max_local_needed)
{
write_max_local(pElem, glyph_w, glyph_h);
}
}
FILESYSTEM_saveTiXml2Document((langcode + "/strings.xml").c_str(), doc);
}
if (!load_lang_doc("numbers", doc, langcode))
{
/* If numbers.xml doesn't exist _at all_, then simply copy it from English,
* so that you can make a new translation simply by making a folder and syncing. */
if (load_lang_doc("numbers", doc, "en"))
{
FILESYSTEM_saveTiXml2Document((langcode + "/numbers.xml").c_str(), doc);
}
}
if (load_lang_doc("strings_plural", doc, "en"))
{
/* Form 255 is technically invalid, but we have to account for it */
bool form_id_used[256];
SDL_zeroa(form_id_used);
for (int num = 0; num < 200; num++)
{
form_id_used[number_plural_form[num]] = true;
}
if (max_local_needed)
{
write_max_local_decl(&doc, glyph_w, glyph_h);
}
FOR_EACH_XML_ELEMENT(hDoc, pElem)
{
EXPECT_ELEM(pElem, "string");
if (max_local_needed)
{
write_max_local(pElem, glyph_w, glyph_h);
}
pElem->DeleteChildren();
const char* eng_plural = pElem->Attribute("english_plural");
for (int form_id = 0; form_id < 255; form_id++)
{
if (form_id_used[form_id] && eng_plural != NULL)
{
subElem = doc.NewElement("translation");
pElem->LinkEndChild(subElem);
subElem->SetAttribute("form", form_id);
char* key = add_disambiguator(form_id+1, eng_plural, NULL);
if (key == NULL)
{
/* Out of memory or something, stop */
return;
}
subElem->SetAttribute("translation", map_lookup_text(map_translation_plural, key, ""));
VVV_free(key);
}
}
}
FILESYSTEM_saveTiXml2Document((langcode + "/strings_plural.xml").c_str(), doc);
}
if (load_lang_doc("cutscenes", doc, "en"))
{
FOR_EACH_XML_ELEMENT(hDoc, pElem)
{
EXPECT_ELEM(pElem, "cutscene");
const char* cutscene_id = pElem->Attribute("id");
if (cutscene_id == NULL)
{
continue;
}
hashmap* map = map_translation_cutscene;
uintptr_t ptr_cutscene_map;
bool found = hashmap_get(map, cutscene_id, SDL_strlen(cutscene_id), &ptr_cutscene_map);
hashmap* cutscene_map = (hashmap*) ptr_cutscene_map;
if (!found || cutscene_map == NULL)
{
continue;
}
FOR_EACH_XML_SUB_ELEMENT(pElem, subElem)
{
EXPECT_ELEM(subElem, "dialogue");
const char* eng = subElem->Attribute("english");
if (eng == NULL)
{
continue;
}
size_t alloc_len;
const std::string eng_unwrapped = font::string_unwordwrap(eng);
char* eng_prefixed = add_disambiguator(subElem->UnsignedAttribute("case", 1), eng_unwrapped.c_str(), &alloc_len);
if (eng_prefixed == NULL)
{
/* Out of memory or something, stop */
return;
}
uintptr_t ptr_format;
found = hashmap_get(cutscene_map, eng_prefixed, alloc_len-1, &ptr_format);
const TextboxFormat* format = (TextboxFormat*) ptr_format;
VVV_free(eng_prefixed);
if (!found || format == NULL)
{
continue;
}
subElem->DeleteAttribute("tt");
subElem->DeleteAttribute("wraplimit");
subElem->DeleteAttribute("centertext");
subElem->DeleteAttribute("pad");
subElem->DeleteAttribute("pad_left");
subElem->DeleteAttribute("pad_right");
subElem->DeleteAttribute("padtowidth");
bool buttons = subElem->BoolAttribute("buttons", false);
subElem->DeleteAttribute("buttons"); // we want this at the end...
if (format->text != NULL)
subElem->SetAttribute("translation", format->text);
if (format->tt)
subElem->SetAttribute("tt", 1);
if (format->wraplimit_raw != 0)
subElem->SetAttribute("wraplimit", format->wraplimit_raw);
if (format->centertext)
subElem->SetAttribute("centertext", 1);
if (format->pad_left == format->pad_right && format->pad_left != 0)
{
subElem->SetAttribute("pad", format->pad_left);
}
else
{
if (format->pad_left != 0)
subElem->SetAttribute("pad_left", format->pad_left);
if (format->pad_right != 0)
subElem->SetAttribute("pad_right", format->pad_right);
}
if (format->padtowidth != 0)
subElem->SetAttribute("padtowidth", format->padtowidth);
if (buttons)
subElem->SetAttribute("buttons", 1);
}
}
FILESYSTEM_saveTiXml2Document((langcode + "/cutscenes.xml").c_str(), doc);
}
if (load_lang_doc("roomnames", doc, "en"))
{
FOR_EACH_XML_ELEMENT(hDoc, pElem)
{
EXPECT_ELEM(pElem, "roomname");
pElem->SetAttribute("translation",
get_roomname_translation(false, pElem->UnsignedAttribute("x"), pElem->UnsignedAttribute("y"))
);
}
FILESYSTEM_saveTiXml2Document((langcode + "/roomnames.xml").c_str(), doc);
}
if (load_lang_doc("roomnames_special", doc, "en"))
{
FOR_EACH_XML_ELEMENT(hDoc, pElem)
{
EXPECT_ELEM(pElem, "roomname");
const char* eng = pElem->Attribute("english");
if (eng != NULL)
{
pElem->SetAttribute("translation", map_lookup_text(map_translation_roomnames_special, eng, ""));
}
}
FILESYSTEM_saveTiXml2Document((langcode + "/roomnames_special.xml").c_str(), doc);
}
}
bool sync_lang_files(void)
{
/* Returns false if we can't set the lang write dir, true otherwise.
* This could maybe be extended with better error reporting,
* problem is getting across which files failed in which languages. */
std::string oldlang = lang;
if (!FILESYSTEM_setLangWriteDir())
{
vlog_error("Cannot set write dir to lang dir, not syncing language files");
return false;
}
for (size_t i = 0; i < languagelist.size(); i++)
{
if (languagelist[i].code != "en")
sync_lang_file(languagelist[i].code);
}
FILESYSTEM_restoreWriteDir();
lang = oldlang;
loadtext(false);
return true;
}
bool save_roomname_to_file(const std::string& langcode, bool custom_level, int roomx, int roomy, const char* tra, const char* explanation)
{
if (custom_level)
{
vlog_error("Saving custom level room names not implemented");
return false;
}
if (!fix_room_coords(custom_level, &roomx, &roomy))
{
return false;
}
tinyxml2::XMLDocument doc;
tinyxml2::XMLHandle hDoc(&doc);
tinyxml2::XMLElement* pElem;
if (!load_lang_doc("roomnames", doc, langcode))
{
return false;
}
bool found = false;
FOR_EACH_XML_ELEMENT(hDoc, pElem)
{
EXPECT_ELEM(pElem, "roomname");
int x = pElem->IntAttribute("x", -1);
int y = pElem->IntAttribute("y", -1);
if (x == roomx && y == roomy)
{
if (explanation != NULL)
{
pElem->SetAttribute("explanation", explanation);
}
if (tra != NULL)
{
pElem->SetAttribute("translation", tra);
}
found = true;
}
}
if (!found)
{
vlog_error("Could not find room %d,%d in language file to replace!", roomx, roomy);
return false;
}
if (!FILESYSTEM_setLangWriteDir())
{
vlog_error("Cannot set write dir to lang dir, so room name can't be saved");
return false;
}
bool save_success = FILESYSTEM_saveTiXml2Document((langcode + "/roomnames.xml").c_str(), doc);
FILESYSTEM_restoreWriteDir();
if (!save_success)
{
vlog_error("Could not write roomnames document!");
return false;
}
return store_roomname_translation(custom_level, roomx, roomy, tra, explanation);
}
bool save_roomname_explanation_to_files(bool custom_level, int roomx, int roomy, const char* explanation)
{
bool success = true;
for (size_t i = 0; i < languagelist.size(); i++)
{
if (!save_roomname_to_file(languagelist[i].code, custom_level, roomx, roomy, NULL, explanation))
{
success = false;
vlog_warn("Could not save room name explanation to language %s", languagelist[i].code.c_str());
}
}
return !languagelist.empty() && success;
}
void local_limits_check(void)
{
text_overflows.clear();
loadtext(true);
limitscheck_current_overflow = 0;
}
void global_limits_check(void)
{
text_overflows.clear();
std::string oldlang = lang;
textbook_clear(&textbook_main);
textbook_set_protected(&textbook_main, true);
for (size_t i = 0; i < languagelist.size(); i++)
{
if (languagelist[i].code != "en")
{
lang = languagelist[i].code;
loadtext(true);
}
}
lang = oldlang;
loadtext(false);
textbook_set_protected(&textbook_main, false);
limitscheck_current_overflow = 0;
}
void populate_testable_script_ids(void)
{
testable_script_ids.clear();
tinyxml2::XMLDocument doc;
tinyxml2::XMLHandle hDoc(&doc);
tinyxml2::XMLElement* pElem;
if (!load_lang_doc("cutscenes", doc))
{
return;
}
FOR_EACH_XML_ELEMENT(hDoc, pElem)
{
EXPECT_ELEM(pElem, "cutscene");
const char* id = pElem->Attribute("id");
if (id != NULL)
{
testable_script_ids.push_back(id);
}
}
}
bool populate_cutscene_test(const char* script_id)
{
tinyxml2::XMLDocument doc;
tinyxml2::XMLHandle hDoc(&doc);
tinyxml2::XMLElement* pElem;
if (!load_lang_doc("cutscenes", doc))
{
return false;
}
const char* original = get_level_original_lang(hDoc);
FOR_EACH_XML_ELEMENT(hDoc, pElem)
{
EXPECT_ELEM(pElem, "cutscene");
if (SDL_strcmp(pElem->Attribute("id"), script_id) != 0)
{
/* Not the correct cutscene */
continue;
}
tinyxml2::XMLElement* subElem;
FOR_EACH_XML_SUB_ELEMENT(pElem, subElem)
{
EXPECT_ELEM(subElem, "dialogue");
const char* tra = subElem->Attribute("translation");
const char* speaker = subElem->Attribute("speaker");
const char* eng = subElem->Attribute(original);
if (tra != NULL && tra[0] != '\0' && speaker != NULL && eng != NULL)
{
script.add_test_line(
speaker,
eng,
subElem->UnsignedAttribute("case", 1),
subElem->BoolAttribute("buttons", false)
);
}
}
return true;
}
return false;
}
} /* namespace loc */