#define LOCALIZATIONSTORAGE_CPP #include "Localization.h" #include "LocalizationStorage.h" #include "Alloc.h" #include "Constants.h" #include "CustomLevels.h" #include "FileSystemUtils.h" #include "Font.h" #include "Graphics.h" #include "Unused.h" #include "UtilityClass.h" #include "VFormat.h" #include "Vlogging.h" namespace loc { bool inited = false; bool inited_custom = false; char* custom_level_path = NULL; std::vector text_overflows; bool load_lang_doc( const std::string& cat, tinyxml2::XMLDocument& doc, const std::string& langcode /*= lang*/, const std::string& asset_cat /*= ""*/ ) { /* Load a language-related XML file. * cat is the "category", so "strings", "numbers", etc. * * asset_cat is only used when loading * from custom level assets is possible. */ bool asset_loaded = false; if (!asset_cat.empty()) { asset_loaded = FILESYSTEM_loadAssetTiXml2Document(("lang/" + langcode + "/" + asset_cat + ".xml").c_str(), doc); } if (!asset_loaded && !FILESYSTEM_loadTiXml2Document(("lang/" + langcode + "/" + cat + ".xml").c_str(), doc)) { vlog_debug("Could not load language file %s/%s.", langcode.c_str(), cat.c_str()); return false; } if (doc.Error()) { vlog_error("Error parsing language file %s/%s: %s", langcode.c_str(), cat.c_str(), doc.ErrorStr()); return false; } return true; } static void loadmeta(LangMeta& meta, const std::string& langcode = lang) { meta.active = true; meta.code = langcode; meta.nativename = langcode; meta.credit = ""; meta.action_hint = "Press Space, Z, or V to select"; meta.gamepad_hint = "Press {button} to select"; meta.autowordwrap = true; meta.toupper = true; meta.toupper_i_dot = false; meta.toupper_lower_escape_char = false; meta.rtl = false; meta.menu_select = "[ {label} ]"; meta.menu_select_tight = "[{label}]"; meta.font_idx = font::get_font_idx_8x8(); tinyxml2::XMLDocument doc; tinyxml2::XMLHandle hDoc(&doc); tinyxml2::XMLElement* pElem; if (!load_lang_doc("meta", doc, langcode)) { return; } FOR_EACH_XML_ELEMENT(hDoc, pElem) { const char* pKey = pElem->Value(); const char* pText = pElem->GetText(); if (pText == NULL) { pText = ""; } if (SDL_strcmp(pKey, "active") == 0) meta.active = help.Int(pText); else if (SDL_strcmp(pKey, "nativename") == 0) meta.nativename = std::string(pText); else if (SDL_strcmp(pKey, "credit") == 0) meta.credit = std::string(pText); else if (SDL_strcmp(pKey, "action_hint") == 0) meta.action_hint = std::string(pText); else if (SDL_strcmp(pKey, "gamepad_hint") == 0) meta.gamepad_hint = std::string(pText); else if (SDL_strcmp(pKey, "autowordwrap") == 0) meta.autowordwrap = help.Int(pText); else if (SDL_strcmp(pKey, "toupper") == 0) meta.toupper = help.Int(pText); else if (SDL_strcmp(pKey, "toupper_i_dot") == 0) meta.toupper_i_dot = help.Int(pText); else if (SDL_strcmp(pKey, "toupper_lower_escape_char") == 0) meta.toupper_lower_escape_char = help.Int(pText); else if (SDL_strcmp(pKey, "rtl") == 0) meta.rtl = help.Int(pText); else if (SDL_strcmp(pKey, "menu_select") == 0) meta.menu_select = std::string(pText); else if (SDL_strcmp(pKey, "menu_select_tight") == 0) meta.menu_select_tight = std::string(pText); else if (SDL_strcmp(pKey, "font") == 0) font::find_main_font_by_name(pText, &meta.font_idx); } } static void map_store_translation(Textbook* textbook, hashmap* map, const char* eng, const char* tra) { /* Add the texts to the given textbook and set the translation in the given hashmap. */ if (eng == NULL) { return; } if (tra == NULL) { tra = ""; } const char* tb_eng = textbook_store(textbook, eng); const char* tb_tra = textbook_store(textbook, tra); if (tb_eng == NULL || tb_tra == NULL) { return; } hashmap_set(map, tb_eng, SDL_strlen(tb_eng), (uintptr_t) tb_tra); } unsigned char form_for_count(int n) { int n_ix; if (n > -100 && n < 100) { /* Plural forms for negative numbers are debatable in any language I'd imagine... * But they shouldn't appear anyway unless there's a bug or you're asking for it. * Or do YOU ever get -10 deaths while collecting -1 trinket? */ n_ix = SDL_abs(n); } else { /* Plural forms for 100 and above always just keep repeating. Thank goodness. */ n_ix = SDL_abs(n % 100) + 100; } return number_plural_form[n_ix]; } static void callback_free_map_value(void* key, size_t ksize, uintptr_t value, void* usr) { UNUSED(key); UNUSED(ksize); UNUSED(usr); hashmap_free((hashmap*) value); } static void resettext_custom(bool final_shutdown) { /* Reset/initialize custom level strings only. * If final_shutdown, this just does a last cleanup of any allocations, * otherwise it makes storage ready for first use (or reuse by a new language). */ if (inited_custom) { hashmap_iterate(map_translation_cutscene_custom, callback_free_map_value, NULL); hashmap_free(map_translation_cutscene_custom); textbook_clear(&textbook_custom); } else if (!final_shutdown) { inited_custom = true; textbook_init(&textbook_custom); } if (!final_shutdown) { map_translation_cutscene_custom = hashmap_create(); SDL_zeroa(translation_roomnames_custom); SDL_zeroa(explanation_roomnames_custom); n_untranslated_roomnames_custom = 0; n_unexplained_roomnames_custom = 0; } } void unloadtext_custom(void) { resettext_custom(false); loc::lang_custom = ""; VVV_free(custom_level_path); custom_level_path = NULL; } void resettext(bool final_shutdown) { /* Reset/initialize strings. * If final_shutdown, this just does a last cleanup of any allocations, * otherwise it makes storage ready for first use (or reuse by a new language). */ if (inited) { hashmap_free(map_translation); hashmap_iterate(map_translation_cutscene, callback_free_map_value, NULL); hashmap_free(map_translation_cutscene); hashmap_free(map_translation_plural); hashmap_free(map_translation_roomnames_special); textbook_clear(&textbook_main); } else if (!final_shutdown) { inited = true; textbook_init(&textbook_main); } if (!final_shutdown) { map_translation = hashmap_create(); map_translation_cutscene = hashmap_create(); map_translation_plural = hashmap_create(); for (size_t i = 0; i <= 100; i++) { number[i] = ""; number2[i] = ""; } SDL_zeroa(number_plural_form); number_plural_form[1] = 1; SDL_zeroa(translation_roomnames); SDL_zeroa(explanation_roomnames); n_untranslated_roomnames = 0; n_unexplained_roomnames = 0; SDL_zeroa(n_untranslated_roomnames_area); SDL_zeroa(n_untranslated); map_translation_roomnames_special = hashmap_create(); } resettext_custom(final_shutdown); } bool parse_max(const char* max, unsigned short* max_w, unsigned short* max_h) { /* Parse a max string, like "33" or "33*3", into two shorts. * Returns true if successful and max_w/max_h have gotten valid values, false otherwise. */ if (max == NULL) { return false; } char* max_mut = SDL_strdup(max); if (max_mut == NULL) { return false; } char* asterisk = SDL_strchr(max_mut, '*'); if (asterisk != NULL) { asterisk[0] = '\0'; *max_h = (unsigned short) help.Int(&asterisk[1], 0); } else { *max_h = 1; } *max_w = (unsigned short) help.Int(max_mut, 0); VVV_free(max_mut); return *max_w != 0 && *max_h != 0; } static bool max_check_string(const char* str, const char* max) { /* Stores a detected overflow in the overflows vector, returns true if this happened */ unsigned short max_w, max_h; if (str == NULL || !parse_max(max, &max_w, &max_h)) { return false; } /* Special case that must ALWAYS be 2 lines even when the font is bigger */ if (SDL_strcmp(str, "You have rescued a crew member!") == 0 && max_h == 1) { max_h = 2; } uint8_t font_idx = get_langmeta()->font_idx; uint32_t print_flags = PR_FONT_IDX(font_idx, get_langmeta()->rtl) | PR_CJK_LOW; uint8_t font_w = 8; uint8_t font_h = 8; font::glyph_dimensions(print_flags, &font_w, &font_h); unsigned short max_w_px = max_w * 8; unsigned short max_h_px = max_h * 10; bool does_overflow = false; if (max_h == 1) { max_h_px = font_h; does_overflow = font::len(print_flags, str) > (int) max_w_px; } else { short lines; font::string_wordwrap(print_flags, str, max_w_px, &lines); does_overflow = lines*SDL_max(10, font_h) > (short) max_h_px; } // Convert max_w and max_h from 8x8 into local max_w = max_w_px / font_w; max_h = max_h_px / SDL_max(10, font_h); if (does_overflow) { TextOverflow overflow; overflow.lang = lang; overflow.text = textbook_store(&textbook_main, str); overflow.max_w = max_w; overflow.max_h = max_h; overflow.max_w_px = max_w_px; overflow.max_h_px = max_h_px; overflow.multiline = max_h > 1; overflow.flags = print_flags; text_overflows.push_back(overflow); vlog_warn("\"%s\" DOESN'T FIT into %s which is %dx%d or %dx%dpx", str, max, max_w, max_h, max_w_px, max_h_px ); } else { vlog_debug("\"%s\" fits into %s which is %dx%d or %dx%dpx", str, max, max_w, max_h, max_w_px, max_h_px ); } return does_overflow; } static void max_check_string_plural(unsigned char form, const char* str, const char* max, const char* var, unsigned int expect) { if (str == NULL || var == NULL) { return; } /* Create an args index from just the name of the variable. * Also get rid of all other placeholders.*/ char args_index[60]; vformat_buf(args_index, sizeof(args_index), "{var}:int, _:int", "var:str", var); char buf[20*SCREEN_WIDTH_CHARS + 1]; if (expect > 100) { /* Treat `expect` as a single example, it's the number of digits that's most important */ if (form_for_count(expect) == form) { vformat_buf(buf, sizeof(buf), str, args_index, expect, 0); max_check_string(buf, max); } } else { /* Test all numbers from 0 to `expect`, since if we have wordy numbers, they have differing lengths */ for (unsigned int test = 0; test <= expect; test++) { if (form_for_count(test) == form) { vformat_buf(buf, sizeof(buf), str, args_index, test, 0); if (max_check_string(buf, max)) { /* One is enough */ break; } } } } } static void tally_untranslated(const char* tra, int* counter) { /* Count this translation in the untranslated count if it's untranslated. */ if (!show_translator_menu) { return; } if (tra == NULL || tra[0] == '\0') { (*counter)++; } } static void loadtext_strings(bool check_max) { tinyxml2::XMLDocument doc; tinyxml2::XMLHandle hDoc(&doc); tinyxml2::XMLElement* pElem; if (!load_lang_doc("strings", doc)) { return; } FOR_EACH_XML_ELEMENT(hDoc, pElem) { EXPECT_ELEM(pElem, "string"); const char* eng = pElem->Attribute("english"); const char* tra = pElem->Attribute("translation"); char textcase = pElem->UnsignedAttribute("case", 0); if (textcase == 0) { map_store_translation( &textbook_main, map_translation, eng, tra ); } else { /* Only prefix with a disambiguator if a specific case number is set */ char* eng_prefixed = add_disambiguator(textcase, eng, NULL); if (eng_prefixed == NULL) { continue; } map_store_translation( &textbook_main, map_translation, eng_prefixed, tra ); VVV_free(eng_prefixed); } /* Only tally an untranslated string if English isn't blank */ if (eng != NULL && eng[0] != '\0') { tally_untranslated(tra, &n_untranslated[UNTRANSLATED_STRINGS]); } if (check_max) { /* VFormat placeholders distort the limits check. * (max_check_string ignores NULL strings.) */ char* filled = vformat_alloc(tra, "_:int", 0); max_check_string(filled, pElem->Attribute("max")); VVV_free(filled); } } } static void loadtext_strings_plural(bool check_max) { tinyxml2::XMLDocument doc; tinyxml2::XMLHandle hDoc(&doc); tinyxml2::XMLElement* pElem; if (!load_lang_doc("strings_plural", doc)) { return; } FOR_EACH_XML_ELEMENT(hDoc, pElem) { EXPECT_ELEM(pElem, "string"); const char* eng_plural = pElem->Attribute("english_plural"); if (eng_plural == NULL) { continue; } tinyxml2::XMLElement* subElem; FOR_EACH_XML_SUB_ELEMENT(pElem, subElem) { EXPECT_ELEM(subElem, "translation"); unsigned char form = subElem->IntAttribute("form", 0); char* key = add_disambiguator(form+1, eng_plural, NULL); if (key == NULL) { continue; } map_store_translation( &textbook_main, map_translation_plural, key, subElem->Attribute("translation") ); VVV_free(key); tally_untranslated(subElem->Attribute("translation"), &n_untranslated[UNTRANSLATED_STRINGS_PLURAL]); if (check_max) { max_check_string_plural( form, subElem->Attribute("translation"), pElem->Attribute("max"), pElem->Attribute("var"), pElem->UnsignedAttribute("expect", 101) ); } } } } static bool get_level_lang_path(bool custom_level, const char* cat, std::string& doc_path, std::string& doc_path_asset) { /* Calculate the path to a translation file for either the MAIN GAME or * a CUSTOM LEVEL. cat can be "roomnames", "cutscenes", etc. * * doc_path and doc_path_asset are "out" parameters, and will be set to * the appropriate filenames to use for language files outside of or * inside level assets respectively (translations for custom levels can * live in the main language folders too) * * Returns whether this is a (valid) custom level path. */ if (custom_level && custom_level_path != NULL && SDL_strncmp(custom_level_path, "levels/", 7) == 0 && SDL_strlen(custom_level_path) > (sizeof(".vvvvvv")-1) ) { /* Get rid of .vvvvvv */ size_t len = SDL_strlen(custom_level_path)-7; doc_path = std::string(custom_level_path, len); doc_path.append("/custom_"); doc_path.append(cat); /* For the asset path, also get rid of the levels/LEVELNAME/ */ doc_path_asset = "custom_"; doc_path_asset.append(cat); return true; } else { doc_path = cat; doc_path_asset = ""; return false; } } const char* get_level_original_lang(tinyxml2::XMLHandle& hDoc) { /* cutscenes and roomnames files can specify the original language as * an attribute of the root tag to change the attribute names of the * original text (normally "english"). This makes level translations * less confusing if the original language isn't English. */ const char* original = NULL; tinyxml2::XMLElement* pRoot = hDoc.FirstChildElement().ToElement(); if (pRoot != NULL) { original = pRoot->Attribute("original"); } if (original == NULL) { original = "english"; } return original; } static std::string& get_level_lang_code(bool custom_level) { if (!custom_level || lang_custom == "") { return lang; } return lang_custom; } static void loadtext_cutscenes(bool custom_level) { tinyxml2::XMLDocument doc; tinyxml2::XMLHandle hDoc(&doc); tinyxml2::XMLElement* pElem; std::string doc_path; std::string doc_path_asset; bool valid_custom_level = get_level_lang_path(custom_level, "cutscenes", doc_path, doc_path_asset); if (custom_level && !valid_custom_level) { return; } if (!load_lang_doc(doc_path, doc, get_level_lang_code(custom_level), doc_path_asset)) { return; } Textbook* textbook; hashmap* map; if (custom_level) { textbook = &textbook_custom; map = map_translation_cutscene_custom; } else { textbook = &textbook_main; map = map_translation_cutscene; } const char* original = get_level_original_lang(hDoc); FOR_EACH_XML_ELEMENT(hDoc, pElem) { EXPECT_ELEM(pElem, "cutscene"); const char* script_id = textbook_store(textbook, pElem->Attribute("id")); if (script_id == NULL) { continue; } hashmap* cutscene_map = hashmap_create(); hashmap_set_free( map, script_id, SDL_strlen(script_id), (uintptr_t) cutscene_map, callback_free_map_value, NULL ); tinyxml2::XMLElement* subElem; FOR_EACH_XML_SUB_ELEMENT(pElem, subElem) { EXPECT_ELEM(subElem, "dialogue"); const char* eng = subElem->Attribute(original); const char* tra = subElem->Attribute("translation"); if (!custom_level) { tally_untranslated(tra, &n_untranslated[UNTRANSLATED_CUTSCENES]); } if (eng == NULL || tra == NULL) { continue; } const std::string eng_unwrapped = font::string_unwordwrap(eng); char* eng_prefixed = add_disambiguator(subElem->UnsignedAttribute("case", 1), eng_unwrapped.c_str(), NULL); if (eng_prefixed == NULL) { continue; } const char* tb_eng = textbook_store(textbook, eng_prefixed); const char* tb_tra = textbook_store(textbook, tra); VVV_free(eng_prefixed); if (tb_eng == NULL || tb_tra == NULL) { continue; } TextboxFormat format; format.text = tb_tra; format.tt = subElem->BoolAttribute("tt", false); format.centertext = subElem->BoolAttribute("centertext", false); format.pad_left = subElem->UnsignedAttribute("pad_left", 0); format.pad_right = subElem->UnsignedAttribute("pad_right", 0); unsigned short pad = subElem->UnsignedAttribute("pad", 0); format.pad_left += pad; format.pad_right += pad; format.wraplimit_raw = subElem->UnsignedAttribute("wraplimit", 0); format.wraplimit = format.wraplimit_raw; if (format.wraplimit == 0) { format.wraplimit = 36*8 - (format.pad_left+format.pad_right)*8; } format.padtowidth = subElem->UnsignedAttribute("padtowidth", 0); const TextboxFormat* tb_format = (TextboxFormat*) textbook_store_raw( textbook, &format, sizeof(TextboxFormat) ); if (tb_format == NULL) { continue; } hashmap_set(cutscene_map, tb_eng, SDL_strlen(tb_eng), (uintptr_t) tb_format); } } } static void loadtext_numbers(void) { tinyxml2::XMLDocument doc; tinyxml2::XMLHandle hDoc(&doc); tinyxml2::XMLElement* pElem; if (!load_lang_doc("numbers", doc)) { return; } FOR_EACH_XML_ELEMENT(hDoc, pElem) { EXPECT_ELEM(pElem, "number"); const char* value_str = pElem->Attribute("value"); int value = help.Int(value_str); if (value >= 0 && value <= 100) { const char* tra = pElem->Attribute("translation"); if (tra == NULL) { tra = ""; } number[value] = std::string(tra); tally_untranslated(tra, &n_untranslated[UNTRANSLATED_NUMBERS]); // FIXME: implement a more flexible system later, where translators define the classes tra = pElem->Attribute("translation2"); if (tra == NULL) { tra = ""; } number2[value] = std::string(tra); } if (value >= 0 && value <= 199) { int form = pElem->IntAttribute("form", 0); number_plural_form[value] = form; if (value < 100) { number_plural_form[value+100] = form; } } } } bool fix_room_coords(bool custom_level, int* roomx, int* roomy) { *roomx %= 100; *roomy %= 100; if (!custom_level && *roomx == 9 && *roomy == 4) { // The Tower has two rooms, unify them *roomy = 9; } int max_x = MAP_MAX_X; int max_y = MAP_MAX_Y; if (custom_level) { max_x = CUSTOM_MAP_MAX_X; max_y = CUSTOM_MAP_MAX_Y; } return !(*roomx < 0 || *roomy < 0 || *roomx > max_x || *roomy > max_y); } static unsigned coords_to_area(int roomx, int roomy) { if (!fix_room_coords(false, &roomx, &roomy)) { return false; } /* We want to know per-area how many room names are untranslated... */ enum area_letter { _, /* None */ S, /* SS1 */ L, /* Lab */ T, /* Tower */ Y, /* SS2 */ W, /* Warp */ I, /* Intermission */ G, /* Gravitron */ F /* Final */ }; static enum area_letter area_map[MAP_MAX_Y+1][MAP_MAX_Y+1] = { {_,L,L,L,L,L,L,L,_,T,_,_,_,W,W,W,W,W,W,W}, {_,L,L,L,L,L,L,_,_,T,_,_,_,_,W,W,W,W,W,W}, {_,_,_,_,L,_,_,_,_,T,_,_,_,_,W,W,W,W,W,W}, {_,_,_,_,L,_,_,_,_,T,_,_,S,S,S,S,W,W,W,W}, {_,_,L,L,L,_,_,_,_,T,T,T,S,S,S,S,_,_,_,_}, {_,_,_,_,_,_,_,_,_,T,Y,Y,S,S,S,S,_,_,_,_}, {_,_,_,_,_,_,_,_,_,T,Y,Y,S,S,S,S,S,_,_,_}, {_,_,_,_,_,_,_,_,_,T,Y,Y,S,S,S,S,S,S,S,_}, {_,_,_,_,_,_,_,_,_,T,_,_,_,Y,Y,S,Y,Y,Y,_}, {_,_,_,_,_,_,_,_,T,T,_,_,_,Y,Y,Y,Y,Y,Y,_}, {_,_,_,_,_,_,_,_,_,T,_,_,_,Y,Y,Y,Y,Y,Y,_}, {_,_,_,_,_,_,_,_,_,T,_,Y,Y,Y,Y,Y,Y,Y,Y,_}, {_,_,_,_,_,_,_,_,_,T,_,Y,Y,Y,Y,Y,Y,_,Y,_}, {_,_,_,_,_,_,_,_,_,T,_,Y,Y,Y,Y,Y,Y,_,Y,_}, {_,_,_,_,_,_,_,_,_,T,_,Y,Y,_,_,_,_,_,Y,_}, {_,_,_,_,_,_,_,L,_,T,_,_,_,_,_,_,_,_,_,_}, {_,_,L,L,L,L,L,L,_,T,_,_,_,_,_,_,_,_,_,_}, {_,L,L,L,L,L,L,L,_,T,_,_,_,_,_,_,_,_,_,_}, {L,L,L,L,L,_,_,L,_,T,_,_,_,_,_,_,_,_,_,_}, {L,L,L,L,L,_,_,L,_,T,_,_,_,_,_,_,_,_,_,_} }; static bool area_map_has_final = false; if (!area_map_has_final) { static const enum area_letter final_map[9][14] = { {_,_,_,_,_,_,_,_,_,_,_,_,G,F}, {_,_,_,_,_,_,_,_,_,_,_,_,G,F}, {_,_,_,_,_,_,_,_,_,_,_,_,G,F}, {F,F,F,F,F,F,F,F,F,F,_,_,G,F}, {F,F,F,F,F,_,F,F,F,F,_,_,G,F}, {_,_,_,_,_,_,_,_,F,F,F,F,F,F}, {_,_,_,_,_,F,F,F,F,F,F,_,_,_}, {_,_,_,_,_,_,_,_,_,_,_,_,_,_}, {I,I,I,I,I,I,I,I,I,I,I,I,I,I} }; for (int y = 0; y < 9; y++) { for (int x = 0; x < 14; x++) { area_map[MAP_MAX_Y+1 - 9 + y][MAP_MAX_X+1 - 14 + x] = final_map[y][x]; } } area_map_has_final = true; } if (area_map[roomy][roomx] == 0) { vlog_error("LocalizationStorage: Room %d,%d has no area associated with it", roomx, roomy); } return area_map[roomy][roomx]; } static void update_left_counter(const char* old_text, const char* new_text, int* counter, int* counter_area) { bool now_filled = new_text[0] != '\0'; if ((old_text == NULL || old_text[0] == '\0') && now_filled) { (*counter)--; if (counter_area != NULL) { (*counter_area)--; } } else if (old_text != NULL && old_text[0] != '\0' && !now_filled) { (*counter)++; if (counter_area != NULL) { (*counter_area)++; } } } bool store_roomname_translation(bool custom_level, int roomx, int roomy, const char* tra, const char* explanation) { if (!fix_room_coords(custom_level, &roomx, &roomy)) { return false; } /* We have some arrays filled with pointers, and we need to change those pointers */ const char** ptr_translation; const char** ptr_explanation; int* ptr_n_untranslated; int* ptr_n_untranslated_area = NULL; int* ptr_n_unexplained; if (custom_level) { ptr_translation = &translation_roomnames_custom[roomy][roomx]; ptr_explanation = &explanation_roomnames_custom[roomy][roomx]; ptr_n_untranslated = &n_untranslated_roomnames_custom; ptr_n_unexplained = &n_unexplained_roomnames_custom; } else { ptr_translation = &translation_roomnames[roomy][roomx]; ptr_explanation = &explanation_roomnames[roomy][roomx]; ptr_n_untranslated = &n_untranslated_roomnames; ptr_n_untranslated_area = &n_untranslated_roomnames_area[coords_to_area(roomx, roomy)]; ptr_n_unexplained = &n_unexplained_roomnames; } if (tra != NULL) { update_left_counter(*ptr_translation, tra, ptr_n_untranslated, ptr_n_untranslated_area); *ptr_translation = textbook_store(&textbook_main, tra); } if (explanation != NULL) { update_left_counter(*ptr_explanation, explanation, ptr_n_unexplained, NULL); *ptr_explanation = textbook_store(&textbook_main, explanation); } return true; } static void loadtext_roomnames(bool custom_level, bool check_max) { tinyxml2::XMLDocument doc; tinyxml2::XMLHandle hDoc(&doc); tinyxml2::XMLElement* pElem; std::string doc_path; std::string doc_path_asset; bool valid_custom_level = get_level_lang_path(custom_level, "roomnames", doc_path, doc_path_asset); if (custom_level && !valid_custom_level) { return; } if (!load_lang_doc(doc_path, doc, get_level_lang_code(custom_level), doc_path_asset)) { return; } const char* original = get_level_original_lang(hDoc); FOR_EACH_XML_ELEMENT(hDoc, pElem) { EXPECT_ELEM(pElem, "roomname"); int x = pElem->IntAttribute("x", -1); int y = pElem->IntAttribute("y", -1); if (custom_level) { /* Extra safeguard: make sure the original room name matches! */ const char* original_roomname = pElem->Attribute(original); if (original_roomname == NULL) { continue; } const RoomProperty* const room = cl.getroomprop(x, y); if (SDL_strcmp(original_roomname, room->roomname.c_str()) != 0) { continue; } n_untranslated_roomnames_custom++; n_unexplained_roomnames_custom++; } else { n_untranslated_roomnames++; n_unexplained_roomnames++; n_untranslated_roomnames_area[coords_to_area(x, y)]++; if (check_max) { max_check_string(pElem->Attribute("translation"), "40"); } } store_roomname_translation( custom_level, x, y, pElem->Attribute("translation"), show_translator_menu ? pElem->Attribute("explanation") : NULL ); } } static void loadtext_roomnames_special(bool check_max) { tinyxml2::XMLDocument doc; tinyxml2::XMLHandle hDoc(&doc); tinyxml2::XMLElement* pElem; if (!load_lang_doc("roomnames_special", doc)) { return; } FOR_EACH_XML_ELEMENT(hDoc, pElem) { EXPECT_ELEM(pElem, "roomname"); map_store_translation( &textbook_main, map_translation_roomnames_special, pElem->Attribute("english"), pElem->Attribute("translation") ); if (check_max) { max_check_string(pElem->Attribute("translation"), "40"); } tally_untranslated(pElem->Attribute("translation"), &n_untranslated[UNTRANSLATED_ROOMNAMES_SPECIAL]); } } void loadtext_custom(const char* custom_path) { resettext_custom(false); if (custom_level_path == NULL && custom_path != NULL) { custom_level_path = SDL_strdup(custom_path); } loadtext_cutscenes(true); loadtext_roomnames(true, false); } void loadtext(bool check_max) { resettext(false); loadmeta(langmeta); if (lang == "en") { if (show_translator_menu) { // We may still need the room name explanations loadtext_roomnames(false, false); n_untranslated_roomnames = 0; SDL_zeroa(n_untranslated_roomnames_area); } } else { loadtext_numbers(); loadtext_strings(check_max); loadtext_strings_plural(check_max); loadtext_cutscenes(false); loadtext_roomnames(false, check_max); loadtext_roomnames_special(check_max); } if (custom_level_path != NULL) { loadtext_custom(NULL); } } void loadlanguagelist(void) { // Load the list of languages for the language screen languagelist.clear(); size_t opt = 0; languagelist_curlang = 0; EnumHandle handle = {}; const char* code; while ((code = FILESYSTEM_enumerateLanguageCodes(&handle)) != NULL) { LangMeta meta; loadmeta(meta, code); if (meta.active) { languagelist.push_back(meta); if (SDL_strcmp(lang.c_str(), code) == 0) { languagelist_curlang = opt; } opt++; } } FILESYSTEM_freeEnumerate(&handle); } const char* map_lookup_text(hashmap* map, const char* eng, const char* fallback) { uintptr_t ptr_tra; bool found = hashmap_get(map, eng, SDL_strlen(eng), &ptr_tra); const char* tra = (const char*) ptr_tra; if (found && tra != NULL && tra[0] != '\0') { return tra; } return fallback; } char* add_disambiguator(char disambiguator, const char* original_string, size_t* ext_alloc_len) { /* Create a version of the string prefixed with the given byte. * This byte is used when the English string is just not enough to identify the correct translation. * It's needed to store plural forms, and when the same text appears multiple times in a cutscene. * Caller must VVV_free. */ size_t alloc_len = 1+SDL_strlen(original_string)+1; char* alloc = (char*) SDL_malloc(alloc_len); if (alloc == NULL) { return NULL; } alloc[0] = disambiguator; SDL_memcpy(&alloc[1], original_string, alloc_len-1); if (ext_alloc_len != NULL) { *ext_alloc_len = alloc_len; } return alloc; } } /* namespace loc */