/* * Copyright (C) 2002-2021 The DOSBox Team * * This program 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 2 of the License, or * (at your option) any later version. * * This program 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 this program; if not, write to the Free Software Foundation, Inc., * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include #include #include #include "dosbox.h" #include "cross.h" #include "logging.h" #include "support.h" #include "setup.h" #include "render.h" #include "control.h" #include "shell.h" #include "menu.h" #include "jfont.h" #include "mapper.h" #include #include #include #include #include #include #include #if defined(__MINGW32__) && !defined(HX_DOS) #include #endif using namespace std; int msgcodepage = 0, lastmsgcp = 0; bool morelen = false, inmsg = false, loadlang = false; bool uselangcp = false; // True if Language file is loaded via CONFIG -set langcp option. Use codepage specified in the language file bool isSupportedCP(int newCP), CodePageHostToGuestUTF8(char *d/*CROSS_LEN*/,const char *s/*CROSS_LEN*/), CodePageGuestToHostUTF8(char *d/*CROSS_LEN*/,const char *s/*CROSS_LEN*/), systemmessagebox(char const * aTitle, char const * aMessage, char const * aDialogType, char const * aIconType, int aDefaultButton), OpenGL_using(void); void InitFontHandle(void), ShutFontHandle(void), refreshExtChar(void), SetIME(void), runRescan(const char *str), menu_update_dynamic(void), menu_update_autocycle(void), update_bindbutton_text(void), set_eventbutton_text(const char *eventname, const char *buttonname), JFONT_Init(), DOSBox_SetSysMenu(), UpdateSDLDrawTexture(), makestdcp950table(), makeseacp951table(); std::string langname = "", langnote = "", GetDOSBoxXPath(bool withexe=false); extern int lastcp, FileDirExistUTF8(std::string &localname, const char *name), toSetCodePage(DOS_Shell *shell, int newCP, int opt); extern bool dos_kernel_disabled, force_conversion, showdbcs, dbcs_sbcs, enableime, tonoime, chinasea, CHCP_changed; extern uint16_t GetDefaultCP(); extern const char * RunningProgram; Bitu DOS_ChangeKeyboardLayout(const char* layoutname, int32_t codepage); Bitu DOS_ChangeCodepage(int32_t codepage, const char* codepagefile); Bitu DOS_LoadKeyboardLayout(const char* layoutname, int32_t codepage, const char* codepagefile); const char* DOS_GetLoadedLayout(void); bool CheckDBCSCP(int32_t codepage); void MSG_Init(void); #define LINE_IN_MAXLEN 2048 struct MessageBlock { std::string name; std::string val; MessageBlock(const char* _name, const char* _val) : name(_name), val(_val) {} }; std::list LangList; // list of translation messages, order maintained in inserted order // Map for fast lookup std::unordered_map::iterator> LangMap; void MSG_Add(const char* _name, const char* _val) { //add messages to the translation message list // If the message already exists, ignore it if(LangMap.find(_name) != LangMap.end()) return; // Add the message to the end of the list LangList.emplace_back(_name, _val); // Save the position in the map LangMap[_name] = std::prev(LangList.end()); } void MSG_Replace(const char* _name, const char* _val) { auto it = LangMap.find(_name); if(it != LangMap.end()) { // Update the value while maintaining the order it->second->val = _val; } else { // If not found, add the message LangList.emplace_back(_name, _val); LangMap[_name] = std::prev(LangList.end()); } } const char* MSG_Get(const char* msg) { // add messages to the translation message list auto it = LangMap.find(msg); if(it != LangMap.end()) return it->second->val.c_str(); // Return the value if found return msg; // Return the original name if not found } std::string formatString(const char* format, ...) { /** * @brief Generates a formatted string using a format specifier and variable arguments. * * @param format A format string (e.g., "File: '%s', Error Code: %d, Language: '%s'.") * @param ... Values corresponding to format specifiers (e.g., "lang_en.msg", 404, "English"). * @return std::string A formatted string. */ va_list args; va_start(args, format); // Determine required buffer size int size = vsnprintf(nullptr, 0, format, args); va_end(args); if(size < 0) { return ""; // Return empty string on error } std::vector buffer(size + 1); // Allocate buffer (+1 for null-terminator) va_start(args, format); vsnprintf(buffer.data(), buffer.size(), format, args); va_end(args); return std::string(buffer.data()); } bool InitCodePage() { if (!dos.loaded_codepage || dos_kernel_disabled || force_conversion) { if (((control->opt_langcp && msgcodepage != dos.loaded_codepage) || uselangcp) && msgcodepage>0) { dos.loaded_codepage = msgcodepage; return true; } Section_prop *section = static_cast(control->GetSection("config")); if (section!=NULL && !control->opt_noconfig && !IS_PC98_ARCH && !IS_JEGA_ARCH && !IS_DOSV) { char *countrystr = (char *)section->Get_string("country"), *r=strchr(countrystr, ','); if (r!=NULL && *(r+1)) { int cp = atoi(trim(r+1)); if(cp > 0 && isSupportedCP(cp) && !msgcodepage) { dos.loaded_codepage = cp; return true; } else dos.loaded_codepage = msgcodepage; return true; } } if (msgcodepage>0) { dos.loaded_codepage = msgcodepage; return true; } } if (!dos.loaded_codepage) { dos.loaded_codepage = GetDefaultCP(); return false; } else return true; } void AddMessages() { MSG_Add("AUTOEXEC_CONFIGFILE_HELP", "Lines in this section will be run at startup.\n" "You can put your MOUNT lines here.\n" ); MSG_Add("CONFIGFILE_INTRO", "# This is the configuration file for DOSBox-X %s. (Please use the latest version of DOSBox-X)\n" "# Lines starting with a # are comment lines and are ignored by DOSBox-X.\n" "# They are used to (briefly) document the effect of each option.\n" "# To write out ALL options, use command 'config -all' with -wc or -writeconf options.\n"); MSG_Add("CONFIG_SUGGESTED_VALUES", "Possible values"); MSG_Add("CONFIG_ADVANCED_OPTION", "Advanced options (see full configuration reference file [dosbox-x.reference.full.conf] for more details)"); MSG_Add("CONFIG_TOOL","DOSBox-X Configuration Tool"); MSG_Add("CONFIG_TOOL_EXIT","Exit configuration tool"); MSG_Add("MAPPER_EDITOR_EXIT","Exit mapper editor"); MSG_Add("SAVE_MAPPER_FILE","Save mapper file"); MSG_Add("WARNING","Warning"); MSG_Add("ERROR", "Error"); MSG_Add("INFORMATION", "Information"); MSG_Add("YES","Yes"); MSG_Add("NO","No"); MSG_Add("OK","OK"); MSG_Add("CANCEL","Cancel"); MSG_Add("CLOSE","Close"); MSG_Add("DEBUGCMD","Enter Debugger Command"); MSG_Add("ADD","Add"); MSG_Add("DEL","Del"); MSG_Add("NEXT","Next"); MSG_Add("SAVE","Save"); MSG_Add("EXIT","Exit"); MSG_Add("CAPTURE","Capture"); MSG_Add("SAVE_CONFIGURATION","Save configuration"); MSG_Add("SAVE_LANGUAGE","Save language file"); MSG_Add("SAVE_RESTART","Save & Restart"); MSG_Add("PASTE_CLIPBOARD","Paste Clipboard"); MSG_Add("APPEND_HISTORY","Append History"); MSG_Add("EXECUTE_NOW","Execute Now"); MSG_Add("ADDITION_CONTENT","Additional Content:"); MSG_Add("CONTENT","Content:"); MSG_Add("EDIT_FOR","Edit %s"); MSG_Add("HELP_FOR","Help for %s"); MSG_Add("SELECT_VALUE", "Select property value"); MSG_Add("CONFIGURATION","Configuration"); MSG_Add("SETTINGS","Settings"); MSG_Add("LOGGING_OUTPUT","DOSBox-X logging output"); MSG_Add("CODE_OVERVIEW","Code overview"); MSG_Add("VISIT_HOMEPAGE","Visit Homepage"); MSG_Add("GET_STARTED","Getting Started"); MSG_Add("CDROM_SUPPORT","CD-ROM Support"); MSG_Add("DRIVE_INFORMATION","Drive information"); MSG_Add("MOUNTED_DRIVE_NUMBER","Mounted drive numbers"); MSG_Add("IDE_CONTROLLER_ASSIGNMENT","IDE controller assignment"); MSG_Add("HELP_COMMAND","Help on DOS command"); MSG_Add("CURRENT_VOLUME","Current sound mixer volumes"); MSG_Add("CURRENT_SBCONFIG","Sound Blaster configuration"); MSG_Add("CURRENT_MIDICONFIG","Current MIDI configuration"); MSG_Add("CREATE_IMAGE","Create blank disk image"); MSG_Add("NETWORK_LIST","Network interface list"); MSG_Add("PRINTER_LIST","Printer device list"); MSG_Add("INTRODUCTION","Introduction"); MSG_Add("SHOW_ADVOPT", "Show advanced options"); MSG_Add("USE_PRIMARYCONFIG", "Use primary config file"); MSG_Add("USE_PORTABLECONFIG", "Use portable config file"); MSG_Add("USE_USERCONFIG", "Use user config file"); MSG_Add("CONFIG_SAVETO", "Enter filename for the configuration file to save to:"); MSG_Add("CONFIG_SAVEALL", "Save all (including advanced) config options to the configuration file"); MSG_Add("LANG_FILENAME", "Enter filename for language file:"); MSG_Add("LANG_LANGNAME", "Language name (optional):"); MSG_Add("INTRO_MESSAGE", "Welcome to DOSBox-X, a free and complete DOS emulation package.\nDOSBox-X creates a DOS shell which looks like the plain DOS.\nYou can also run Windows 3.x and 9x/Me inside the DOS machine."); MSG_Add("DRIVE","Drive"); MSG_Add("TYPE","Type"); MSG_Add("LABEL","Label"); MSG_Add("DRIVE_NUMBER","Drive number"); MSG_Add("DISK_NAME","Disk name"); MSG_Add("IDE_POSITION","IDE position"); MSG_Add("SWAP_SLOT","Swap slot"); MSG_Add("EMPTY_SLOT","Empty slot"); MSG_Add("SLOT","Slot"); MSG_Add("SELECT_EVENT", "Select an event to change."); MSG_Add("SELECT_DIFFERENT_EVENT", "Select an event or press Add/Del/Next buttons."); MSG_Add("PRESS_JOYSTICK_KEY", "Press a key/joystick button or move the joystick."); MSG_Add("CAPTURE_ENABLED", "Capture enabled. Hit ESC to release capture."); MSG_Add("MAPPER_FILE_SAVED", "Mapper file saved"); MSG_Add("AUTO_CYCLE_MAX","Auto cycles [max]"); MSG_Add("AUTO_CYCLE_AUTO","Auto cycles [auto]"); MSG_Add("AUTO_CYCLE_OFF","Auto cycles [off]"); MSG_Add("LANG_LOAD_ERROR", "Could not load language message file %s. The default language will be used."); MSG_Add("LANG_JP_INCOMPATIBLE", "You have specified a language file which uses a code page incompatible with the Japanese PC-98 or JEGA/AX system.\n\n" "Are you sure to use the language file for this machine type?"); MSG_Add("LANG_DOSV_INCOMPATIBLE", "You have specified a language file which uses a code page incompatible with the current DOS/V system.\n\n" "Are you sure to use the language file for this system type?"); MSG_Add("LANG_CHANGE_CP", "The specified language file uses code page %d. Do you want to change to this code page accordingly?"); } // True if specified codepage is a DBCS codepage bool CheckDBCSCP(int32_t codepage) { if(codepage == 932 || codepage == 936 || codepage == 949 || codepage == 950 || codepage == 951) { //LOG_MSG("CheckDBCSCP: Codepage %d true", codepage); return true; } else return false; } FILE* testLoadLangFile(const char* fname) { std::string exepath = GetDOSBoxXPath(); std::string config_path, res_path; Cross::GetPlatformConfigDir(config_path); Cross::GetPlatformResDir(res_path); std::vector base_paths = { "", exepath, config_path, res_path, "languages/", exepath + "languages/", config_path + "languages/", res_path + "languages/", "language/", exepath + "language/", config_path + "language/", res_path + "language/" }; std::vector suffixes = { "", ".lng" }; for(const auto& base : base_paths) { for(const auto& suffix : suffixes) { std::string full_path = base + fname + suffix; FILE* mfile = fopen(full_path.c_str(), "rt"); if(mfile) return mfile; } } #if defined(WIN32) && defined(C_SDL2) std::string localname = fname; if(FileDirExistUTF8(localname, fname) == 1) { FILE* mfile = fopen(localname.c_str(), "rt"); if(mfile) return mfile; } #endif return nullptr; } static std::string loaded_fname; void LoadMessageFile(const char* fname) { if(!fname || *fname == '\0') return; if(loaded_fname == fname) { return; } LOG(LOG_MISC, LOG_DEBUG)("Loading message file %s", fname); FILE* mfile = testLoadLangFile(fname); if(!mfile) { std::string message = formatString(MSG_Get("LANG_LOAD_ERROR"), fname); systemmessagebox("Warning", message.c_str(), "ok", "warning", 1); SetVal("dosbox", "language", ""); LOG_MSG("MSG:Cannot load language file: %s", fname); control->opt_lang = ""; return; } loaded_fname = fname; langname.clear(); langnote.clear(); std::string linein, name, menu_name, mapper_name, string; morelen = inmsg = true; bool res = true; bool loadlangcp = false; int cp = dos.loaded_codepage; if(!dos.loaded_codepage) res = InitCodePage(); const size_t buffer_size = LINE_IN_MAXLEN + 1024; char* buffer = new char[buffer_size]; while(fgets(buffer, buffer_size, mfile)) { linein = buffer; // Remove \r and \n linein.erase(std::remove(linein.begin(), linein.end(), '\r'), linein.end()); linein.erase(std::remove(linein.begin(), linein.end(), '\n'), linein.end()); if(linein.empty()) { string += "\n"; continue; } std::string trimmed = linein.substr(1); trim(trimmed); if(linein[0] == ':') { string.clear(); if(!strncasecmp(linein.c_str() + 1, "DOSBOX-X:", 9)) { std::string p = linein.substr(10); size_t colon_pos = p.find(':'); if(colon_pos != std::string::npos && colon_pos > 0 && colon_pos + 1 < p.size()) { std::string key = p.substr(0, colon_pos); std::string val = p.substr(colon_pos + 1); if(key == "CODEPAGE") { int c = std::atoi(val.c_str()); if(!isSupportedCP(c)) { LOG_MSG("Language file: Invalid codepage :DOSBOX-X:CODEPAGE:%d", c); loadlangcp = false; } else if(((IS_PC98_ARCH || IS_JEGA_ARCH) && c != 437 && c != 932 && !systemmessagebox("DOSBox-X language file", MSG_Get("LANG_JP_INCOMPATIBLE"), "yesno", "question", 2)) || (((IS_JDOSV && c != 932) || (IS_PDOSV && c != 936) || (IS_KDOSV && c != 949) || (IS_TDOSV && c != 950 && c != 951)) && c != 437 && !systemmessagebox("DOSBox-X language file", MSG_Get("LANG_DOSV_INCOMPATIBLE"), "yesno", "question", 2))) { fclose(mfile); dos.loaded_codepage = cp; return; } else { std::string msg = formatString(MSG_Get("LANG_CHANGE_CP"), c); if(c == dos.loaded_codepage) { msgcodepage = c; lastmsgcp = msgcodepage; } if(c != dos.loaded_codepage && (control->opt_langcp || uselangcp || !CHCP_changed || CheckDBCSCP(c) || !loadlang || (loadlang && systemmessagebox("DOSBox-X language file", msg.c_str(), "yesno", "question", 1)))) { loadlangcp = true; if(c == 950 && dos.loaded_codepage == 951) msgcodepage = 951; // zh_tw defaults to CP950, but CP951 is acceptable as well so keep it else if(c == 951 && dos.loaded_codepage == 950) msgcodepage = 950; // And vice versa for lang files requiring CP951 else msgcodepage = c; dos.loaded_codepage = c; if(c == 950 && !chinasea) makestdcp950table(); if(c == 951 && chinasea) makeseacp951table(); lastmsgcp = c; } } } else if(key == "LANGUAGE") { langname = val; } else if(key == "REMARK") { langnote = val; } } } else if(!strncasecmp(linein.c_str() + 1, "MENU:", 5) && linein.size() > 6) { name.clear(); mapper_name.clear(); menu_name = linein.substr(6); } else if(!strncasecmp(linein.c_str() + 1, "MAPPER:", 7) && linein.size() > 8) { name.clear(); menu_name.clear(); mapper_name = linein.substr(8); } else { menu_name.clear(); mapper_name.clear(); name = linein.substr(1); } } else if(linein[0] == '.' && trimmed.empty()) { if(!string.empty() && string.back() == '\n') string.pop_back(); if(!name.empty()) { MSG_Replace(name.c_str(), string.c_str()); } else if(menu_name.size() > 6 && menu_name.rfind("drive_", 0) == 0) { for(char c = 'A'; c <= 'Z'; ++c) { std::string mname = "drive_" + std::string(1, c) + menu_name.substr(5); if(mainMenu.item_exists(mname)) { mainMenu.get_item(mname).set_text(string); } } } else if(!menu_name.empty() && mainMenu.item_exists(menu_name)) { mainMenu.get_item(menu_name).set_text(string); if(menu_name.rfind("mapper_", 0) == 0 && menu_name.size() > 7) { set_eventbutton_text(menu_name.c_str() + 7, string.c_str()); } } else if(!mapper_name.empty()) { set_eventbutton_text(mapper_name.c_str(), string.c_str()); } } else { size_t temp_size = linein.size() * 3 + 1; // Converting to UTF-8 will expand to max. 3-bytes / character + null terminator std::vector temp(temp_size); if(!CodePageHostToGuestUTF8(temp.data(), linein.c_str())) { string += linein + "\n"; } else { string += std::string(temp.data()) + "\n"; } } } morelen = inmsg = false; delete[] buffer; fclose(mfile); menu_update_dynamic(); menu_update_autocycle(); update_bindbutton_text(); dos.loaded_codepage = cp; if(loadlangcp && msgcodepage > 0) { const char* layoutname = DOS_GetLoadedLayout(); if(!IS_DOSV && !IS_JEGA_ARCH && !IS_PC98_ARCH && layoutname != nullptr) { toSetCodePage(nullptr, msgcodepage, -1); } } refreshExtChar(); LOG_MSG("LoadMessageFile: Loaded language file: %s", fname); loadlang = true; } bool MSG_Write(const char* location, const char* name) { std::unique_ptr temp(new char[4096]); FILE* out = fopen(location, "w+t"); if(out == nullptr) return false; // Failed to open file for writing if(name != nullptr) langname = std::string(name); if(!langname.empty()) fprintf(out, ":DOSBOX-X:LANGUAGE:%s\n", langname.c_str()); if(dos.loaded_codepage) fprintf(out, ":DOSBOX-X:CODEPAGE:%d\n", dos.loaded_codepage); fprintf(out, ":DOSBOX-X:VERSION:%s\n", VERSION); fprintf(out, ":DOSBOX-X:REMARK:%s\n", langnote.c_str()); morelen = inmsg = true; // Output messages in insertion order using LangList for(const auto& msg : LangList) { const char* out_text = msg.val.c_str(); if(!CodePageGuestToHostUTF8(temp.get(), out_text)) fprintf(out, ":%s\n%s\n.\n", msg.name.c_str(), out_text); else fprintf(out, ":%s\n%s\n.\n", msg.name.c_str(), temp.get()); } // Output menu items (menu filtering conditions remain unchanged) std::vector master_list = mainMenu.get_master_list(); for(auto& id : master_list) { if(!id.is_allocated() || id.get_type() == DOSBoxMenu::separator_type_id || id.get_type() == DOSBoxMenu::vseparator_type_id || (id.get_name().size() == 5 && id.get_name().substr(0, 4) == "slot") || (id.get_name().size() > 9 && id.get_name().substr(0, 8) == "command_") || (id.get_name().size() == 6 && id.get_name().substr(0, 5) == "Drive" && id.get_name().back() >= 'A' && id.get_name().back() <= 'Z') || (id.get_name().size() > 9 && id.get_name().substr(0, 6) == "drive_" && id.get_name()[6] >= 'B' && id.get_name()[6] <= 'Z' && id.get_name()[7] == '_') || id.get_name() == "mapper_cycauto") continue; std::string text = id.get_text(); if(id.get_name() == "hostkey_mapper" || id.get_name() == "clipboard_device") { size_t found = text.find(":"); if(found != std::string::npos) text = text.substr(0, found); } std::string idname = (id.get_name().size() > 9 && id.get_name().substr(0, 8) == "drive_A_") ? "drive_" + id.get_name().substr(8) : id.get_name(); const char* out_text = text.c_str(); if(!CodePageGuestToHostUTF8(temp.get(), out_text)) fprintf(out, ":MENU:%s\n%s\n.\n", idname.c_str(), out_text); else fprintf(out, ":MENU:%s\n%s\n.\n", idname.c_str(), temp.get()); } // Output MAPPER items that differ from mainMenu mappings std::map event_map = get_event_map(); for(const auto& it : event_map) { if(mainMenu.item_exists("mapper_" + it.first) && mainMenu.get_item("mapper_" + it.first).get_text() == it.second) continue; const char* out_text = it.second.c_str(); if(!CodePageGuestToHostUTF8(temp.get(), out_text)) fprintf(out, ":MAPPER:%s\n%s\n.\n", it.first.c_str(), out_text); else fprintf(out, ":MAPPER:%s\n%s\n.\n", it.first.c_str(), temp.get()); } morelen = inmsg = false; fclose(out); return true; } void ResolvePath(std::string& in); void MSG_Init() { Section_prop *section=static_cast(control->GetSection("dosbox")); if (control->opt_lang != "") { LoadMessageFile(control->opt_lang.c_str()); SetVal("dosbox", "language", control->opt_lang.c_str()); if (control->opt_langcp && msgcodepage>0 && isSupportedCP(msgcodepage)) { Section_prop *sec = static_cast(control->GetSection("config")); char cstr[20]; cstr[0] = 0; if (sec!=NULL) { char *countrystr = (char *)sec->Get_string("country"), *r=strchr(countrystr, ','); if (r!=NULL) *r=0; if (strlen(countrystr)>10) countrystr[0] = 0; sprintf(cstr, "%s,%d", countrystr, msgcodepage); SetVal("config", "country", cstr); const char *imestr = section->Get_string("ime"); if (tonoime && !strcasecmp(imestr, "auto") && CheckDBCSCP(msgcodepage)) { tonoime = false; enableime = true; SetIME(); } } } if (tonoime) { tonoime = enableime = false; #if defined(WIN32) && !defined(HX_DOS) && !defined(_WIN32_WINDOWS) ImmDisableIME((DWORD)(-1)); #endif SetIME(); } } else { Prop_path* pathprop = section->Get_path("language"); if (pathprop != NULL) { std::string path = pathprop->realpath; ResolvePath(path); if (testLoadLangFile(path.c_str())) LoadMessageFile(path.c_str()); else { std::string lang = section->Get_string("language"); if (lang.size()) LoadMessageFile(lang.c_str()); } } } std::string showdbcsstr = static_cast(control->GetSection("dosv"))->Get_string("showdbcsnodosv"); #if defined(USE_TTF) showdbcs = showdbcsstr=="true"||showdbcsstr=="1"||(showdbcsstr=="auto" && (loadlang || dbcs_sbcs)); #else showdbcs = showdbcsstr=="true"||showdbcsstr=="1"||(showdbcsstr=="auto" && loadlang); #endif if (!IS_EGAVGA_ARCH) showdbcs = false; }