From 8de5f6b1e25931b9c658bcfc0b8e778871b4fc68 Mon Sep 17 00:00:00 2001 From: "Seth N. Hetu" Date: Fri, 16 Oct 2020 17:54:22 -0400 Subject: [PATCH 1/5] Initial Work on Using Translations Via LcfTrans commit fd9b9e41444c0e322356ef18a3b6be61a824c90d Author: Seth N. Hetu Date: Fri Oct 16 13:48:31 2020 -0400 Quick fix for new LcfTrans organization of files commit b61a5e5bf57d128071e3ce0053f8c95e8ec17f63 Author: Seth N. Hetu Date: Tue Sep 29 01:24:07 2020 -0400 Fix a minor issue with standalone choice boxes commit 359315a39bcd572e832ce460b6381773d5c987fc Author: Seth N. Hetu Date: Tue Sep 29 01:09:34 2020 -0400 Minor fix for case where a message box at the end of the event list needs to be expanded commit 249f37050d0485cdaedb5782deee6d049a49c43e Author: Seth N. Hetu Date: Mon Sep 28 21:21:56 2020 -0400 Add in Treemap translations commit de06e5b1b0426a2353e66659e3a0db0fbed1427c Author: Seth N. Hetu Date: Mon Sep 28 21:10:19 2020 -0400 Clean up the translation code a bit commit 3fcd92a76b7fd3dd180b7bd5e709b67e8a014377 Author: Seth N. Hetu Date: Mon Sep 28 19:23:31 2020 -0400 Update doc strings commit 5a0607dd3c70fc3888fefea2d8c3e2b803848ef7 Author: Seth N. Hetu Date: Mon Sep 28 17:24:56 2020 -0400 Documentation and cleanup commit 1931fe9d64e87e0423a2c5d7a77c4e0c949821d5 Author: Seth N. Hetu Date: Mon Sep 28 17:11:26 2020 -0400 More consistent naming of variables, where possible commit 923e135420bb3e378c353c5fc4036f5048ed3a35 Author: Seth N. Hetu Date: Mon Sep 28 16:59:33 2020 -0400 Revert game_interpreter and game_system changes commit 67aa5d03e2f51c7b5ae14caac999a90784be0c9d Author: Seth N. Hetu Date: Mon Sep 28 16:57:37 2020 -0400 Cleanup commit 0e5e5ad8d1614eef6c992f7f9ba1be97c89841f9 Author: Seth N. Hetu Date: Mon Sep 28 16:53:33 2020 -0400 Revert async_handler changes commit d5cd3c816033a9f62cc5e48f76168bb8162f1103 Author: Seth N. Hetu Date: Mon Sep 28 10:08:09 2020 -0400 Switch to Before logic and fix memory error commit cf661d4e3e8718102930fe0834320248ba213534 Author: Seth N. Hetu Date: Mon Sep 28 01:38:10 2020 -0400 Initial work on AsyncRequest for audio commit 1cd38dd8697a1934cf6986ad0331cb1b99a086cf Author: Seth N. Hetu Date: Mon Sep 28 00:51:26 2020 -0400 Some indenting cleanup commit 91741b344c600af52f3270e7ae167280e111e6b9 Author: Seth N. Hetu Date: Mon Sep 28 00:45:43 2020 -0400 Allow inserting and removing message boxes with the same style. commit e2e5d6c6f3bc6711d098beecddeaa7b487e8eb0f Author: Seth N. Hetu Date: Sun Sep 27 23:27:48 2020 -0400 Rewrite battle events, and fix a bug where no translation overwrote the original translation with nothing commit 9c1df5dd67eab8bb7b8f0ad972cc9eb0e30950c8 Author: Seth N. Hetu Date: Sun Sep 27 22:44:02 2020 -0400 Fix changes from merging commit f6b16178c24d73fb21b07c2c4ad15f30dd603120 Author: Seth N. Hetu Date: Sun Sep 27 22:21:54 2020 -0400 Fix common/battle import, and actually use common commit 2508f77fdafdbaad117467b06152b6a0073a29c9 Author: Seth N. Hetu Date: Sat Sep 26 22:54:13 2020 -0400 Fix invalid choice parameter option, and remove dead code commit 390dcea1f595b31d72df04e45b5c58db25a6dea2 Author: Seth N. Hetu Date: Sat Sep 26 22:32:28 2020 -0400 Avoid RTP search on translated pictures commit 7caa40c5b2683156b2a0f34a493b436766e09e62 Author: Seth N. Hetu Date: Sat Sep 26 20:51:27 2020 -0400 Better translation iterator-like class; fixed an issue with indexes not being updated commit 3c057ed2040fb65044975827c9216005ba6cf01e Author: Seth N. Hetu Date: Fri Sep 25 20:16:47 2020 -0400 Translate the map directly when switching to the map, instead of trying to do it at runtime when the message box pops up commit b35f1dfada80cf3b6a4d5a475f24e899dbc29e22 Author: Seth N. Hetu Date: Thu Sep 24 03:01:27 2020 -0400 Choice boxes work, although it's a little hacky. commit 4052ad630f957909542980581dd439d10a832596 Author: Seth N. Hetu Date: Thu Sep 24 02:30:54 2020 -0400 Fix a few errors with messages not loading if substitutions are present (and Game_Actors not resetting substitutions). commit d4b7e90ad78eda7041aa0820a74f3ad90405993c Author: Seth N. Hetu Date: Tue Sep 15 00:18:21 2020 -0400 Load the Meta.ini file to retrieve the translation's name, and show Translation help text when appropriate. commit 118d3ed209b27182fa6be1e44f266acb2efab45a Author: Seth N. Hetu Date: Mon Sep 14 23:05:34 2020 -0400 Fix capitalization issues for language and root directory. commit 37d16222e314b13e0d43f50027e30f851048690d Author: Seth N. Hetu Date: Mon Sep 14 22:18:57 2020 -0400 Remove dynamic cast and replace GetEntry() with a faster TranslateString() commit 2acfba0f76a88220e319399a8be8a80c664c0e9a Author: Seth N. Hetu Date: Mon Sep 14 01:20:10 2020 -0400 Put CommandIndices into a struct to ensure nothing is missed on reset commit e7e9f12499635348574690d06097680fb8c94cec Author: Seth N. Hetu Date: Sun Sep 13 22:48:00 2020 -0400 Added in Ghabry's dictionary parsing code and removed tinygettext commit 97a819d0ec91d3431f6c41284df887651924e46c Author: Seth N. Hetu Date: Sun Sep 13 18:13:00 2020 -0400 Always reload the database on translation change to deal with cached entries commit 2f6d844b6fb26980c79d997e1eb8ff0a5db88a3c Author: Seth N. Hetu Date: Sun Sep 13 01:43:27 2020 -0400 Second draft Translation (Localization) code. Rewrites the database for non-messages. Hotswaps messages and assets. --- CMakeLists.txt | 2 + Makefile.am | 2 + src/filefinder.cpp | 25 +- src/game_map.cpp | 8 + src/meta.cpp | 6 + src/meta.h | 6 + src/player.cpp | 4 + src/player.h | 4 + src/scene_title.cpp | 143 ++++++- src/scene_title.h | 68 +++- src/translation.cpp | 784 ++++++++++++++++++++++++++++++++++++++ src/translation.h | 284 ++++++++++++++ src/window_selectable.cpp | 3 + src/window_selectable.h | 8 + 14 files changed, 1317 insertions(+), 30 deletions(-) create mode 100644 src/translation.cpp create mode 100644 src/translation.h diff --git a/CMakeLists.txt b/CMakeLists.txt index b97a532dda..c538e6c291 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -318,6 +318,8 @@ add_library(${PROJECT_NAME} STATIC src/transform.h src/transition.cpp src/transition.h + src/translation.cpp + src/translation.h src/util_macro.h src/utils.cpp src/utils.h diff --git a/Makefile.am b/Makefile.am index 25ac4adf46..bef4e39cf4 100644 --- a/Makefile.am +++ b/Makefile.am @@ -313,6 +313,8 @@ libeasyrpg_player_a_SOURCES = \ src/transform.h \ src/transition.cpp \ src/transition.h \ + src/translation.cpp \ + src/translation.h \ src/util_macro.h \ src/utils.cpp \ src/utils.h \ diff --git a/src/filefinder.cpp b/src/filefinder.cpp index 3a8e08381d..91f400cfd1 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -87,10 +87,14 @@ namespace { std::string FindFile(FileFinder::DirectoryTree const& tree, const std::string& dir, const std::string& name, - char const* exts[]) + char const* exts[], + bool translate=false) { using namespace FileFinder; + // Avoid searching entirely if there is no active translation + if (translate && Tr::CurrTranslationId().empty()) { return ""; } + #ifdef EMSCRIPTEN // The php filefinder should have given us an useable path std::string em_file = MakePath(dir, name); @@ -99,9 +103,9 @@ namespace { return em_file; #endif - std::string corrected_dir = lcf::ReaderUtil::Normalize(dir); + std::string corrected_dir = lcf::ReaderUtil::Normalize(translate?Tr::TranslationDir():dir); std::string const escape_symbol = Player::escape_symbol; - std::string corrected_name = lcf::ReaderUtil::Normalize(name); + std::string corrected_name = lcf::ReaderUtil::Normalize(translate?MakePath(MakePath(Tr::CurrTranslationId(), dir), name):name); std::string combined_path = MakePath(corrected_dir, corrected_name); std::string canon = MakeCanonical(combined_path, 1); @@ -229,9 +233,18 @@ namespace { return normal_search(); } - std::string FindFile(const std::string &dir, const std::string& name, const char* exts[]) { + std::string FindFile(const std::string &dir, const std::string& name, const char* exts[], bool tryTranslate=false) { + // Search for translated resources first. const std::shared_ptr tree = FileFinder::GetDirectoryTree(); - std::string ret = FindFile(*tree, dir, name, exts); + if (tryTranslate) { + std::string ret = FindFile(*tree, dir, name, exts, true); + if (!ret.empty()) { + return ret; + } + } + + // Try without translating. + std::string ret = FindFile(*tree, dir, name, exts, false); if (!ret.empty()) { return ret; } @@ -685,7 +698,7 @@ std::string FileFinder::FindImage(const std::string& dir, const std::string& nam #endif static const char* IMG_TYPES[] = { ".bmp", ".png", ".xyz", NULL }; - return FindFile(dir, name, IMG_TYPES); + return FindFile(dir, name, IMG_TYPES, true); } std::string FileFinder::FindDefault(const std::string& dir, const std::string& name) { diff --git a/src/game_map.cpp b/src/game_map.cpp index 2eef1222aa..44b91b3d18 100644 --- a/src/game_map.cpp +++ b/src/game_map.cpp @@ -299,6 +299,14 @@ std::unique_ptr Game_Map::loadMapFile(int map_id) { } void Game_Map::SetupCommon() { + if (!Tr::CurrTranslationId().empty()) { + // Build our map translation id. + std::stringstream ss; + ss << "map" << std::setfill('0') << std::setw(4) << GetMapId() << ".po"; + + // Translate all messages for this map + Player::translation.RewriteMapMessages(ss.str(), *map); + } SetNeedRefresh(true); int current_index = GetMapIndex(GetMapId()); diff --git a/src/meta.cpp b/src/meta.cpp index 45a3684647..a55bd34a73 100644 --- a/src/meta.cpp +++ b/src/meta.cpp @@ -47,6 +47,8 @@ #define MTINI_EXVOCAB_IMPORT_SAVE_HELP_VALUE "Import Existing Save (Multi-Games Only)" #define MTINI_EXVOCAB_IMPORT_SAVE_TITLE_KEY "Vocab_ImportSave" #define MTINI_EXVOCAB_IMPORT_SAVE_TITLE_VALUE "Import Save" +#define MTINI_EXVOCAB_TRANSLATE_TITLE_KEY "Vocab_Translate" +#define MTINI_EXVOCAB_TRANSLATE_TITLE_VALUE "Translation" // Helper: Get the CRC32 of a given file as a hex string @@ -218,6 +220,10 @@ std::string Meta::GetExVocabImportSaveTitleText() const { return GetExVocab(MTINI_EXVOCAB_IMPORT_SAVE_TITLE_KEY, MTINI_EXVOCAB_IMPORT_SAVE_TITLE_VALUE); } +std::string Meta::GetExVocabTranslateTitleText() const { + return GetExVocab(MTINI_EXVOCAB_TRANSLATE_TITLE_KEY, MTINI_EXVOCAB_TRANSLATE_TITLE_VALUE); +} + std::string Meta::GetExVocab(const std::string& term, const std::string& def_value) const { if (!Empty()) { return ini->GetString(canon_ini_lookup, term, def_value); diff --git a/src/meta.h b/src/meta.h index c8c7968b6b..6bd91336d8 100644 --- a/src/meta.h +++ b/src/meta.h @@ -123,6 +123,12 @@ class Meta { * @return the INI-defined value, or the defualt value for this vocabulary term */ std::string GetExVocabImportSaveTitleText() const; + + /** + * Retrieve the menu item text for Scen_Title translations (languages) + * @return the INI-defined value, or the default value for this vocabulary term + */ + std::string GetExVocabTranslateTitleText() const; private: diff --git a/src/player.cpp b/src/player.cpp index 4d514dcb59..7e05d9ba30 100644 --- a/src/player.cpp +++ b/src/player.cpp @@ -115,6 +115,7 @@ namespace Player { int patch; std::shared_ptr meta; FileExtGuesser::RPG2KFileExtRemap fileext_map; + Translation translation; int frames; std::string replay_input_path; std::string record_input_path; @@ -676,6 +677,9 @@ void Player::CreateGameObjects() { } escape_char = Utils::DecodeUTF32(Player::escape_symbol).front(); + // Check for translation-related directories and load language names. + translation.InitTranslations(); + std::string game_path = Main_Data::GetProjectPath(); std::string save_path = Main_Data::GetSavePath(); if (game_path == save_path) { diff --git a/src/player.h b/src/player.h index b0e720acd8..b081cb3fd4 100644 --- a/src/player.h +++ b/src/player.h @@ -21,6 +21,7 @@ // Headers #include "fileext_guesser.h" #include "meta.h" +#include "translation.h" #include "game_clock.h" #include "game_config.h" #include @@ -348,6 +349,9 @@ namespace Player { /** File extension rewriter, for non-standard extensions. */ extern FileExtGuesser::RPG2KFileExtRemap fileext_map; + /** Translation manager, including list of languages and current translation. */ + extern Translation translation; + /** * The default speed modifier applied when the speed up button is pressed * Only used for configuring the speedup, don't read this var directly use diff --git a/src/scene_title.cpp b/src/scene_title.cpp index fa02913e5b..2b90a8576e 100644 --- a/src/scene_title.cpp +++ b/src/scene_title.cpp @@ -32,6 +32,7 @@ #include "meta.h" #include "output.h" #include "player.h" +#include "translation.h" #include "scene_battle.h" #include "scene_import.h" #include "scene_load.h" @@ -53,6 +54,14 @@ void Scene_Title::Start() { } CreateCommandWindow(); + CreateTranslationWindow(); + CreateHelpWindow(); +} + +void Scene_Title::CreateHelpWindow() { + help_window.reset(new Window_Help(0, 0, SCREEN_TARGET_WIDTH, 32)); + help_window->SetVisible(false); + translate_window->SetHelpWindow(help_window.get()); } @@ -109,18 +118,35 @@ void Scene_Title::Update() { return; } - command_window->Update(); + if (active_window == 0) { + command_window->Update(); + } else { + translate_window->Update(); + } if (Input::IsTriggered(Input::DECISION)) { - int index = command_window->GetIndex(); - if (index == new_game_index) { // New Game - CommandNewGame(); - } else if (index == continue_index) { // Load Game - CommandContinue(); - } else if (index == import_index) { // Import (multi-part games) - CommandImport(); - } else if (index == exit_index) { // Exit Game - CommandShutdown(); + if (active_window == 0) { + int index = command_window->GetIndex(); + if (index == indices.new_game) { // New Game + CommandNewGame(); + } else if (index == indices.continue_game) { // Load Game + CommandContinue(); + } else if (index == indices.import) { // Import (multi-part games) + CommandImport(); + } else if (index == indices.translate) { // Choose a Translation (Language) + CommandTranslation(); + } else if (index == indices.exit) { // Exit Game + CommandShutdown(); + } + } else if (active_window == 1) { + int index = translate_window->GetIndex(); + ChangeLanguage(lang_dirs.at(index)); + } + } else if (Input::IsTriggered(Input::CANCEL)) { + if (active_window == 1) { + // Switch back + Game_System::SePlay(Game_System::GetSystemSE(Game_System::SFX_Cancel)); + HideTranslationWindow(); } } } @@ -139,29 +165,44 @@ void Scene_Title::CreateTitleGraphic() { } } +void Scene_Title::RepositionWindow(Window_Command& window, bool centerVertical) { + if (!centerVertical) { + window.SetX(SCREEN_TARGET_WIDTH / 2 - window.GetWidth() / 2); + window.SetY(SCREEN_TARGET_HEIGHT * 53 / 60 - window.GetHeight()); + } else { + window.SetX(SCREEN_TARGET_WIDTH / 2 - window.GetWidth() / 2); + window.SetY(SCREEN_TARGET_HEIGHT / 2 - window.GetHeight() / 2); + } +} + void Scene_Title::CreateCommandWindow() { // Create Options Window std::vector options; options.push_back(ToString(lcf::Data::terms.new_game)); options.push_back(ToString(lcf::Data::terms.load_game)); + // Reset index to fix issues on reuse. + indices = CommandIndices(); + // Set "Import" based on metadata if (Player::meta->IsImportEnabled()) { options.push_back(Player::meta->GetExVocabImportSaveTitleText()); - import_index = 2; - exit_index = 3; + indices.import = indices.exit; + indices.exit++; + } + + // Set "Translate" based on metadata + if (Player::translation.HasTranslations()) { + options.push_back(Player::meta->GetExVocabTranslateTitleText()); + indices.translate = indices.exit; + indices.exit++; } options.push_back(ToString(lcf::Data::terms.exit_game)); command_window.reset(new Window_Command(options)); - if (!Player::hide_title_flag) { - command_window->SetX(SCREEN_TARGET_WIDTH / 2 - command_window->GetWidth() / 2); - command_window->SetY(SCREEN_TARGET_HEIGHT * 53 / 60 - command_window->GetHeight()); - } else { - command_window->SetX(SCREEN_TARGET_WIDTH / 2 - command_window->GetWidth() / 2); - command_window->SetY(SCREEN_TARGET_HEIGHT / 2 - command_window->GetHeight() / 2); - } + RepositionWindow(*command_window, Player::hide_title_flag); + // Enable load game if available continue_enabled = FileFinder::HasSavegame(); if (continue_enabled) { @@ -182,6 +223,37 @@ void Scene_Title::CreateCommandWindow() { command_window->SetVisible(true); } +void Scene_Title::CreateTranslationWindow() { + // Build a list of 'Default' and all known languages. + std::vector lang_names; + lang_names.push_back("Default Language"); + lang_dirs.push_back(""); + lang_helps.push_back("Play the game in its original language."); + + // Push menu entries with the display name, but also save the directory location and help text. + for (const Language& lg : Player::translation.GetLanguages()) { + lang_names.push_back(lg.langName); + lang_dirs.push_back(lg.langDir); + lang_helps.push_back(lg.langDesc); + } + + translate_window.reset(new Window_Command(lang_names)); + translate_window->UpdateHelpFn = [this](Window_Help& win, int index) { + if (index>=0 && indexSetBackOpacity(128); + } + + translate_window->SetVisible(false); +} + void Scene_Title::PlayTitleMusic() { // Workaround Android problem: BGM doesn't start when game is started again Main_Data::game_system->BgmStop(); @@ -227,6 +299,39 @@ void Scene_Title::CommandImport() { Scene::Push(std::make_shared()); } +void Scene_Title::CommandTranslation() { + Game_System::SePlay(Game_System::GetSystemSE(Game_System::SFX_Decision)); + + // Switch windows + active_window = 1; + command_window->SetVisible(false); + translate_window->SetVisible(true); + help_window->SetVisible(true); +} + +void Scene_Title::ChangeLanguage(const std::string& trstr) { + Game_System::SePlay(Game_System::GetSystemSE(Game_System::SFX_Decision)); + + // No-op? + if (trstr == Player::translation.GetCurrLanguageId()) { + HideTranslationWindow(); + return; + } + + // First change the language + Player::translation.SelectLanguage(trstr); + + // Now reset the scene (force asset reload) + Scene::Push(std::make_shared(), true); +} + +void Scene_Title::HideTranslationWindow() { + active_window = 0; + command_window->SetVisible(true); + translate_window->SetVisible(false); + help_window->SetVisible(false); +} + void Scene_Title::CommandShutdown() { Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); Transition::instance().InitErase(Transition::TransitionFadeOut, this); diff --git a/src/scene_title.h b/src/scene_title.h index 3d05ce90d9..4bf9894170 100644 --- a/src/scene_title.h +++ b/src/scene_title.h @@ -51,6 +51,16 @@ class Scene_Title : public Scene { */ void CreateCommandWindow(); + /** + * Creates the Window displaying available translations. + */ + void CreateTranslationWindow(); + + /** + * Creates the Help window and hides it + */ + void CreateHelpWindow(); + /** * Plays the title music. */ @@ -89,6 +99,12 @@ class Scene_Title : public Scene { */ void CommandImport(); + /** + * Option Translation. + * Shows the Translation menu, for picking between multiple languages or localizations + */ + void CommandTranslation(); + /** * Option Shutdown. * Does a player shutdown. @@ -103,17 +119,59 @@ class Scene_Title : public Scene { private: void OnTitleSpriteReady(FileRequestResult* result); + /** + * Moves a window (typically the New/Continue/Quit menu) to the middle or bottom-center of the screen. + * @param centerVertical If true, the menu will be centered vertically. Otherwise, it will be at the bottom of the screen. + */ + void RepositionWindow(Window_Command& window, bool centerVertical); + + /** + * Picks a new language based and switches to it. + * @param langStr If the empty string, switches the game to 'No Translation'. Otherwise, switch to that translation by name. + */ + void ChangeLanguage(const std::string& langStr); + + void HideTranslationWindow(); + /** Displays the options of the title scene. */ std::unique_ptr command_window; + /** Displays all available translations (languages). */ + std::unique_ptr translate_window; + + /** Displays help text for a given language **/ + std::unique_ptr help_window; + + /** Contains directory names for each language; entry 0 is resverd for the default (no) translation */ + std::vector lang_dirs; + + /** Contains help strings for each language; entry 0 is resverd for the default (no) translation */ + std::vector lang_helps; + /** Background graphic. */ std::unique_ptr title; - /** Offsets for each selection, in case "Import" is enabled. */ - int new_game_index = 0; - int continue_index = 1; - int exit_index = 2; - int import_index = -1; + /** + * Current active window + * 0 = command + * 1 = translate + */ + int active_window = 0; + + /** + * Offsets for each selection, in case "Import" or "Translate" is enabled. + * Listed in the order they may appear; exit_index will always be last, + * and import appears before translate, if it exists. + * Stored in a struct for easy resetting, as Scene_Title can be reused. + */ + struct CommandIndices { + int new_game = 0; + int continue_game = 1; + int import = -1; + int translate = -1; + int exit = 2; + }; + CommandIndices indices; /** Contains the state of continue button. */ bool continue_enabled = false; diff --git a/src/translation.cpp b/src/translation.cpp new file mode 100644 index 0000000000..bfb07ecfef --- /dev/null +++ b/src/translation.cpp @@ -0,0 +1,784 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#include "translation.h" + +// Headers +#include +#include +#include +#include +#include +#include "lcf/rpg/mapinfo.h" + +#include "cache.h" +#include "main_data.h" +#include "game_actors.h" +#include "game_map.h" +#include "player.h" +#include "output.h" +#include "utils.h" + + +// Name of the translate directory +#define TRDIR_NAME "languages" + +// Name of expected files +#define TRFILE_RPG_RT_LDB "rpg_rt.ldb.po" +#define TRFILE_RPG_RT_COMMON "rpg_rt.ldb.common.po" +#define TRFILE_RPG_RT_BATTLE "rpg_rt.ldb.battle.po" +#define TRFILE_RPG_RT_LMT "rpg_rt.lmt.po" +#define TRFILE_META_INI "META.INI" + +// Message box commands to remove a message box or add one in place. +#define TRCUST_REMOVEMSG "__EASY_RPG_CMD:REMOVE_MSGBOX__" +#define TRCUST_ADDMSG "__EASY_RPG_CMD:ADD_MSGBOX__" + + +std::string Tr::TranslationDir() { + return Player::translation.RootDir(); +} + +std::string Tr::CurrTranslationId() { + return Player::translation.GetCurrLanguageId(); +} + +void Translation::Reset() +{ + ClearTranslationLookups(); + + languages.clear(); + currLanguage = ""; + translationRootDir = ""; +} + +void Translation::InitTranslations() +{ + // Reset + Reset(); + + // Determine if the "languages" directory exists, and convert its case. + auto tree = FileFinder::GetDirectoryTree(); + auto langIt = tree->directories.find(TRDIR_NAME); + if (langIt != tree->directories.end()) { + // Save the root directory for later. + translationRootDir = langIt->second; + + // Now list all directories within the translate dir + auto translation_path = FileFinder::MakePath(FileFinder::GetDirectoryTree()->directory_path, translationRootDir); + auto translation_tree = FileFinder::CreateDirectoryTree(translation_path, FileFinder::RECURSIVE); + if (translation_tree == nullptr) { return; } + + // Now iterate over every subdirectory. + for (const auto& trName : translation_tree->directories) { + Language item; + item.langDir = trName.second; + item.langName = trName.second; + + // If there's a manifest file, read the language name and help text from that. + std::string metaName = FileFinder::FindDefault(*translation_tree, trName.second, TRFILE_META_INI); + if (!metaName.empty()) { + lcf::INIReader ini(metaName); + item.langName = ini.GetString("Language", "Name", item.langName); + item.langDesc = ini.GetString("Language", "Description", ""); + } + + languages.push_back(item); + } + } +} + +std::string Translation::GetCurrLanguageId() const +{ + return currLanguage; +} + +std::string Translation::RootDir() const +{ + return translationRootDir; +} + +bool Translation::HasTranslations() const +{ + return !languages.empty(); +} + +const std::vector& Translation::GetLanguages() const +{ + return languages; +} + + +void Translation::SelectLanguage(const std::string& langId) +{ + // Try to read in our language files. + Output::Debug("Changing language to: '{}'", (!langId.empty() ? langId : "")); + if (!ParseLanguageFiles(langId)) { + return; + } + currLanguage = langId; + + // We reload the entire database as a precaution. + Player::LoadDatabase(); + + // Rewrite our database+messages (unless we are on the Default language). + // Note that map Message boxes are changed on map load, to avoid slowdown here. + if (!currLanguage.empty()) { + RewriteDatabase(); + RewriteTreemapNames(); + RewriteBattleEventMessages(); + RewriteCommonEventMessages(); + } + + // Reset the cache, so that all images load fresh. + Cache::Clear(); +} + + +bool Translation::ParseLanguageFiles(const std::string& langId) +{ + // Create the directory tree (for lookups). + std::shared_ptr translation_tree; + if (langId != "") { + auto translation_path = FileFinder::MakePath(FileFinder::MakePath(FileFinder::GetDirectoryTree()->directory_path, RootDir()), langId); + translation_tree = FileFinder::CreateDirectoryTree(translation_path, FileFinder::FILES); + if (translation_tree == nullptr) { + Output::Warning("Translation for '{}' does not appear to exist", langId); + return false; + } + } + + // Clear the old translation + ClearTranslationLookups(); + + // For default, this is all we need. + if (translation_tree == nullptr) { + return true; + } + + // Scan for files in the directory and parse them. + for (const auto& trName : translation_tree->files) { + std::string fileName = FileFinder::FindDefault(*translation_tree, trName.first); + + if (trName.first == TRFILE_RPG_RT_LDB) { + sys.reset(new Dictionary()); + ParsePoFile(fileName, *sys); + } else if (trName.first == TRFILE_RPG_RT_BATTLE) { + battle.reset(new Dictionary()); + ParsePoFile(fileName, *battle); + } else if (trName.first == TRFILE_RPG_RT_COMMON) { + common.reset(new Dictionary()); + ParsePoFile(fileName, *common); + } else if (trName.first == TRFILE_RPG_RT_LMT) { + mapnames.reset(new Dictionary()); + ParsePoFile(fileName, *mapnames); + } else { + std::unique_ptr dict; + dict.reset(new Dictionary()); + ParsePoFile(fileName, *dict); + maps[trName.first] = std::move(dict); + } + } + + // Log + Output::Debug("Translation loaded {} sys, {} common, {} battle, and {} map .po files", (sys==nullptr?0:1), (battle==nullptr?0:1), (common==nullptr?0:1), maps.size()); + return true; +} + + +void Translation::RewriteDatabase() +{ + lcf::rpg::ForEachString(lcf::Data::data, [this](lcf::DBString& value, auto& ctxt) { + // When we re-write the database, we only care about translations that are exactly one level deep. + if (ctxt.parent==nullptr || ctxt.parent->parent!=nullptr) { + return; + } + + // Look up the indexed form first; e.g., "actors.1.name", starting from 1 instead of 0 + if (ctxt.index >= 0) { + if (sys->TranslateString(fmt::format("{}.{}.{}", ctxt.parent->name, ctxt.parent->index+1, ctxt.name), value)) { + return; + } + } + + // Look up the non-indexed form second; e.g., "actors.name" + if (sys->TranslateString(fmt::format("{}.{}", ctxt.parent->name, ctxt.name), value)) { + return; + } + + // Finally, look up a context-free form (just by value). + if (sys->TranslateString("", value)) { + return; + } + }); + + // Game_Actors caches some values; we need to force re-write them here. + // As far as I can tell, this is the best way to accomplish this. + Main_Data::game_actors = std::make_unique(); +} + +void Translation::RewriteTreemapNames() +{ + if (mapnames) { + for (lcf::rpg::MapInfo& map : lcf::Data::treemap.maps) { + mapnames->TranslateString("", map.name); + } + } +} + +void Translation::RewriteBattleEventMessages() +{ + // Rewrite all event commands on all pages. + if (battle) { + for (lcf::rpg::Troop& troop : lcf::Data::troops) { + for (lcf::rpg::TroopPage& page : troop.pages) { + RewriteEventCommandMessage(*battle, page.event_commands); + } + } + } +} + + +void Translation::RewriteCommonEventMessages() +{ + // Rewrite all event commands on all pages. + if (common) { + for (lcf::rpg::CommonEvent& ev : lcf::Data::commonevents) { + RewriteEventCommandMessage(*common, ev.event_commands); + } + } +} + + +namespace { + /** + * Helper class for iterating over and rewriting event commands. + * Starts at index 0. + */ + class CommandIterator { + public: + CommandIterator(std::vector& commands) : commands(commands) {} + + /// Returns true if the index is past the end of the command list + bool Done() const { + return index >= commands.size(); + } + + /// Advance the index through the command list by 1 + void Advance() { + index += 1; + } + + /// Retrieve the code of the EventCommand at the index + lcf::rpg::EventCommand::Code CurrCmdCode() const { + return static_cast(commands[index].code); + } + + /// Retrieve the string of the EventCommand at the index + std::string CurrCmdString() const { + return ToString(commands[index].string); + } + + /// Retrieve the indent level of the EventCommand at the index + int CurrCmdIndent() const { + return commands[index].indent; + } + + /// Retrieve parameter at position 'pos' of the EventCommand at the current index, or the devValue if no such parameter exists. + int CurrCmdParam(size_t pos, int defVal) const { + if (pos < commands[index].parameters.size()) { + return commands[index].parameters[pos]; + } + return defVal; + } + + /// Returns true if the current Event Command is ShowMessage + bool CurrIsShowMessage() const { + return CurrCmdCode() == lcf::rpg::EventCommand::Code::ShowMessage; + } + + /// Returns true if the current Event Command is ShowMessage_2 + bool CurrIsShowMessage2() const { + return CurrCmdCode() == lcf::rpg::EventCommand::Code::ShowMessage_2; + } + + /// Returns true if the current Event Command is ShowChoice + bool CurrIsShowChoice() const { + return CurrCmdCode() == lcf::rpg::EventCommand::Code::ShowChoice; + } + + /// Returns true if the current Event Command is ShowChoiceOption + bool CurrIsShowChoiceOption() const { + return CurrCmdCode() == lcf::rpg::EventCommand::Code::ShowChoiceOption; + } + + /// Returns true if the current Event Command is ShowChoiceEnd + bool CurrIsShowChoiceEnd() const { + return CurrCmdCode() == lcf::rpg::EventCommand::Code::ShowChoiceEnd; + } + + /// Add each line of a [ShowMessage,ShowMessage_2,...] chain to "msg_str" (followed by a newline) + /// and save to "indexes" the index of each ShowMessage(2) command that was used to populate this + /// (for rewriting later). + /// Advances the index until after the last ShowMessage(2) command + void BuildMessageString(std::stringstream& msg_str, std::vector& indexes) { + // No change if we're not on the right command. + if (Done() || !CurrIsShowMessage()) { + return; + } + + // Add the first line + msg_str << CurrCmdString() <<"\n"; + indexes.push_back(index); + Advance(); + + // Build lines 2 through 4 + while (!Done() && CurrIsShowMessage2()) { + msg_str << CurrCmdString() <<"\n"; + indexes.push_back(index); + Advance(); + } + } + + /// Add each line of a [ShowChoice,ShowChoiceOption,...,ShowChoiceEnd] chain to "msg_str" (followed by a newline) + /// and save to "indexes" the index of each ShowChoiceOption command that was used to populate this + /// (for rewriting later). + /// Advances the index until after the (first) ShowChoice command (but it will likely still be on a ShowChoiceOption/End) + void BuildChoiceString(std::stringstream& msg_str, std::vector& indexes) { + // No change if we're not on the right command. + if (Done() || !CurrIsShowChoice()) { + return; + } + + // Advance to ChoiceOptions + Advance(); + if(Done()) { + return; + } + + // Choices must be on the same indent level. + // We have to save/restore the index, though, in the rare case that we skip something that can be translated. + int indent = CurrCmdIndent(); + size_t savedIndex = index; + while (!Done()) { + if (indent == CurrCmdIndent()) { + // Handle a new index + if (CurrIsShowChoiceOption() && CurrCmdParam(0,0) < 4) { + msg_str << CurrCmdString() <<"\n"; + indexes.push_back(index); + } + + // Done? + if (CurrIsShowChoiceEnd()) { + break; + } + } + Advance(); + } + index = savedIndex; + } + + /// Change the string value of the EventCommand at position "idx" to "newStr" + void ReWriteString(size_t idx, const std::string& newStr) { + if (idx < commands.size()) { + commands[idx].string = lcf::DBString(newStr); + } + } + + /// Puts a "ShowMessage" or "ShowMessage_2" command into the command stack before position "idx". + /// Sets the string value to "line". Note that ShowMessage_2 is chosen if baseMsgBox is false. + /// This also updates the index if relevant, but it does not update external index caches. + void PutShowMessageBeforeIndex(const std::string& line, size_t idx, bool baseMsgBox) { + // We need a reference index for the indent. + size_t refIndent = 0; + if (idx < commands.size()) { + refIndent = commands[idx].indent; + } else if (idx == commands.size() && commands.size()>0) { + refIndent = commands[idx-1].indent; + } + + if (idx <= commands.size()) { + lcf::rpg::EventCommand newCmd; + newCmd.code = static_cast(baseMsgBox ? lcf::rpg::EventCommand::Code::ShowMessage : lcf::rpg::EventCommand::Code::ShowMessage_2); + newCmd.indent = refIndent; + newCmd.string = lcf::DBString(line); + commands.insert(commands.begin()+idx, newCmd); + + // Update our index + if (index >= idx) { + index += 1; + } + } + } + + /// Remove the EventCommand at position "idx" from the command stack. + /// Also updates our index, if relevant. + void RemoveByIndex(size_t idx) { + if (idx < commands.size()) { + commands.erase(commands.begin() + idx); + + // Update our index + if (index > idx) { + index -= 1; + } + } + } + + /// Add multiple message boxes to the command stack before "idx". + /// The "msgs" each represent lines in new, independent message boxes (so they will have both ShowMessage and ShowMessage_2) + /// Also updates our index, if relevant. + void InsertMultiMessageBefore(std::vector>& msgs, size_t idx) { + for (std::vector& lines : msgs) { + while (!lines.empty()) { + PutShowMessageBeforeIndex(lines.back(), idx, lines.size()==1); + lines.pop_back(); + } + } + } + + private: + std::vector& commands; + size_t index = 0; + }; +} + + + +std::vector> Translation::TranslateMessageStream(const Dictionary& dict, const std::stringstream& msg1, const std::stringstream* msg2, char trimChar) { + // Prepare source string + std::string msgStr = msg1.str(); + if (msg2!=nullptr) { + msgStr += msg2->str(); + } + if (msgStr.size()>0 && msgStr.back() == trimChar) { + msgStr.pop_back(); + } + + // Translation exists? + std::vector> res; + if (dict.TranslateString("", msgStr)) { + // First, get all lines. + std::vector lines = Utils::Tokenize(msgStr, [](char32_t c) { return c=='\n'; }); + + // Now, break into message boxes based on the ADDMSG string + res.push_back(std::vector()); + for (const std::string& line : lines) { + if (line == TRCUST_ADDMSG) { + res.push_back(std::vector()); + } else { + res.back().push_back(line); + } + + // Special case: stop once you've found a REMMSG (to avoid the case where both add/rem are present) + if (line == TRCUST_REMOVEMSG) { + break; + } + } + + // Ensure we never get an empty vector (force using the REMMSG command) + for (std::vector& msgbox : res) { + if (msgbox.empty()) { + msgbox.push_back(""); + } + } + } + + return res; +} + + +void Translation::RewriteEventCommandMessage(const Dictionary& dict, std::vector& commandsOrig) { + // A note on this function: it is (I feel) necessarily complicated, given what it's actually doing. + // I've tried to abstract most of this complexity away behind an "iterator" interface, so that we do not + // have to track the current index directly in this function. + CommandIterator commands(commandsOrig); + while (!commands.Done()) { + // Logic: build up both the Message stream and the Choice stream, since we'll need to + // deal with both eventually. Then, if they can be merged do that. + if (commands.CurrIsShowMessage() || commands.CurrIsShowChoice()) { + // First build up the lines of Message texts + std::stringstream msg_str; + std::vector msg_indexes; + commands.BuildMessageString(msg_str, msg_indexes); + + // Next, build up the lines of Choice elements + std::stringstream choice_str; + std::vector choice_indexes; // Number of entries == number of choices + commands.BuildChoiceString(choice_str, choice_indexes); + + // Will they fit on screen if we combine them? + bool combine = false; //msg_indexes.size()>0 && msg_indexes.size()+choice_indexes.size() <= 4; + + // Go through messages first, including possible choices + if (msg_indexes.size()>0) { + // Get our lines, possibly including "combined" + std::vector> msgs = TranslateMessageStream(dict, msg_str, (combine?&choice_str:nullptr), '\n'); + if (msgs.size()>0) { + // The complex replacement logic is based on the last message box, then all remaining things are simply left back in. + std::vector& lines = msgs.back(); + + // There is a special case here: if we are asked to remove a message box, we should cancel the "combine" action and do nothing further + // This command is *only* respected as the first line of a message box. + if (lines[0]==TRCUST_REMOVEMSG) { + // Update the index of all choice items first + for (size_t& index : choice_indexes) { + index -= msg_indexes.size(); + } + + // Now clear all message boxes in reverse order. + while (!msg_indexes.empty()) { + commands.RemoveByIndex(msg_indexes.back()); + msg_indexes.pop_back(); + } + + // Finally, reset the "combine" flag so that we process the message box command correctly. + combine = false; + } else { + // We only need the last X Choices from the translation, since we can't change the Choice count. + size_t maxLines = 4; + if (combine) { + // Go backwards through our lines/choices + while (!choice_indexes.empty() && !lines.empty()) { + commands.ReWriteString(choice_indexes.back(), lines.back()); + choice_indexes.pop_back(); + lines.pop_back(); + maxLines -= 1; + } + } + + // Trim lines down to allowed remaining (with choices). + while (lines.size() > maxLines) { + lines.pop_back(); + } + + // First, pop extra entries from the back of msg_cmd; this preserves the remaining entries' indexes + while (msg_indexes.size() > lines.size()) { + commands.RemoveByIndex(msg_indexes.back()); + msg_indexes.pop_back(); + } + + // Next, push extra entries from the back of lines to msg_cmd + while (lines.size() > msg_indexes.size()) { + commands.PutShowMessageBeforeIndex("", msg_indexes.back()+1, false); + msg_indexes.push_back(msg_indexes.back()+1); + } + + // Now simply go through each entry and update it. + for (size_t num=0; num0) { + // Translate, break back into lines. + std::vector> msgs = TranslateMessageStream(dict, choice_str, nullptr, '\n'); + if (msgs.size()>0) { + // Logic here is also based on the last message box. + std::vector& lines = msgs.back(); + + // We only pick the first X entries from the translation, since we can't change the Choice count. + for (size_t num=0; numsecond, pg.event_commands); + } + } +} + + +void Translation::ParsePoFile(const std::string& path, Dictionary& out) +{ + std::ifstream in(path.c_str()); + if (in.good()) { + if (!Dictionary::FromPo(out, in)) { + Output::Warning("Failure parsing PO file, resetting: '{}'", path); + out = Dictionary(); + } + } +} + + +void Translation::ClearTranslationLookups() +{ + sys.reset(); + common.reset(); + battle.reset(); + mapnames.reset(); + maps.clear(); +} + + +////////////////////////////////////////////////////////// +// NOTE: The code from here on out is duplicated in LcfTrans. +// At some point it should be merged to a common location. +////////////////////////////////////////////////////////// + + +void Dictionary::addEntry(const Entry& entry) +{ + // Space-saving measure: If the translation string is empty, there's no need to save it (since we will just show the original). + if (!entry.translation.empty()) { + entries[entry.context][entry.original] = entry.translation; + } +} + +// Returns success +bool Dictionary::FromPo(Dictionary& res, std::istream& in) +{ + std::string line; + bool found_header = false; + bool parse_item = false; + + Entry e; + + auto extract_string = [&line](int offset, bool& error) { + std::stringstream out; + bool slash = false; + bool first_quote = false; + + for (char c : line.substr(offset)) { + if (c == ' ' && !first_quote) { + continue; + } else if (c == '"' && !first_quote) { + first_quote = true; + continue; + } + + if (!slash && c == '\\') { + slash = true; + } else if (slash) { + slash = false; + switch (c) { + case '\\': + out << c; + break; + case 'n': + out << '\n'; + break; + case '"': + out << '"'; + break; + default: + Output::Error("Parse error {} ({})", line, c); + error = true; + break; + } + } else { + // no-slash + if (c == '"') { + // done + return out.str(); + } + out << c; + } + } + + Output::Error("Parse error: Unterminated line: {}", line); + error = true; + return out.str(); + }; + + auto read_msgstr = [&](bool& error) { + // Parse multiply lines until empty line or comment + e.translation = extract_string(6, error); + + while (std::getline(in, line, '\n')) { + if (line.empty() || ToStringView(line).starts_with("#")) { + break; + } + e.translation += extract_string(0, error); + } + + parse_item = false; + res.addEntry(e); + }; + + auto read_msgid = [&](bool& error) { + // Parse multiply lines until empty line or msgstr is encountered + e.original = extract_string(5, error); + + while (std::getline(in, line, '\n')) { + if (line.empty() || ToStringView(line).starts_with("msgstr")) { + read_msgstr(error); + return; + } + e.original += extract_string(0, error); + } + }; + + bool error = false; + while (std::getline(in, line, '\n')) { + auto lineSV = ToStringView(line); + if (!found_header) { + if (lineSV.starts_with("msgstr")) { + found_header = true; + } + continue; + } + + if (!parse_item) { + if (lineSV.starts_with("msgctxt")) { + e.context = extract_string(7, error); + parse_item = true; + } else if (lineSV.starts_with("msgid")) { + parse_item = true; + read_msgid(error); + } + } else { + if (lineSV.starts_with("msgid")) { + read_msgid(error); + } else if (lineSV.starts_with("msgstr")) { + read_msgstr(error); + } + } + } + return !error; +} + + + + diff --git a/src/translation.h b/src/translation.h new file mode 100644 index 0000000000..2fef94a3f5 --- /dev/null +++ b/src/translation.h @@ -0,0 +1,284 @@ +/* + * This file is part of EasyRPG Player. + * + * EasyRPG Player is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * EasyRPG Player is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with EasyRPG Player. If not, see . + */ + +#ifndef EP_TRANSLATION_H +#define EP_TRANSLATION_H + +// Headers +#include +#include +#include +#include + +#include "filefinder.h" + +namespace lcf { + namespace rpg { + class Map; + class EventCommand; + } + class DBString; +} + + +/** + * Namespace used to retrieve translations of Terms, BattlEvents, etc. + */ +namespace Tr { + /** + * The name of the translation directory. + * @return The translation directory. + */ + std::string TranslationDir(); + + /** + * The id of the current translation (e.g., "Spanish"). If empty, there is no active translation. + * @return The translation ID + */ + std::string CurrTranslationId(); + +} // End namespace Tr + + +////////////////////////////////////////////////////////// +// NOTE: The code for Entry and Dictionary is duplicated in LcfTrans. +// At some point it should be merged to a common location. +////////////////////////////////////////////////////////// + +/** + * An entry in the dictionary + */ +class Entry { +public: + std::string original; // msgid + std::string translation; // msgstr + std::string context; // msgctxt +}; + +/** + * A .po file loaded into memory. Contains a dictionary of entries. + */ +class Dictionary { +public: + /** + * Super simple parser. + * Only parses msgstr, msgid and msgctx + * Returns false on failure. + * + * @param res The dictionary to store the translated entries in. + * @param in The stream to load the translated entries from. + * @param return True if the file was loaded without error; false otherwise. + */ + static bool FromPo(Dictionary& res, std::istream& in); + + /** + * Replace an original string with the translated string. + * Template can be "std::string" or "lcf::DBString" + * + * @param context The 'context' of this string; used to differentiate strings that have the same + * original language value but different target language values. + * @param original The string to lookup. Will be replaced if a lookup is found. + * @return True if the original string was replaced; false otherwise. + */ + template + bool TranslateString(const std::string& context, StringType& original) const; + +private: + /** + * Add an entry to the dictionary. + * + * @param entry The entry to add. + */ + void addEntry(const Entry& entry); + + // Lookup by context, where context can be empty ("") for no context. + std::unordered_map> entries; +}; + + +// Template implementation +template +bool Dictionary::TranslateString(const std::string& context, StringType& original) const +{ + std::stringstream key; + key <second.find(key.str()); + if (it2 != it->second.end()) { + original = StringType(it2->second); + return true; + } + } + return false; +} + + +/** + * Properties of a language + */ +struct Language { + std::string langDir; // Language directory (e.g., "en", "English_Localization") + std::string langName; // Display name for this language (e.g., "English") + std::string langDesc; // Helper text to show when the menu is highlighted +}; + + +/** + * Class that holds a list of all available translations and tracks the current one. + * Allows changing the language and rewriting the database. + */ +class Translation { +public: + /** + * Scan the Languages directory and build up a list of known languages. + */ + void InitTranslations(); + + /** + * Do any translations besides the default exist? + * + * @return True if there is at least one translation (language); false otherwise + */ + bool HasTranslations() const; + + /** + * Retrieve the root directory for all Languages + * + * @return the Languages directory. + */ + std::string RootDir() const; + + /** + * Retrieves a vector of all known languages. + * + * @return the Languages vector + */ + const std::vector& GetLanguages() const; + + /** + * Switches to a given language. Resets the database and the Image Cache. + * + * @param langId The language ID (or "" for "Default") + */ + void SelectLanguage(const std::string& langId); + + /** + * Rewrite all Messages and Choices in this Map + * + * @param mapName The name of the map with formatting similar to the .po file; e.g., "map0104.po" + * @param map The map object itself (for modifying). + */ + void RewriteMapMessages(const std::string& mapName, lcf::rpg::Map& map); + + /** + * Retrieve the ID of the current (active) language. + * + * @return the current language ID, or "" for the Default language + */ + std::string GetCurrLanguageId() const; + + +private: + /** + * Reset all saved language data and revert to "no translation". + */ + void Reset(); + + /** + * Reset all lookups loaded from .po files for the active language. + */ + void ClearTranslationLookups(); + + /** + * Parse a .po file and save its language-related strings. + * + * @param path The path to the .po file used as input. + * @param out The Dictionary to save these entries in (output). + */ + void ParsePoFile(const std::string& path, Dictionary& out); + + /** + * Parse all .po files for the given language. + * + * @param langId The ID of the language to parse, or "" for Default (no parsing is done) + * @return True if the language directory was found; false otherwise + */ + bool ParseLanguageFiles(const std::string& langId); + + /** + * Rewrite RPG_RT.ldb with the current translation entries + */ + void RewriteDatabase(); + + /** + * Rewrite RPG_RT.lmt with the current translation entries + */ + void RewriteTreemapNames(); + + /** + * Rewrite all Battle Messages with the current translation entries + */ + void RewriteBattleEventMessages(); + + /** + * Rewrite all Common Event Messages with the current translation entries + */ + void RewriteCommonEventMessages(); + + /** + * Convert a stream of msgbox + choices to a list of output message boxes + * + * @param dict The dictionary to use for translation + * @param msg1 The first message string to use for lookup. String with newlines. + * @param msg2 The (optiona) second message string to use for lookup. String with newlines. + * @param trimChar Trim this character if the lookup string ends with this. + * @return A vector of Message Boxes, where each Message Box is represented as a vector of lines (strings), or an empty vector if there is no translation. + * It is guaranteed that each MessageBox vector will have at least one entry (containing "") if it would otherwise be empty; this can happen + * if the message box insertion commands are used. Note that the last MessageBox vector may contain translated "Choice" entries (it is based on the input). + */ + std::vector> TranslateMessageStream(const Dictionary& dict, const std::stringstream& msg1, const std::stringstream* msg2, char trimChar); + + /** + * Rewrite a list of event commands (from any map, battle, or common event) given a dictionary. + * Takes into account deleting and adding message boxes. + * + * @param dict The dictionary to use for translation. + * @param commands The commands to search through and update. + */ + void RewriteEventCommandMessage(const Dictionary& dict, std::vector& commands); + + +private: + // Our translations are broken apart into multiple files; we store a lookup for each one. + std::unique_ptr sys; // RPG_RT.ldb.po + std::unique_ptr common; // RPG_RT.ldb.common.po + std::unique_ptr battle; // RPG_RT.ldb.battle.po + std::unique_ptr mapnames; // RPG_RT.lmt.po (map names, used only in the "Teleport" event command) + std::unordered_map> maps; // map.po, indexed by map name + + // Our list of available Languages (translations, localizations), determined by scanning the files on disk. + std::vector languages; + + // The "languages" directory, but with appropriate capitalization. + std::string translationRootDir; + + // The translation we are currently showing (e.g., "English_1") + std::string currLanguage; +}; + +#endif // EP_TRANSLATION_H diff --git a/src/window_selectable.cpp b/src/window_selectable.cpp index 51e0bfe0be..e111f52d40 100644 --- a/src/window_selectable.cpp +++ b/src/window_selectable.cpp @@ -83,6 +83,9 @@ void Window_Selectable::SetHelpWindow(Window_Help* nhelp_window) { } void Window_Selectable::UpdateHelp() { + if (UpdateHelpFn && help_window != nullptr) { + UpdateHelpFn(*help_window, index); + } } // Update Cursor Rect diff --git a/src/window_selectable.h b/src/window_selectable.h index 27f3cd8231..0d64923aea 100644 --- a/src/window_selectable.h +++ b/src/window_selectable.h @@ -19,6 +19,7 @@ #define EP_WINDOW_SELECTABLE_H // Headers +#include #include "window_base.h" #include "window_help.h" @@ -51,6 +52,13 @@ class Window_Selectable: public Window_Base { */ Rect GetItemRect(int index); + /** + * Function called by the base UpdateHelp() implementation. + * Passes in the Help Window and the current selected index + * Will not be called if the help_window is null + */ + std::function UpdateHelpFn; + Window_Help* GetHelpWindow(); /** From 0ee5db7da8657cb4b736bc2e322295e046e008da Mon Sep 17 00:00:00 2001 From: "Seth N. Hetu" Date: Fri, 16 Oct 2020 18:04:22 -0400 Subject: [PATCH 2/5] Update to new SE syntax --- src/scene_title.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/scene_title.cpp b/src/scene_title.cpp index 2b90a8576e..7f1d6b9904 100644 --- a/src/scene_title.cpp +++ b/src/scene_title.cpp @@ -145,7 +145,7 @@ void Scene_Title::Update() { } else if (Input::IsTriggered(Input::CANCEL)) { if (active_window == 1) { // Switch back - Game_System::SePlay(Game_System::GetSystemSE(Game_System::SFX_Cancel)); + Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Cancel)); HideTranslationWindow(); } } @@ -300,7 +300,7 @@ void Scene_Title::CommandImport() { } void Scene_Title::CommandTranslation() { - Game_System::SePlay(Game_System::GetSystemSE(Game_System::SFX_Decision)); + Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); // Switch windows active_window = 1; @@ -310,7 +310,7 @@ void Scene_Title::CommandTranslation() { } void Scene_Title::ChangeLanguage(const std::string& trstr) { - Game_System::SePlay(Game_System::GetSystemSE(Game_System::SFX_Decision)); + Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); // No-op? if (trstr == Player::translation.GetCurrLanguageId()) { From 210f6cbc7ef20d3e83bd958e8776a9c30c64d1df Mon Sep 17 00:00:00 2001 From: "Seth N. Hetu" Date: Fri, 16 Oct 2020 18:19:08 -0400 Subject: [PATCH 3/5] Complexity reduction now that we can treat Messges and Choices separately --- src/translation.cpp | 62 ++++++++++++++------------------------------- src/translation.h | 7 +++-- 2 files changed, 22 insertions(+), 47 deletions(-) diff --git a/src/translation.cpp b/src/translation.cpp index bfb07ecfef..6f9ebccae1 100644 --- a/src/translation.cpp +++ b/src/translation.cpp @@ -458,12 +458,9 @@ namespace { -std::vector> Translation::TranslateMessageStream(const Dictionary& dict, const std::stringstream& msg1, const std::stringstream* msg2, char trimChar) { +std::vector> Translation::TranslateMessageStream(const Dictionary& dict, const std::stringstream& msg, char trimChar) { // Prepare source string - std::string msgStr = msg1.str(); - if (msg2!=nullptr) { - msgStr += msg2->str(); - } + std::string msgStr = msg.str(); if (msgStr.size()>0 && msgStr.back() == trimChar) { msgStr.pop_back(); } @@ -507,60 +504,32 @@ void Translation::RewriteEventCommandMessage(const Dictionary& dict, std::vector // have to track the current index directly in this function. CommandIterator commands(commandsOrig); while (!commands.Done()) { - // Logic: build up both the Message stream and the Choice stream, since we'll need to - // deal with both eventually. Then, if they can be merged do that. - if (commands.CurrIsShowMessage() || commands.CurrIsShowChoice()) { - // First build up the lines of Message texts + // We only need to deal with either Message or Choice commands + if (commands.CurrIsShowMessage()) { + // Build up the lines of Message texts std::stringstream msg_str; std::vector msg_indexes; commands.BuildMessageString(msg_str, msg_indexes); - // Next, build up the lines of Choice elements - std::stringstream choice_str; - std::vector choice_indexes; // Number of entries == number of choices - commands.BuildChoiceString(choice_str, choice_indexes); - - // Will they fit on screen if we combine them? - bool combine = false; //msg_indexes.size()>0 && msg_indexes.size()+choice_indexes.size() <= 4; - // Go through messages first, including possible choices if (msg_indexes.size()>0) { // Get our lines, possibly including "combined" - std::vector> msgs = TranslateMessageStream(dict, msg_str, (combine?&choice_str:nullptr), '\n'); + std::vector> msgs = TranslateMessageStream(dict, msg_str, '\n'); if (msgs.size()>0) { // The complex replacement logic is based on the last message box, then all remaining things are simply left back in. std::vector& lines = msgs.back(); - // There is a special case here: if we are asked to remove a message box, we should cancel the "combine" action and do nothing further + // There is a special case here: if we are asked to remove a message box, we should do nothing further // This command is *only* respected as the first line of a message box. if (lines[0]==TRCUST_REMOVEMSG) { - // Update the index of all choice items first - for (size_t& index : choice_indexes) { - index -= msg_indexes.size(); - } - - // Now clear all message boxes in reverse order. + // Clear all message boxes in reverse order. while (!msg_indexes.empty()) { commands.RemoveByIndex(msg_indexes.back()); msg_indexes.pop_back(); } - - // Finally, reset the "combine" flag so that we process the message box command correctly. - combine = false; } else { - // We only need the last X Choices from the translation, since we can't change the Choice count. - size_t maxLines = 4; - if (combine) { - // Go backwards through our lines/choices - while (!choice_indexes.empty() && !lines.empty()) { - commands.ReWriteString(choice_indexes.back(), lines.back()); - choice_indexes.pop_back(); - lines.pop_back(); - maxLines -= 1; - } - } - // Trim lines down to allowed remaining (with choices). + const size_t maxLines = 4; while (lines.size() > maxLines) { lines.pop_back(); } @@ -589,10 +558,17 @@ void Translation::RewriteEventCommandMessage(const Dictionary& dict, std::vector } } - // Go through choices second. - if (!combine && choice_indexes.size()>0) { + // Note that commands.Advance() has already happened within the above code. + } else if (commands.CurrIsShowChoice()) { + // Build up the lines of Choice elements + std::stringstream choice_str; + std::vector choice_indexes; // Number of entries == number of choices + commands.BuildChoiceString(choice_str, choice_indexes); + + // Go through choices. + if (choice_indexes.size()>0) { // Translate, break back into lines. - std::vector> msgs = TranslateMessageStream(dict, choice_str, nullptr, '\n'); + std::vector> msgs = TranslateMessageStream(dict, choice_str, '\n'); if (msgs.size()>0) { // Logic here is also based on the last message box. std::vector& lines = msgs.back(); diff --git a/src/translation.h b/src/translation.h index 2fef94a3f5..97dc2c52c1 100644 --- a/src/translation.h +++ b/src/translation.h @@ -241,17 +241,16 @@ class Translation { void RewriteCommonEventMessages(); /** - * Convert a stream of msgbox + choices to a list of output message boxes + * Convert a stream of msgbox or choices to a list of output message boxes * * @param dict The dictionary to use for translation - * @param msg1 The first message string to use for lookup. String with newlines. - * @param msg2 The (optiona) second message string to use for lookup. String with newlines. + * @param msg The message string to use for lookup. String with newlines. * @param trimChar Trim this character if the lookup string ends with this. * @return A vector of Message Boxes, where each Message Box is represented as a vector of lines (strings), or an empty vector if there is no translation. * It is guaranteed that each MessageBox vector will have at least one entry (containing "") if it would otherwise be empty; this can happen * if the message box insertion commands are used. Note that the last MessageBox vector may contain translated "Choice" entries (it is based on the input). */ - std::vector> TranslateMessageStream(const Dictionary& dict, const std::stringstream& msg1, const std::stringstream* msg2, char trimChar); + std::vector> TranslateMessageStream(const Dictionary& dict, const std::stringstream& msg, char trimChar); /** * Rewrite a list of event commands (from any map, battle, or common event) given a dictionary. From 1abd74df2a3350635108f4b435406b4fd0d174cf Mon Sep 17 00:00:00 2001 From: "Seth N. Hetu" Date: Sat, 5 Dec 2020 15:24:22 -0500 Subject: [PATCH 4/5] Fixes from code reivew. --- src/filefinder.cpp | 6 +- src/game_map.cpp | 2 +- src/scene_title.cpp | 16 ++--- src/scene_title.h | 19 ++--- src/translation.cpp | 168 +++++++++++++++++++++++--------------------- src/translation.h | 28 ++++---- 6 files changed, 125 insertions(+), 114 deletions(-) diff --git a/src/filefinder.cpp b/src/filefinder.cpp index 91f400cfd1..a80391094d 100644 --- a/src/filefinder.cpp +++ b/src/filefinder.cpp @@ -93,7 +93,7 @@ namespace { using namespace FileFinder; // Avoid searching entirely if there is no active translation - if (translate && Tr::CurrTranslationId().empty()) { return ""; } + if (translate && Tr::GetCurrentTranslationId().empty()) { return ""; } #ifdef EMSCRIPTEN // The php filefinder should have given us an useable path @@ -103,9 +103,9 @@ namespace { return em_file; #endif - std::string corrected_dir = lcf::ReaderUtil::Normalize(translate?Tr::TranslationDir():dir); + std::string corrected_dir = lcf::ReaderUtil::Normalize(translate?Tr::GetTranslationDir():dir); std::string const escape_symbol = Player::escape_symbol; - std::string corrected_name = lcf::ReaderUtil::Normalize(translate?MakePath(MakePath(Tr::CurrTranslationId(), dir), name):name); + std::string corrected_name = lcf::ReaderUtil::Normalize(translate?MakePath(MakePath(Tr::GetCurrentTranslationId(), dir), name):name); std::string combined_path = MakePath(corrected_dir, corrected_name); std::string canon = MakeCanonical(combined_path, 1); diff --git a/src/game_map.cpp b/src/game_map.cpp index 44b91b3d18..1f0a37116f 100644 --- a/src/game_map.cpp +++ b/src/game_map.cpp @@ -299,7 +299,7 @@ std::unique_ptr Game_Map::loadMapFile(int map_id) { } void Game_Map::SetupCommon() { - if (!Tr::CurrTranslationId().empty()) { + if (!Tr::GetCurrentTranslationId().empty()) { // Build our map translation id. std::stringstream ss; ss << "map" << std::setfill('0') << std::setw(4) << GetMapId() << ".po"; diff --git a/src/scene_title.cpp b/src/scene_title.cpp index 7f1d6b9904..b55307055f 100644 --- a/src/scene_title.cpp +++ b/src/scene_title.cpp @@ -165,8 +165,8 @@ void Scene_Title::CreateTitleGraphic() { } } -void Scene_Title::RepositionWindow(Window_Command& window, bool centerVertical) { - if (!centerVertical) { +void Scene_Title::RepositionWindow(Window_Command& window, bool center_vertical) { + if (!center_vertical) { window.SetX(SCREEN_TARGET_WIDTH / 2 - window.GetWidth() / 2); window.SetY(SCREEN_TARGET_HEIGHT * 53 / 60 - window.GetHeight()); } else { @@ -232,9 +232,9 @@ void Scene_Title::CreateTranslationWindow() { // Push menu entries with the display name, but also save the directory location and help text. for (const Language& lg : Player::translation.GetLanguages()) { - lang_names.push_back(lg.langName); - lang_dirs.push_back(lg.langDir); - lang_helps.push_back(lg.langDesc); + lang_names.push_back(lg.lang_name); + lang_dirs.push_back(lg.lang_dir); + lang_helps.push_back(lg.lang_desc); } translate_window.reset(new Window_Command(lang_names)); @@ -309,17 +309,17 @@ void Scene_Title::CommandTranslation() { help_window->SetVisible(true); } -void Scene_Title::ChangeLanguage(const std::string& trstr) { +void Scene_Title::ChangeLanguage(const std::string& lang_str) { Main_Data::game_system->SePlay(Main_Data::game_system->GetSystemSE(Main_Data::game_system->SFX_Decision)); // No-op? - if (trstr == Player::translation.GetCurrLanguageId()) { + if (lang_str == Player::translation.GetCurrentLanguageId()) { HideTranslationWindow(); return; } // First change the language - Player::translation.SelectLanguage(trstr); + Player::translation.SelectLanguage(lang_str); // Now reset the scene (force asset reload) Scene::Push(std::make_shared(), true); diff --git a/src/scene_title.h b/src/scene_title.h index 4bf9894170..5fbb04f3c1 100644 --- a/src/scene_title.h +++ b/src/scene_title.h @@ -121,15 +121,16 @@ class Scene_Title : public Scene { /** * Moves a window (typically the New/Continue/Quit menu) to the middle or bottom-center of the screen. - * @param centerVertical If true, the menu will be centered vertically. Otherwise, it will be at the bottom of the screen. + * @param window The window to resposition. + * @param center_vertical If true, the menu will be centered vertically. Otherwise, it will be at the bottom of the screen. */ - void RepositionWindow(Window_Command& window, bool centerVertical); + void RepositionWindow(Window_Command& window, bool center_vertical); /** * Picks a new language based and switches to it. - * @param langStr If the empty string, switches the game to 'No Translation'. Otherwise, switch to that translation by name. + * @param lang_str If the empty string, switches the game to 'No Translation'. Otherwise, switch to that translation by name. */ - void ChangeLanguage(const std::string& langStr); + void ChangeLanguage(const std::string& lang_str); void HideTranslationWindow(); @@ -165,11 +166,11 @@ class Scene_Title : public Scene { * Stored in a struct for easy resetting, as Scene_Title can be reused. */ struct CommandIndices { - int new_game = 0; - int continue_game = 1; - int import = -1; - int translate = -1; - int exit = 2; + int new_game = 0; + int continue_game = 1; + int import = -1; + int translate = -1; + int exit = 2; }; CommandIndices indices; diff --git a/src/translation.cpp b/src/translation.cpp index 6f9ebccae1..b0a1de6dcf 100644 --- a/src/translation.cpp +++ b/src/translation.cpp @@ -49,12 +49,12 @@ #define TRCUST_ADDMSG "__EASY_RPG_CMD:ADD_MSGBOX__" -std::string Tr::TranslationDir() { +std::string Tr::GetTranslationDir() { return Player::translation.RootDir(); } -std::string Tr::CurrTranslationId() { - return Player::translation.GetCurrLanguageId(); +std::string Tr::GetCurrentTranslationId() { + return Player::translation.GetCurrentLanguageId(); } void Translation::Reset() @@ -62,8 +62,8 @@ void Translation::Reset() ClearTranslationLookups(); languages.clear(); - currLanguage = ""; - translationRootDir = ""; + current_language = ""; + translation_root_dir = ""; } void Translation::InitTranslations() @@ -76,25 +76,25 @@ void Translation::InitTranslations() auto langIt = tree->directories.find(TRDIR_NAME); if (langIt != tree->directories.end()) { // Save the root directory for later. - translationRootDir = langIt->second; + translation_root_dir = langIt->second; // Now list all directories within the translate dir - auto translation_path = FileFinder::MakePath(FileFinder::GetDirectoryTree()->directory_path, translationRootDir); + auto translation_path = FileFinder::MakePath(FileFinder::GetDirectoryTree()->directory_path, translation_root_dir); auto translation_tree = FileFinder::CreateDirectoryTree(translation_path, FileFinder::RECURSIVE); if (translation_tree == nullptr) { return; } // Now iterate over every subdirectory. for (const auto& trName : translation_tree->directories) { Language item; - item.langDir = trName.second; - item.langName = trName.second; + item.lang_dir = trName.second; + item.lang_name = trName.second; // If there's a manifest file, read the language name and help text from that. std::string metaName = FileFinder::FindDefault(*translation_tree, trName.second, TRFILE_META_INI); if (!metaName.empty()) { lcf::INIReader ini(metaName); - item.langName = ini.GetString("Language", "Name", item.langName); - item.langDesc = ini.GetString("Language", "Description", ""); + item.lang_name = ini.GetString("Language", "Name", item.lang_name); + item.lang_desc = ini.GetString("Language", "Description", ""); } languages.push_back(item); @@ -102,14 +102,14 @@ void Translation::InitTranslations() } } -std::string Translation::GetCurrLanguageId() const +std::string Translation::GetCurrentLanguageId() const { - return currLanguage; + return current_language; } std::string Translation::RootDir() const { - return translationRootDir; + return translation_root_dir; } bool Translation::HasTranslations() const @@ -123,21 +123,21 @@ const std::vector& Translation::GetLanguages() const } -void Translation::SelectLanguage(const std::string& langId) +void Translation::SelectLanguage(const std::string& lang_id) { // Try to read in our language files. - Output::Debug("Changing language to: '{}'", (!langId.empty() ? langId : "")); - if (!ParseLanguageFiles(langId)) { + Output::Debug("Changing language to: '{}'", (!lang_id.empty() ? lang_id : "")); + if (!ParseLanguageFiles(lang_id)) { return; } - currLanguage = langId; + current_language = lang_id; // We reload the entire database as a precaution. Player::LoadDatabase(); // Rewrite our database+messages (unless we are on the Default language). // Note that map Message boxes are changed on map load, to avoid slowdown here. - if (!currLanguage.empty()) { + if (!current_language.empty()) { RewriteDatabase(); RewriteTreemapNames(); RewriteBattleEventMessages(); @@ -149,15 +149,15 @@ void Translation::SelectLanguage(const std::string& langId) } -bool Translation::ParseLanguageFiles(const std::string& langId) +bool Translation::ParseLanguageFiles(const std::string& lang_id) { // Create the directory tree (for lookups). std::shared_ptr translation_tree; - if (langId != "") { - auto translation_path = FileFinder::MakePath(FileFinder::MakePath(FileFinder::GetDirectoryTree()->directory_path, RootDir()), langId); + if (lang_id != "") { + auto translation_path = FileFinder::MakePath(FileFinder::MakePath(FileFinder::GetDirectoryTree()->directory_path, RootDir()), lang_id); translation_tree = FileFinder::CreateDirectoryTree(translation_path, FileFinder::FILES); if (translation_tree == nullptr) { - Output::Warning("Translation for '{}' does not appear to exist", langId); + Output::Warning("Translation for '{}' does not appear to exist", lang_id); return false; } } @@ -273,94 +273,98 @@ namespace { public: CommandIterator(std::vector& commands) : commands(commands) {} - /// Returns true if the index is past the end of the command list + /** Returns true if the index is past the end of the command list */ bool Done() const { return index >= commands.size(); } - /// Advance the index through the command list by 1 + /** Advance the index through the command list by 1 */ void Advance() { index += 1; } - /// Retrieve the code of the EventCommand at the index - lcf::rpg::EventCommand::Code CurrCmdCode() const { + /** Retrieve the code of the EventCommand at the index */ + lcf::rpg::EventCommand::Code CurrentCmdCode() const { return static_cast(commands[index].code); } - /// Retrieve the string of the EventCommand at the index - std::string CurrCmdString() const { + /** Retrieve the string of the EventCommand at the index */ + std::string CurrentCmdString() const { return ToString(commands[index].string); } - /// Retrieve the indent level of the EventCommand at the index - int CurrCmdIndent() const { + /** Retrieve the indent level of the EventCommand at the index */ + int CurrentCmdIndent() const { return commands[index].indent; } - /// Retrieve parameter at position 'pos' of the EventCommand at the current index, or the devValue if no such parameter exists. - int CurrCmdParam(size_t pos, int defVal) const { + /** Retrieve parameter at position 'pos' of the EventCommand at the current index, or the devValue if no such parameter exists. */ + int CurrentCmdParam(size_t pos, int defVal) const { if (pos < commands[index].parameters.size()) { return commands[index].parameters[pos]; } return defVal; } - /// Returns true if the current Event Command is ShowMessage - bool CurrIsShowMessage() const { - return CurrCmdCode() == lcf::rpg::EventCommand::Code::ShowMessage; + /** Returns true if the current Event Command is ShowMessage */ + bool CurrentIsShowMessage() const { + return CurrentCmdCode() == lcf::rpg::EventCommand::Code::ShowMessage; } - /// Returns true if the current Event Command is ShowMessage_2 - bool CurrIsShowMessage2() const { - return CurrCmdCode() == lcf::rpg::EventCommand::Code::ShowMessage_2; + /** Returns true if the current Event Command is ShowMessage_2 */ + bool CurrentIsShowMessage2() const { + return CurrentCmdCode() == lcf::rpg::EventCommand::Code::ShowMessage_2; } - /// Returns true if the current Event Command is ShowChoice - bool CurrIsShowChoice() const { - return CurrCmdCode() == lcf::rpg::EventCommand::Code::ShowChoice; + /** Returns true if the current Event Command is ShowChoice */ + bool CurrentIsShowChoice() const { + return CurrentCmdCode() == lcf::rpg::EventCommand::Code::ShowChoice; } - /// Returns true if the current Event Command is ShowChoiceOption - bool CurrIsShowChoiceOption() const { - return CurrCmdCode() == lcf::rpg::EventCommand::Code::ShowChoiceOption; + /** Returns true if the current Event Command is ShowChoiceOption */ + bool CurrentIsShowChoiceOption() const { + return CurrentCmdCode() == lcf::rpg::EventCommand::Code::ShowChoiceOption; } - /// Returns true if the current Event Command is ShowChoiceEnd - bool CurrIsShowChoiceEnd() const { - return CurrCmdCode() == lcf::rpg::EventCommand::Code::ShowChoiceEnd; + /** Returns true if the current Event Command is ShowChoiceEnd */ + bool CurrentIsShowChoiceEnd() const { + return CurrentCmdCode() == lcf::rpg::EventCommand::Code::ShowChoiceEnd; } - /// Add each line of a [ShowMessage,ShowMessage_2,...] chain to "msg_str" (followed by a newline) - /// and save to "indexes" the index of each ShowMessage(2) command that was used to populate this - /// (for rewriting later). - /// Advances the index until after the last ShowMessage(2) command + /** + * Add each line of a [ShowMessage,ShowMessage_2,...] chain to "msg_str" (followed by a newline) + * and save to "indexes" the index of each ShowMessage(2) command that was used to populate this + * (for rewriting later). + * Advances the index until after the last ShowMessage(2) command + */ void BuildMessageString(std::stringstream& msg_str, std::vector& indexes) { // No change if we're not on the right command. - if (Done() || !CurrIsShowMessage()) { + if (Done() || !CurrentIsShowMessage()) { return; } // Add the first line - msg_str << CurrCmdString() <<"\n"; + msg_str << CurrentCmdString() <<"\n"; indexes.push_back(index); Advance(); // Build lines 2 through 4 - while (!Done() && CurrIsShowMessage2()) { - msg_str << CurrCmdString() <<"\n"; + while (!Done() && CurrentIsShowMessage2()) { + msg_str << CurrentCmdString() <<"\n"; indexes.push_back(index); Advance(); } } - /// Add each line of a [ShowChoice,ShowChoiceOption,...,ShowChoiceEnd] chain to "msg_str" (followed by a newline) - /// and save to "indexes" the index of each ShowChoiceOption command that was used to populate this - /// (for rewriting later). - /// Advances the index until after the (first) ShowChoice command (but it will likely still be on a ShowChoiceOption/End) + /** + * Add each line of a [ShowChoice,ShowChoiceOption,...,ShowChoiceEnd] chain to "msg_str" (followed by a newline) + * and save to "indexes" the index of each ShowChoiceOption command that was used to populate this + * (for rewriting later). + * Advances the index until after the (first) ShowChoice command (but it will likely still be on a ShowChoiceOption/End) + */ void BuildChoiceString(std::stringstream& msg_str, std::vector& indexes) { // No change if we're not on the right command. - if (Done() || !CurrIsShowChoice()) { + if (Done() || !CurrentIsShowChoice()) { return; } @@ -372,18 +376,18 @@ namespace { // Choices must be on the same indent level. // We have to save/restore the index, though, in the rare case that we skip something that can be translated. - int indent = CurrCmdIndent(); + int indent = CurrentCmdIndent(); size_t savedIndex = index; while (!Done()) { - if (indent == CurrCmdIndent()) { + if (indent == CurrentCmdIndent()) { // Handle a new index - if (CurrIsShowChoiceOption() && CurrCmdParam(0,0) < 4) { - msg_str << CurrCmdString() <<"\n"; + if (CurrentIsShowChoiceOption() && CurrentCmdParam(0,0) < 4) { + msg_str << CurrentCmdString() <<"\n"; indexes.push_back(index); } // Done? - if (CurrIsShowChoiceEnd()) { + if (CurrentIsShowChoiceEnd()) { break; } } @@ -392,16 +396,18 @@ namespace { index = savedIndex; } - /// Change the string value of the EventCommand at position "idx" to "newStr" + /** Change the string value of the EventCommand at position "idx" to "newStr" */ void ReWriteString(size_t idx, const std::string& newStr) { if (idx < commands.size()) { commands[idx].string = lcf::DBString(newStr); } } - /// Puts a "ShowMessage" or "ShowMessage_2" command into the command stack before position "idx". - /// Sets the string value to "line". Note that ShowMessage_2 is chosen if baseMsgBox is false. - /// This also updates the index if relevant, but it does not update external index caches. + /** + * Puts a "ShowMessage" or "ShowMessage_2" command into the command stack before position "idx". + * Sets the string value to "line". Note that ShowMessage_2 is chosen if baseMsgBox is false. + * This also updates the index if relevant, but it does not update external index caches. + */ void PutShowMessageBeforeIndex(const std::string& line, size_t idx, bool baseMsgBox) { // We need a reference index for the indent. size_t refIndent = 0; @@ -425,8 +431,10 @@ namespace { } } - /// Remove the EventCommand at position "idx" from the command stack. - /// Also updates our index, if relevant. + /** + * Remove the EventCommand at position "idx" from the command stack. + * Also updates our index, if relevant. + */ void RemoveByIndex(size_t idx) { if (idx < commands.size()) { commands.erase(commands.begin() + idx); @@ -438,9 +446,11 @@ namespace { } } - /// Add multiple message boxes to the command stack before "idx". - /// The "msgs" each represent lines in new, independent message boxes (so they will have both ShowMessage and ShowMessage_2) - /// Also updates our index, if relevant. + /** + * Add multiple message boxes to the command stack before "idx". + * The "msgs" each represent lines in new, independent message boxes (so they will have both ShowMessage and ShowMessage_2) + * Also updates our index, if relevant. + */ void InsertMultiMessageBefore(std::vector>& msgs, size_t idx) { for (std::vector& lines : msgs) { while (!lines.empty()) { @@ -505,7 +515,7 @@ void Translation::RewriteEventCommandMessage(const Dictionary& dict, std::vector CommandIterator commands(commandsOrig); while (!commands.Done()) { // We only need to deal with either Message or Choice commands - if (commands.CurrIsShowMessage()) { + if (commands.CurrentIsShowMessage()) { // Build up the lines of Message texts std::stringstream msg_str; std::vector msg_indexes; @@ -559,7 +569,7 @@ void Translation::RewriteEventCommandMessage(const Dictionary& dict, std::vector } // Note that commands.Advance() has already happened within the above code. - } else if (commands.CurrIsShowChoice()) { + } else if (commands.CurrentIsShowChoice()) { // Build up the lines of Choice elements std::stringstream choice_str; std::vector choice_indexes; // Number of entries == number of choices @@ -592,9 +602,9 @@ void Translation::RewriteEventCommandMessage(const Dictionary& dict, std::vector } } -void Translation::RewriteMapMessages(const std::string& mapName, lcf::rpg::Map& map) { +void Translation::RewriteMapMessages(const std::string& map_name, lcf::rpg::Map& map) { // Retrieve lookup for this map. - auto mapIt = maps.find(mapName); + auto mapIt = maps.find(map_name); if (mapIt==maps.end()) { return; } // Rewrite all event commands on all pages. diff --git a/src/translation.h b/src/translation.h index 97dc2c52c1..1e97938c2c 100644 --- a/src/translation.h +++ b/src/translation.h @@ -43,13 +43,13 @@ namespace Tr { * The name of the translation directory. * @return The translation directory. */ - std::string TranslationDir(); + std::string GetTranslationDir(); /** * The id of the current translation (e.g., "Spanish"). If empty, there is no active translation. * @return The translation ID */ - std::string CurrTranslationId(); + std::string GetCurrentTranslationId(); } // End namespace Tr @@ -132,9 +132,9 @@ bool Dictionary::TranslateString(const std::string& context, StringType& origina * Properties of a language */ struct Language { - std::string langDir; // Language directory (e.g., "en", "English_Localization") - std::string langName; // Display name for this language (e.g., "English") - std::string langDesc; // Helper text to show when the menu is highlighted + std::string lang_dir; // Language directory (e.g., "en", "English_Localization") + std::string lang_name; // Display name for this language (e.g., "English") + std::string lang_desc; // Helper text to show when the menu is highlighted }; @@ -173,24 +173,24 @@ class Translation { /** * Switches to a given language. Resets the database and the Image Cache. * - * @param langId The language ID (or "" for "Default") + * @param lang_id The language ID (or "" for "Default") */ - void SelectLanguage(const std::string& langId); + void SelectLanguage(const std::string& lang_id); /** * Rewrite all Messages and Choices in this Map * - * @param mapName The name of the map with formatting similar to the .po file; e.g., "map0104.po" + * @param map_name The name of the map with formatting similar to the .po file; e.g., "map0104.po" * @param map The map object itself (for modifying). */ - void RewriteMapMessages(const std::string& mapName, lcf::rpg::Map& map); + void RewriteMapMessages(const std::string& map_name, lcf::rpg::Map& map); /** * Retrieve the ID of the current (active) language. * * @return the current language ID, or "" for the Default language */ - std::string GetCurrLanguageId() const; + std::string GetCurrentLanguageId() const; private: @@ -215,10 +215,10 @@ class Translation { /** * Parse all .po files for the given language. * - * @param langId The ID of the language to parse, or "" for Default (no parsing is done) + * @param lang_id The ID of the language to parse, or "" for Default (no parsing is done) * @return True if the language directory was found; false otherwise */ - bool ParseLanguageFiles(const std::string& langId); + bool ParseLanguageFiles(const std::string& lang_id); /** * Rewrite RPG_RT.ldb with the current translation entries @@ -274,10 +274,10 @@ class Translation { std::vector languages; // The "languages" directory, but with appropriate capitalization. - std::string translationRootDir; + std::string translation_root_dir; // The translation we are currently showing (e.g., "English_1") - std::string currLanguage; + std::string current_language; }; #endif // EP_TRANSLATION_H From 87c8d99279a700377d1db2f4e6fa01679a5e7edf Mon Sep 17 00:00:00 2001 From: "Seth N. Hetu" Date: Thu, 17 Dec 2020 13:46:22 -0500 Subject: [PATCH 5/5] Replace magic strings with a more visually pleasing variant and expand on documentation --- src/translation.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/translation.cpp b/src/translation.cpp index b0a1de6dcf..7ce323e388 100644 --- a/src/translation.cpp +++ b/src/translation.cpp @@ -45,8 +45,11 @@ #define TRFILE_META_INI "META.INI" // Message box commands to remove a message box or add one in place. -#define TRCUST_REMOVEMSG "__EASY_RPG_CMD:REMOVE_MSGBOX__" -#define TRCUST_ADDMSG "__EASY_RPG_CMD:ADD_MSGBOX__" +// These commands are added by translators in the .po files to manipulate +// text boxes at runtime. They are magic strings that will not otherwise +// appear in the source of EasyRPG, but they should not be deleted. +#define TRCUST_REMOVEMSG "" +#define TRCUST_ADDMSG "" std::string Tr::GetTranslationDir() {