diff --git a/desktop_version/src/Game.cpp b/desktop_version/src/Game.cpp index 84ba2851..24b9647a 100644 --- a/desktop_version/src/Game.cpp +++ b/desktop_version/src/Game.cpp @@ -6189,6 +6189,7 @@ void Game::createmenu( enum Menu::MenuName t, bool samemenu/*= false*/ ) option("audio"); option("game pad"); option("accessibility"); + option(loc::gettext("language")); option("return"); menuyoff = 0; maxspacing = 15; diff --git a/desktop_version/src/Localization.cpp b/desktop_version/src/Localization.cpp new file mode 100644 index 00000000..8e9e2d60 --- /dev/null +++ b/desktop_version/src/Localization.cpp @@ -0,0 +1,389 @@ +#define LOCALIZATION_CPP +#include "Localization.h" +#include "LocalizationStorage.h" + +#include + +#include "Game.h" +#include "UtilityClass.h" +#include "VFormat.h" + +namespace loc +{ + +bool lang_set = false; +std::string lang = "en"; +std::string lang_custom = ""; +LangMeta langmeta; + +// language screen list +std::vector languagelist; +int languagelist_curlang; +bool show_translator_menu = false; +size_t limitscheck_current_overflow; + +int n_untranslated_roomnames = 0; +int n_unexplained_roomnames = 0; +int n_untranslated_roomnames_custom = 0; +int n_unexplained_roomnames_custom = 0; + +int n_untranslated[COUNT_UNTRANSLATED_INDEX] = {0}; + +const LangMeta* get_langmeta(void) +{ + if (game.currentmenuname == Menu::language && (unsigned)game.currentmenuoption < languagelist.size()) + { + return &languagelist[game.currentmenuoption]; + } + + return &langmeta; +} + +const char* gettext(const char* eng) +{ + if (lang == "en") + { + return eng; + } + + return map_lookup_text(map_translation, eng, eng); +} + +static const char* gettext_plural_english(const char* eng_plural, const char* eng_singular, int n) +{ + /* Do be consistent with negative number handling for other languages... */ + if (n == 1 || n == -1) + { + return eng_singular; + } + return eng_plural; +} + +const char* gettext_plural(const char* eng_plural, const char* eng_singular, int n) +{ + if (lang != "en") + { + unsigned char form = form_for_count(n); + char* key = add_disambiguator(form+1, eng_plural, NULL); + if (key != NULL) + { + const char* tra = map_lookup_text(map_translation_plural, key, NULL); + + SDL_free(key); + + if (tra != NULL) + { + return tra; + } + } + } + return gettext_plural_english(eng_plural, eng_singular, n); +} + +void gettext_plural_fill(char* buf, size_t buf_len, const char* eng_plural, const char* eng_singular, const char* args_index, ...) +{ + /* Choose the right plural string based on a number, and then vformat that string. + * The first vararg determines the specific plural form. */ + + va_list args; + va_start(args, args_index); + int count = va_arg(args, int); + va_end(args); + + const char* tra = gettext_plural(eng_plural, eng_singular, count); + + va_start(args, args_index); + vformat_buf_valist(buf, buf_len, tra, args_index, args); + va_end(args); +} + +std::string getnumber(int n) +{ + if (n < 0 || n > 100) + { + return help.String(n); + } + + if (number[n].empty()) + { + return help.String(n); + } + return number[n]; +} + +static bool is_script_custom(const char* script_id) +{ + return SDL_strncmp(script_id, "custom_", 7) == 0; +} + +const TextboxFormat* gettext_cutscene(const std::string& script_id, const std::string& eng, char textcase) +{ + hashmap* map; + const char* map_script_key; + if (is_script_custom(script_id.c_str())) + { + map = map_translation_cutscene_custom; + map_script_key = &script_id.c_str()[7]; + } + else + { + if (lang == "en") + { + return NULL; + } + + map = map_translation_cutscene; + map_script_key = script_id.c_str(); + } + + uintptr_t ptr_cutscene_map; + bool found = hashmap_get(map, (void*) map_script_key, SDL_strlen(map_script_key), &ptr_cutscene_map); + hashmap* cutscene_map = (hashmap*) ptr_cutscene_map; + + if (!found || cutscene_map == NULL) + { + return NULL; + } + + size_t alloc_len; + char* key = add_disambiguator(textcase, eng.c_str(), &alloc_len); + if (key == NULL) + { + return NULL; + } + + uintptr_t ptr_format; + found = hashmap_get(cutscene_map, (void*) key, alloc_len-1, &ptr_format); + const TextboxFormat* format = (TextboxFormat*) ptr_format; + + SDL_free(key); + + if (!found) + { + return NULL; + } + return format; +} + +const char* get_roomname_explanation(bool custom_level, int roomx, int roomy) +{ + /* Never returns NULL. */ + + if (!fix_room_coords(custom_level, &roomx, &roomy)) + { + return ""; + } + + const char* explanation; + if (custom_level) + { + explanation = explanation_roomnames_custom[roomy][roomx]; + } + else + { + explanation = explanation_roomnames[roomy][roomx]; + } + if (explanation == NULL) + { + return ""; + } + return explanation; +} + +const char* get_roomname_translation(bool custom_level, int roomx, int roomy) +{ + /* Only looks for the translation, doesn't return English fallback. + * Never returns NULL. + * Also used for room name translation mode. */ + + if (!fix_room_coords(custom_level, &roomx, &roomy)) + { + return ""; + } + + const char* tra; + if (custom_level) + { + tra = translation_roomnames_custom[roomy][roomx]; + } + else + { + tra = translation_roomnames[roomy][roomx]; + } + + if (tra == NULL) + { + return ""; + } + return tra; +} + +const char* gettext_roomname(bool custom_level, int roomx, int roomy, const char* eng, bool special) +{ + if (!custom_level && lang == "en") + { + return eng; + } + + if (special) + { + return gettext_roomname_special(eng); + } + + const char* tra = get_roomname_translation(custom_level, roomx, roomy); + if (tra[0] == '\0') + { + return eng; + } + return tra; +} + +const char* gettext_roomname_special(const char* eng) +{ + if (lang == "en") + { + return eng; + } + + return map_lookup_text(map_translation_roomnames_special, eng, eng); +} + +bool is_cutscene_translated(const std::string& script_id) +{ + hashmap* map; + const char* map_script_key; + if (is_script_custom(script_id.c_str())) + { + map = map_translation_cutscene_custom; + map_script_key = &script_id.c_str()[7]; + } + else + { + if (lang == "en") + { + return false; + } + + map = map_translation_cutscene; + map_script_key = script_id.c_str(); + } + + uintptr_t ptr_unused; + return hashmap_get(map, (void*) map_script_key, SDL_strlen(map_script_key), &ptr_unused); +} + +uint32_t toupper_ch(uint32_t ch) +{ + // Convert a single Unicode codepoint to its uppercase variant + // Supports important Latin (1 and A), Cyrillic and Greek + + // Turkish i? + if (get_langmeta()->toupper_i_dot && ch == 'i') return 0x130; + + // a-z? + if ('a' <= ch && ch <= 'z') return ch - 0x20; + + // Latin-1 Supplement? But not the division sign + if (0xE0 <= ch && ch <= 0xFE && ch != 0xF7) return ch - 0x20; + + // ß? Yes, we do have this! And otherwise we could only replace it with SS later on. + if (ch == 0xDF) return 0x1E9E; + + // ÿ? + if (ch == 0xFF) return 0x178; + + // Let's get some exceptions for Latin Extended-A out of the way, starting with ı + if (ch == 0x131) return 'I'; + + // This range between two obscure exceptions... + if (0x139 <= ch && ch <= 0x148 && ch % 2 == 0) return ch - 1; + + // The rest of Latin Extended-A? + if (0x100 <= ch && ch <= 0x177 && ch % 2 == 1) return ch - 1; + + // Okay, Ÿ also pushed some aside... + if (0x179 <= ch && ch <= 0x17E && ch % 2 == 0) return ch - 1; + + // Can't hurt to support Romanian properly... + if (ch == 0x219 || ch == 0x21B) return ch - 1; + + // Cyrillic а-я? + if (0x430 <= ch && ch <= 0x44F) return ch - 0x20; + + // There's probably a good reason Cyrillic upper and lower accents are wrapped around the alphabet... + if (0x450 <= ch && ch <= 0x45F) return ch - 0x50; + + // Apparently a Ukrainian letter is all the way over there, why not. + if (ch == 0x491) return ch - 1; + + // Time for Greek, thankfully we're not making a lowercasing function with that double sigma! + if (ch == 0x3C2) return 0x3A3; + + // The entire Greek alphabet then, along with two accented letters + if (0x3B1 <= ch && ch <= 0x3CB) return ch - 0x20; + + // Unfortunately Greek accented letters are all over the place. + if (ch == 0x3AC) return 0x386; + if (0x3AD <= ch && ch <= 0x3AF) return ch - 0x25; + if (ch == 0x3CC) return 0x38C; + if (ch == 0x3CD || ch == 0x3CE) return ch - 0x3F; + + // Nothing matched! Just leave it as is + return ch; +} + +std::string toupper(const std::string& lower) +{ + // Convert a UTF-8 string to uppercase + if (!get_langmeta()->toupper) + { + return lower; + } + + std::string upper; + std::back_insert_iterator inserter = std::back_inserter(upper); + std::string::const_iterator iter = lower.begin(); + bool ignorenext = false; + while (iter != lower.end()) + { + uint32_t ch = utf8::unchecked::next(iter); + + if (get_langmeta()->toupper_lower_escape_char && ch == '~') + { + ignorenext = true; + continue; + } + + if (!ignorenext) + { + ch = toupper_ch(ch); + } + utf8::unchecked::append(ch, inserter); + + ignorenext = false; + } + + return upper; +} + +std::string remove_toupper_escape_chars(const std::string& _s) +{ + // No-op, except if langmeta.toupper_lower_escape_char, to remove the ~ escape character + + if (!get_langmeta()->toupper_lower_escape_char) + { + return _s; + } + + std::string s = std::string(_s); + for (signed int i = s.size()-1; i >= 0; i--) + { + if (s[i] == '~') + { + s.erase(i, 1); + } + } + return s; +} + +} /* namespace loc */ diff --git a/desktop_version/src/Localization.h b/desktop_version/src/Localization.h new file mode 100644 index 00000000..4ef98a2a --- /dev/null +++ b/desktop_version/src/Localization.h @@ -0,0 +1,93 @@ +#ifndef LOCALIZATION_H +#define LOCALIZATION_H + +#include +#include +#include + +/* The translator menu will appear in any of the following circumstances: + * - The "lang" folder is NOT next to data.zip, but it is found in "desktop_version" within which the game is running + * - The command line argument "-translator" is passed + * - ALWAYS_SHOW_TRANSLATOR_MENU is defined + */ +// #define ALWAYS_SHOW_TRANSLATOR_MENU + +namespace loc +{ + +struct LangMeta +{ + bool active; // = true, language is shown in the list + std::string code; + std::string nativename; + std::string credit; + std::string action_hint; + bool autowordwrap; // = true; enable automatic wordwrapping + bool toupper; // = true; enable automatic full-caps for menu options + bool toupper_i_dot; // = false; enable Turkish i mapping when uppercasing + bool toupper_lower_escape_char; // = false; enable ~ to mark lowercase letters for uppercasing + std::string menu_select; + std::string menu_select_tight; + unsigned char font_w; + unsigned char font_h; +}; + +struct TextboxFormat +{ + const char* text; + unsigned short wraplimit; // = 36*8-pad_left-pad_right; no effect if tt or !langmeta.autowordwrap + unsigned short wraplimit_raw; // original value of wraplimit, only used for language file sync + bool tt; // teletype, don't auto-wordwrap + bool centertext; // whether the text should be centered inside the box + unsigned char pad_left; // pad with X characters + unsigned char pad_right; + unsigned short padtowidth; // pad to X pixels (0 to disable) +}; + +extern bool lang_set; +extern std::string lang; +extern std::string lang_custom; +extern LangMeta langmeta; +extern std::vector languagelist; +extern int languagelist_curlang; +extern bool show_translator_menu; +extern size_t limitscheck_current_overflow; + +extern int n_untranslated_roomnames; +extern int n_unexplained_roomnames; +extern int n_untranslated_roomnames_custom; +extern int n_unexplained_roomnames_custom; + +enum n_untranslated_index +{ + UNTRANSLATED_STRINGS = 0, + UNTRANSLATED_NUMBERS, + UNTRANSLATED_STRINGS_PLURAL, + UNTRANSLATED_CUTSCENES, + UNTRANSLATED_ROOMNAMES_SPECIAL, + COUNT_UNTRANSLATED_INDEX +}; +extern int n_untranslated[COUNT_UNTRANSLATED_INDEX]; + + +const LangMeta* get_langmeta(void); + +const char* gettext(const char* eng); +const char* gettext_plural(const char* eng_plural, const char* eng_singular, int count); +void gettext_plural_fill(char* buf, size_t buf_len, const char* eng_plural, const char* eng_singular, const char* args_index, ...); +std::string getnumber(int n); +const TextboxFormat* gettext_cutscene(const std::string& script_id, const std::string& eng, char textcase); +const char* get_roomname_explanation(bool custom_level, int roomx, int roomy); +const char* get_roomname_translation(bool custom_level, int roomx, int roomy); +const char* gettext_roomname(bool custom_level, int roomx, int roomy, const char* eng, bool special); +const char* gettext_roomname_special(const char* eng); + +bool is_cutscene_translated(const std::string& script_id); + +uint32_t toupper_ch(uint32_t ch); +std::string toupper(const std::string& lower); +std::string remove_toupper_escape_chars(const std::string& _s); + +} /* namespace loc */ + +#endif /* LOCALIZATION_H */