#include "Local.h" #include "Log.h" #include "Plugin.h" #include "config/Config.hpp" #include #include #include #include #include #include #include #include #include #include #include #include #include "BaseDefine.h" #include "string_parser/StringParser.hpp" #include "cpprest/details/http_helpers.h" namespace GakumasLocal::Local { std::unordered_map i18nData{}; std::unordered_map i18nDumpData{}; std::unordered_map genericText{}; std::unordered_map genericSplitText{}; std::unordered_map genericFmtText{}; std::vector genericTextDumpData{}; std::vector genericSplittedDumpData{}; std::vector genericOrigTextDumpData{}; std::vector genericFmtTextDumpData{}; std::unordered_set translatedText{}; int genericDumpFileIndex = 0; const std::string splitTextPrefix = "[__split__]"; std::filesystem::path GetBasePath() { return Plugin::GetInstance().GetHookInstaller()->localizationFilesDir; } std::string trim(const std::string& str) { auto is_not_space = [](char ch) { return !std::isspace(ch); }; auto start = std::ranges::find_if(str, is_not_space); auto end = std::ranges::find_if(str | std::views::reverse, is_not_space).base(); if (start < end) { return {start, end}; } return ""; } std::string findInMapIgnoreSpace(const std::string& key, const std::unordered_map& searchMap) { auto is_space = [](char ch) { return std::isspace(ch); }; auto front = std::ranges::find_if_not(key, is_space); auto back = std::ranges::find_if_not(key | std::views::reverse, is_space).base(); std::string prefix(key.begin(), front); std::string suffix(back, key.end()); std::string trimmedKey = trim(key); if ( auto it = searchMap.find(trimmedKey); it != searchMap.end()) { return prefix + it->second + suffix; } else { return ""; } } enum class DumpStrStat { DEFAULT = 0, SPLITTABLE_ORIG = 1, SPLITTED = 2, FMT = 3 }; enum class SplitTagsTranslationStat { NO_TRANS, PART_TRANS, FULL_TRANS, NO_SPLIT, NO_SPLIT_AND_EMPTY }; void LoadJsonDataToMap(const std::filesystem::path& filePath, std::unordered_map& dict, const bool insertToTranslated = false, const bool needClearDict = true, const bool needCheckSplitPrefix = false) { if (!exists(filePath)) return; try { if (needClearDict) { dict.clear(); } std::ifstream file(filePath); if (!file.is_open()) { Log::ErrorFmt("Load %s failed.\n", filePath.string().c_str()); return; } std::string fileContent((std::istreambuf_iterator(file)), std::istreambuf_iterator()); file.close(); auto fileData = nlohmann::json::parse(fileContent); for (auto& i : fileData.items()) { const auto& key = i.key(); const std::string value = i.value(); if (needCheckSplitPrefix && key.starts_with(splitTextPrefix) && value.starts_with(splitTextPrefix)) { static const auto splitTextPrefixLength = splitTextPrefix.size(); const auto splitValue = value.substr(splitTextPrefixLength); genericSplitText[key.substr(splitTextPrefixLength)] = splitValue; if (insertToTranslated) translatedText.emplace(splitValue); } else { dict[key] = value; if (insertToTranslated) translatedText.emplace(value); } } } catch (std::exception& e) { Log::ErrorFmt("Load %s failed: %s\n", filePath.string().c_str(), e.what()); } } void DumpMapDataToJson(const std::filesystem::path& dumpBasePath, const std::filesystem::path& fileName, const std::unordered_map& dict) { const auto dumpFilePath = dumpBasePath / fileName; try { if (!is_directory(dumpBasePath)) { std::filesystem::create_directories(dumpBasePath); } if (!std::filesystem::exists(dumpFilePath)) { std::ofstream dumpWriteLrcFile(dumpFilePath, std::ofstream::out); dumpWriteLrcFile << "{}"; dumpWriteLrcFile.close(); } std::ifstream dumpLrcFile(dumpFilePath); std::string fileContent((std::istreambuf_iterator(dumpLrcFile)), std::istreambuf_iterator()); dumpLrcFile.close(); auto fileData = nlohmann::ordered_json::parse(fileContent); for (const auto& i : dict) { fileData[i.first] = i.second; } const auto newStr = fileData.dump(4, 32, false); std::ofstream dumpWriteLrcFile(dumpFilePath, std::ofstream::out); dumpWriteLrcFile << newStr.c_str(); dumpWriteLrcFile.close(); } catch (std::exception& e) { Log::ErrorFmt("DumpMapDataToJson %s failed: %s", dumpFilePath.c_str(), e.what()); } } void DumpVectorDataToJson(const std::filesystem::path& dumpBasePath, const std::filesystem::path& fileName, const std::vector& vec, const std::string& prefix = "") { const auto dumpFilePath = dumpBasePath / fileName; try { if (!is_directory(dumpBasePath)) { std::filesystem::create_directories(dumpBasePath); } if (!std::filesystem::exists(dumpFilePath)) { std::ofstream dumpWriteLrcFile(dumpFilePath, std::ofstream::out); dumpWriteLrcFile << "{}"; dumpWriteLrcFile.close(); } std::ifstream dumpLrcFile(dumpFilePath); std::string fileContent((std::istreambuf_iterator(dumpLrcFile)), std::istreambuf_iterator()); dumpLrcFile.close(); auto fileData = nlohmann::ordered_json::parse(fileContent); for (const auto& i : vec) { if (!prefix.empty()) { fileData[prefix + i] = prefix + i; } else { fileData[i] = i; } } const auto newStr = fileData.dump(4, 32, false); std::ofstream dumpWriteLrcFile(dumpFilePath, std::ofstream::out); dumpWriteLrcFile << newStr.c_str(); dumpWriteLrcFile.close(); } catch (std::exception& e) { Log::ErrorFmt("DumpVectorDataToJson %s failed: %s", dumpFilePath.c_str(), e.what()); } } std::string to_lower(const std::string& str) { std::string lower_str = str; std::transform(lower_str.begin(), lower_str.end(), lower_str.begin(), ::tolower); return lower_str; } bool IsPureStringValue(const std::string& str) { static std::unordered_set notDeeds = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', ':', '/', ' ', '.', '%', ',', '+', '-', 'x', '\n'}; for (const auto& i : str) { if (!notDeeds.contains(i)) { return false; } } return true; } std::vector SplitByTags(const std::string& origText) { static const std::regex tagsRe("<.*?>(.*?)"); std::string text = origText; std::smatch match; std::vector ret{}; std::string lastSuffix; while (std::regex_search(text, match, tagsRe)) { const auto tagValue = match[1].str(); if (IsPureStringValue(tagValue)) { ret.push_back(match.prefix().str()); lastSuffix = match.suffix().str(); } text = match.suffix().str(); } if (!lastSuffix.empty()) { ret.push_back(lastSuffix); } return ret; } void ProcessGenericTextLabels() { std::unordered_map appendsText{}; for (const auto& i : genericText) { const auto origContents = SplitByTags(i.first); if (origContents.empty()) { continue; } const auto translatedContents = SplitByTags(i.second); if (origContents.size() == translatedContents.size()) { for (const auto& [orig, trans] : std::ranges::views::zip(origContents, translatedContents)) { appendsText.emplace(orig, trans); } } } genericText.insert(appendsText.begin(), appendsText.end()); } bool ReplaceString(std::string* str, const std::string& oldSubstr, const std::string& newSubstr) { size_t pos = str->find(oldSubstr); if (pos != std::string::npos) { str->replace(pos, oldSubstr.length(), newSubstr); return true; } return false; } bool GetSplitTagsTranslation(const std::string& origText, std::string* newText, std::vector& unTransResultRet) { if (!origText.contains('<')) return false; const auto splitResult = SplitByTags(origText); if (splitResult.empty()) return false; *newText = origText; bool ret = true; for (const auto& i : splitResult) { if (const auto iter = genericText.find(i); iter != genericText.end()) { ReplaceString(newText, i, iter->second); } else { unTransResultRet.emplace_back(i); ret = false; } } return ret; } void ReplaceNumberComma(std::string* orig) { if (!orig->contains(",")) return; std::string newStr = *orig; ReplaceString(&newStr, ",", ","); if (IsPureStringValue(newStr)) { *orig = newStr; } } SplitTagsTranslationStat GetSplitTagsTranslationFull(const std::string& origTextIn, std::string* newText, std::vector& unTransResultRet) { // static const std::u16string splitFlags = u"0123456789++--%%【】."; static const std::unordered_set splitFlags = {u'0', u'1', u'2', u'3', u'4', u'5', u'6', u'7', u'8', u'9', u'+', u'+', u'-', u'-', u'%', u'%', u'【', u'】', u'.', u':', u':', u'×'}; const auto origText = Misc::ToUTF16(origTextIn); bool isInTag = false; std::vector waitingReplaceTexts{}; std::u16string currentWaitingReplaceText; #ifdef GKMS_WINDOWS #define checkCurrentWaitingReplaceTextAndClear() \ if (!currentWaitingReplaceText.empty()) { \ auto trimmed = trim(Misc::ToUTF8(currentWaitingReplaceText)); \ waitingReplaceTexts.push_back(trimmed); \ currentWaitingReplaceText.clear(); } #else #define checkCurrentWaitingReplaceTextAndClear() \ if (!currentWaitingReplaceText.empty()) { \ waitingReplaceTexts.push_back(Misc::ToUTF8(currentWaitingReplaceText)); \ currentWaitingReplaceText.clear(); } #endif for (char16_t currChar : origText) { if (currChar == u'<') { isInTag = true; } if (currChar == u'>') { isInTag = false; checkCurrentWaitingReplaceTextAndClear() continue; } if (isInTag) { checkCurrentWaitingReplaceTextAndClear() continue; } if (!splitFlags.contains(currChar)) { currentWaitingReplaceText.push_back(currChar); } else { checkCurrentWaitingReplaceTextAndClear() } } if (waitingReplaceTexts.empty()) { if (currentWaitingReplaceText.empty()) { return SplitTagsTranslationStat::NO_SPLIT_AND_EMPTY; } else { if (!(!origText.empty() && splitFlags.contains(origText[0]))) { // 开头为特殊符号或数字 return SplitTagsTranslationStat::NO_SPLIT; } } } checkCurrentWaitingReplaceTextAndClear() *newText = origTextIn; SplitTagsTranslationStat ret; bool hasTrans = false; bool hasNotTrans = false; if (!waitingReplaceTexts.empty()) { for (const auto& i : waitingReplaceTexts) { std::string searchResult = findInMapIgnoreSpace(i, genericSplitText); if (!searchResult.empty()) { ReplaceNumberComma(&searchResult); ReplaceString(newText, i, searchResult); hasTrans = true; } else { unTransResultRet.emplace_back(trim(i)); hasNotTrans = true; } } if (hasTrans && hasNotTrans) { ret = SplitTagsTranslationStat::PART_TRANS; } else if (hasTrans && !hasNotTrans) { ret = SplitTagsTranslationStat::FULL_TRANS; } else { ret = SplitTagsTranslationStat::NO_TRANS; } } else { ret = SplitTagsTranslationStat::NO_TRANS; } return ret; } void LoadData() { static auto localizationFile = GetBasePath() / "local-files" / "localization.json"; static auto genericFile = GetBasePath() / "local-files" / "generic.json"; static auto genericSplitFile = GetBasePath() / "local-files" / "generic.split.json"; static auto genericDir = GetBasePath() / "local-files" / "genericTrans"; if (!std::filesystem::is_regular_file(localizationFile)) { Log::ErrorFmt("localizationFile: %s not found.", localizationFile.c_str()); return; } LoadJsonDataToMap(localizationFile, i18nData, true); Log::InfoFmt("%ld localization items loaded.", i18nData.size()); LoadJsonDataToMap(genericFile, genericText, true, true, true); genericSplitText.clear(); genericFmtText.clear(); LoadJsonDataToMap(genericSplitFile, genericSplitText, true, true, true); if (std::filesystem::exists(genericDir) || std::filesystem::is_directory(genericDir)) { for (const auto& entry : std::filesystem::recursive_directory_iterator(genericDir)) { if (std::filesystem::is_regular_file(entry.path())) { const auto& currFile = entry.path(); if (to_lower(currFile.extension().string()) == ".json") { if (currFile.filename().string().ends_with(".split.json")) { // split text file LoadJsonDataToMap(currFile, genericSplitText, true, false, true); } if (currFile.filename().string().ends_with(".fmt.json")) { // fmt text file LoadJsonDataToMap(currFile, genericFmtText, true, false, false); } else { LoadJsonDataToMap(currFile, genericText, true, false, true); } } } } } ProcessGenericTextLabels(); Log::InfoFmt("%ld generic text items loaded.", genericText.size()); static auto dumpBasePath = GetBasePath() / "dump-files"; static auto dumpFilePath = dumpBasePath / "localization.json"; LoadJsonDataToMap(dumpFilePath, i18nDumpData); } bool GetI18n(const std::string& key, std::string* ret) { if (const auto iter = i18nData.find(key); iter != i18nData.end()) { *ret = iter->second; return true; } return false; } bool inDump = false; void DumpI18nItem(const std::string& key, const std::string& value) { if (!Config::dumpText) return; if (i18nDumpData.contains(key)) return; i18nDumpData[key] = value; Log::DebugFmt("DumpI18nItem: %s - %s", key.c_str(), value.c_str()); static auto dumpBasePath = GetBasePath() / "dump-files"; if (inDump) return; inDump = true; std::thread([](){ std::this_thread::sleep_for(std::chrono::seconds(5)); DumpMapDataToJson(dumpBasePath, "localization.json", i18nDumpData); inDump = false; }).detach(); } std::string readFileToString(const std::string& filename) { std::ifstream file(filename); if (!file.is_open()) { throw std::exception(); } std::string content((std::istreambuf_iterator(file)), (std::istreambuf_iterator())); file.close(); return content; } bool GetResourceText(const std::string& name, std::string* ret) { static std::filesystem::path basePath = GetBasePath(); try { const auto targetFilePath = basePath / "local-files" / "resource" / name; // Log::DebugFmt("GetResourceText: %s", targetFilePath.c_str()); if (exists(targetFilePath)) { auto readStr = readFileToString(targetFilePath.string()); *ret = readStr; return true; } } catch (std::exception& e) { Log::ErrorFmt("read file: %s failed.", name.c_str()); } return false; } std::string GetDumpGenericFileName(DumpStrStat stat = DumpStrStat::DEFAULT) { if (stat == DumpStrStat::SPLITTABLE_ORIG) { if (genericDumpFileIndex == 0) return "generic_orig.json"; return Log::StringFormat("generic_orig_%d.json", genericDumpFileIndex); } else if (stat == DumpStrStat::FMT) { if (genericDumpFileIndex == 0) return "generic.fmt.json"; return Log::StringFormat("generic_%d.fmt.json", genericDumpFileIndex); } else { if (genericDumpFileIndex == 0) return "generic.json"; return Log::StringFormat("generic_%d.json", genericDumpFileIndex); } } bool inDumpGeneric = false; void DumpGenericText(const std::string& origText, DumpStrStat stat = DumpStrStat::DEFAULT) { if (translatedText.contains(origText)) return; std::array>, 4> targets = { genericTextDumpData, genericOrigTextDumpData, genericSplittedDumpData, genericFmtTextDumpData }; auto& appendTarget = targets[static_cast(stat)].get(); if (std::find(appendTarget.begin(), appendTarget.end(), origText) != appendTarget.end()) { return; } if (IsPureStringValue(origText)) return; appendTarget.push_back(origText); static auto dumpBasePath = GetBasePath() / "dump-files"; if (inDumpGeneric) return; inDumpGeneric = true; std::thread([](){ std::this_thread::sleep_for(std::chrono::seconds(5)); DumpVectorDataToJson(dumpBasePath, GetDumpGenericFileName(DumpStrStat::DEFAULT), genericTextDumpData); DumpVectorDataToJson(dumpBasePath, GetDumpGenericFileName(DumpStrStat::SPLITTABLE_ORIG), genericOrigTextDumpData); DumpVectorDataToJson(dumpBasePath, GetDumpGenericFileName(DumpStrStat::SPLITTED), genericSplittedDumpData, splitTextPrefix); DumpVectorDataToJson(dumpBasePath, GetDumpGenericFileName(DumpStrStat::FMT), genericFmtTextDumpData); genericTextDumpData.clear(); genericSplittedDumpData.clear(); genericOrigTextDumpData.clear(); genericFmtTextDumpData.clear(); inDumpGeneric = false; }).detach(); } bool GetGenericText(const std::string& origText, std::string* newStr) { // 完全匹配 if (const auto iter = genericText.find(origText); iter != genericText.end()) { *newStr = iter->second; return true; } // 不翻译翻译过的文本 if (translatedText.contains(origText)) { return false; } // 匹配升级卡名 if (auto plusPos = origText.find_last_not_of('+'); plusPos != std::string::npos) { const auto noPlusText = origText.substr(0, plusPos + 1); if (const auto iter = genericText.find(noPlusText); iter != genericText.end()) { size_t plusCount = origText.length() - (plusPos + 1); *newStr = iter->second + std::string(plusCount, '+'); return true; } } // fmt 文本 auto fmtText = StringParser::ParseItems::parse(origText, false); if (fmtText.isValid) { const auto fmtStr = fmtText.ToFmtString(); if (auto it = genericFmtText.find(fmtStr); it != genericFmtText.end()) { auto newRet = fmtText.MergeText(it->second); if (!newRet.empty()) { *newStr = newRet; return true; } } if (Config::dumpText) { DumpGenericText(fmtStr, DumpStrStat::FMT); } } auto ret = false; // 分割匹配 std::vector unTransResultRet; const auto splitTransStat = GetSplitTagsTranslationFull(origText, newStr, unTransResultRet); switch (splitTransStat) { case SplitTagsTranslationStat::FULL_TRANS: { DumpGenericText(origText, DumpStrStat::SPLITTABLE_ORIG); return true; } break; case SplitTagsTranslationStat::NO_SPLIT_AND_EMPTY: { return false; } break; case SplitTagsTranslationStat::NO_SPLIT: { ret = false; } break; case SplitTagsTranslationStat::NO_TRANS: { ret = false; } break; case SplitTagsTranslationStat::PART_TRANS: { ret = true; } break; } if (!Config::dumpText) { return ret; } if (unTransResultRet.empty() || (splitTransStat == SplitTagsTranslationStat::NO_SPLIT)) { DumpGenericText(origText); } else { for (const auto& i : unTransResultRet) { DumpGenericText(i, DumpStrStat::SPLITTED); } // 若未翻译部分长度为1,且未翻译文本等于原文本,则不 dump 到原文本文件 //if (unTransResultRet.size() != 1 || unTransResultRet[0] != origText) { DumpGenericText(origText, DumpStrStat::SPLITTABLE_ORIG); //} } return ret; } std::string ChangeDumpTextIndex(int changeValue) { if (!Config::dumpText) return ""; genericDumpFileIndex += changeValue; return Log::StringFormat("GenericDumpFile: %s", GetDumpGenericFileName().c_str()); } std::string OnKeyDown(int message, int key) { if (message == WM_KEYDOWN) { switch (key) { case KEY_ADD: { return ChangeDumpTextIndex(1); } break; case KEY_SUB: { return ChangeDumpTextIndex(-1); } break; } } return ""; } }