From f11fc93ba8c2edd8d7dc79baf0fd092938856952 Mon Sep 17 00:00:00 2001 From: yuanyuanxiang <962914132@qq.com> Date: Tue, 5 May 2026 13:22:47 +0200 Subject: [PATCH] Feature: Embed language resources, disk files act as optional patches --- common/IniParser.h | 140 ++++++++++++++++++++++-------- server/2015Remote/2015Remote.cpp | 9 +- server/2015Remote/2015Remote.rc | Bin 156868 -> 157168 bytes server/2015Remote/LangManager.h | 142 ++++++++++++++++++++++++++----- server/2015Remote/resource.h | 5 ++ 5 files changed, 235 insertions(+), 61 deletions(-) diff --git a/common/IniParser.h b/common/IniParser.h index ddd848b..a3f3eb1 100644 --- a/common/IniParser.h +++ b/common/IniParser.h @@ -50,47 +50,71 @@ public: char line[4096]; while (fgets(line, sizeof(line), f)) { - // 去除行尾换行符 - size_t len = strlen(line); - while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) - line[--len] = '\0'; + ParseLine(line, currentSection); + } - if (len == 0) - continue; + fclose(f); + return true; + } - // 跳过注释 - if (line[0] == ';' || line[0] == '#') - continue; + // 从内存加载 INI 数据,返回是否成功 + // 用于加载嵌入的资源数据 + bool LoadFromMemory(const char* data, size_t size) + { + Clear(); - // 检测 section 头: [SectionName] - // 真正的 section 头:']' 后面没有 '='(否则是 key=value) - if (line[0] == '[') { - char* end = strchr(line, ']'); - if (end) { - char* eqAfter = strchr(end + 1, '='); - if (!eqAfter) { - // 纯 section 头,如 [Strings] - *end = '\0'; - currentSection = line + 1; - continue; - } - // ']' 后有 '=',如 [使用FRP]=[Using FRP],当作 key=value 处理 - } + if (!data || size == 0) + return false; + + std::string currentSection; + const char* p = data; + const char* end = data + size; + + while (p < end) { + // 找到行尾 + const char* lineEnd = p; + while (lineEnd < end && *lineEnd != '\n' && *lineEnd != '\r') + lineEnd++; + + // 复制行内容 + size_t lineLen = lineEnd - p; + if (lineLen > 0 && lineLen < 4096) { + char line[4096]; + memcpy(line, p, lineLen); + line[lineLen] = '\0'; + ParseLine(line, currentSection); } - // 不在任何 section 内则跳过 - if (currentSection.empty()) - continue; + // 跳过换行符 + p = lineEnd; + while (p < end && (*p == '\n' || *p == '\r')) + p++; + } - // 解析 key=value(只按第一个 '=' 分割,不 trim) - // key 和 value 均做反转义(\n \r \t \\ \") - char* eq = strchr(line, '='); - if (eq && eq != line) { - *eq = '\0'; - std::string key = Unescape(std::string(line)); - std::string value = Unescape(std::string(eq + 1)); - m_sections[currentSection][key] = value; - } + return true; + } + + // 追加加载(不清除现有数据,用于覆盖) + bool LoadFileAppend(const char* filePath) + { + if (!filePath || !filePath[0]) + return false; + + FILE* f = nullptr; +#ifdef _MSC_VER + if (fopen_s(&f, filePath, "r") != 0 || !f) + return false; +#else + f = fopen(filePath, "r"); + if (!f) + return false; +#endif + + std::string currentSection; + char line[4096]; + + while (fgets(line, sizeof(line), f)) { + ParseLine(line, currentSection); } fclose(f); @@ -138,6 +162,52 @@ public: private: TSections m_sections; + // 解析单行 INI 内容 + void ParseLine(char* line, std::string& currentSection) + { + // 去除行尾换行符 + size_t len = strlen(line); + while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r')) + line[--len] = '\0'; + + if (len == 0) + return; + + // 跳过注释 + if (line[0] == ';' || line[0] == '#') + return; + + // 检测 section 头: [SectionName] + // 真正的 section 头:']' 后面没有 '='(否则是 key=value) + if (line[0] == '[') { + char* end = strchr(line, ']'); + if (end) { + char* eqAfter = strchr(end + 1, '='); + if (!eqAfter) { + // 纯 section 头,如 [Strings] + *end = '\0'; + currentSection = line + 1; + return; + } + // ']' 后有 '=',如 [使用FRP]=[Using FRP],当作 key=value 处理 + } + } + + // 不在任何 section 内则跳过 + if (currentSection.empty()) + return; + + // 解析 key=value(只按第一个 '=' 分割,不 trim) + // key 和 value 均做反转义(\n \r \t \\ \") + char* eq = strchr(line, '='); + if (eq && eq != line) { + *eq = '\0'; + std::string key = Unescape(std::string(line)); + std::string value = Unescape(std::string(eq + 1)); + m_sections[currentSection][key] = value; + } + } + // 反转义:将字面量 \n \r \t \\ \" 转为对应的控制字符 static std::string Unescape(const std::string& s) { diff --git a/server/2015Remote/2015Remote.cpp b/server/2015Remote/2015Remote.cpp index a0bc424..5642aa7 100644 --- a/server/2015Remote/2015Remote.cpp +++ b/server/2015Remote/2015Remote.cpp @@ -526,14 +526,13 @@ BOOL CMy2015RemoteApp::InitInstance() SetChineseThreadLocale(); // 加载语言包(必须在显示任何文本之前) + // 内嵌资源支持 en_US 和 zh_TW,无需外部文件 auto lang = THIS_CFG.GetStr("settings", "Language", "en_US"); auto langDir = THIS_CFG.GetStr("settings", "LangDir", "./lang"); langDir = langDir.empty() ? "./lang" : langDir; - if (PathFileExists(langDir.c_str())) { - g_Lang.Init(langDir.c_str()); - g_Lang.Load(lang.c_str()); - Mprintf("语言包目录已经指定[%s], 语言数量: %d\n", langDir.c_str(), g_Lang.GetLanguageCount()); - } + g_Lang.Init(langDir.c_str()); // 初始化目录(用于磁盘补丁文件) + g_Lang.Load(lang.c_str()); // 加载语言(优先内嵌资源,再覆盖磁盘文件) + Mprintf("语言: %s, 目录: %s\n", lang.c_str(), langDir.c_str()); // 创建并显示启动画面 CSplashDlg* pSplash = new CSplashDlg(); diff --git a/server/2015Remote/2015Remote.rc b/server/2015Remote/2015Remote.rc index 4bed78b785b18a543f99f2767e2e7dd463ca7929..80f3cc9b1934b762021a8baa7947df974cb3f06d 100644 GIT binary patch delta 201 zcmX?dlJmo9&W0AoElk_ooVggd81#WqfuS|fC-!YOLzheMgglP_ADiBG_k=js1^esz zwWo6a=VI_=aA62yh-dI&aAfdfa0jwnfpk1W==6u0OybiM+?b54a~KjC@)*(?Vi;0^ zG*FHqm_d&r6UfeFPy*V7X+{)-2T)xIL-^#6qM~3!WD$l`F=Rj_5r#}xv}W45J&W0AoElk_owm))b;*$daUd9L@ diff --git a/server/2015Remote/LangManager.h b/server/2015Remote/LangManager.h index 9844596..cc7037a 100644 --- a/server/2015Remote/LangManager.h +++ b/server/2015Remote/LangManager.h @@ -1,11 +1,13 @@ #pragma once #include +#include #include #include #include #include #include "common/IniParser.h" +#include "resource.h" // 用于内嵌语言资源 ID // 设置线程区域为简体中文 // 这样 MBCS 程序在非中文系统上创建对话框时,也能正确解码 RC 资源中的 GBK 中文 @@ -60,12 +62,18 @@ public: CreateDirectory(m_langDir, NULL); } - // 获取可用的语言列表 + // 获取可用的语言列表(包括内嵌语言) std::vector GetAvailableLanguages() { std::vector langs; - CString searchPath = m_langDir + _T("\\*.ini"); + std::set langSet; // 用于去重 + // 1. 添加内嵌语言(始终可用) + langSet.insert(_T("en_US")); + langSet.insert(_T("zh_TW")); + + // 2. 扫描磁盘上的语言文件 + CString searchPath = m_langDir + _T("\\*.ini"); WIN32_FIND_DATA fd; HANDLE hFind = FindFirstFile(searchPath, &fd); if (hFind != INVALID_HANDLE_VALUE) { @@ -73,30 +81,43 @@ public: CString filename(fd.cFileName); int dotPos = filename.ReverseFind(_T('.')); if (dotPos > 0) { - langs.push_back(filename.Left(dotPos)); + langSet.insert(filename.Left(dotPos)); } } while (FindNextFile(hFind, &fd)); FindClose(hFind); } + + // 转为 vector 返回 + for (const auto& lang : langSet) { + langs.push_back(lang); + } return langs; } // 检查语言文件编码是否为 ANSI - // 返回 false 表示文件不存在或编码不是 ANSI(检测 BOM 和 UTF-8 无 BOM) + // 返回 false 表示编码不是 ANSI(检测 BOM 和 UTF-8 无 BOM) + // 内嵌语言(en_US, zh_TW)直接返回 true bool CheckEncoding(const CString& langCode) { + // 中文模式无需检查 if (langCode == _T("zh_CN") || langCode.IsEmpty()) { TRACE("[LangEnc] zh_CN or empty, skip check\n"); return true; } + // 内嵌语言无需检查(已确保编码正确) + if (langCode == _T("en_US") || langCode == _T("zh_TW")) { + TRACE("[LangEnc] builtin language, skip check\n"); + return true; + } + CString langFile = m_langDir + _T("\\") + langCode + _T(".ini"); TRACE("[LangEnc] Checking: %s\n", (LPCSTR)langFile); FILE* f = nullptr; if (fopen_s(&f, (LPCSTR)langFile, "rb") != 0 || !f) { TRACE("[LangEnc] fopen failed\n"); - return false; + return false; // 非内嵌语言必须有磁盘文件 } // 读取文件内容(最多检测前 4KB 即可判断) @@ -164,26 +185,103 @@ public: } // 加载语言文件 + // 优先从内嵌资源加载,然后用磁盘文件覆盖(如果存在) bool Load(const CString& langCode) { m_strings.clear(); m_currentLang = langCode; - // 如果是中文,不需要加载翻译 + // 中文模式:检查是否有补丁文件 if (langCode == _T("zh_CN") || langCode.IsEmpty()) { + // 尝试加载中文补丁文件(可选) + CString patchFile = m_langDir + _T("\\zh_CN.ini"); + if (GetFileAttributes(patchFile) != INVALID_FILE_ATTRIBUTES) { + CIniParser ini; + if (ini.LoadFile((LPCSTR)patchFile)) { + const CIniParser::TKeyVal* pSection = ini.GetSection("Strings"); + if (pSection) { + for (const auto& kv : *pSection) { + m_strings[CString(kv.first.c_str())] = CString(kv.second.c_str()); + } + } + TRACE("[Lang] Loaded zh_CN patch: %d strings\n", (int)m_strings.size()); + } + } return true; } - CString langFile = m_langDir + _T("\\") + langCode + _T(".ini"); + // 1. 先从内嵌资源加载(英语和繁体中文) + bool hasBuiltin = LoadFromResource(langCode); - // 检查文件是否存在 - if (GetFileAttributes(langFile) == INVALID_FILE_ATTRIBUTES) { + // 2. 再从磁盘文件加载(覆盖内嵌翻译) + CString langFile = m_langDir + _T("\\") + langCode + _T(".ini"); + if (GetFileAttributes(langFile) != INVALID_FILE_ATTRIBUTES) { + CIniParser ini; + // 如果有内嵌资源,使用追加模式覆盖;否则使用普通加载 + if (hasBuiltin) { + // 追加模式:磁盘文件中的翻译会覆盖内嵌翻译 + if (ini.LoadFile((LPCSTR)langFile)) { + const CIniParser::TKeyVal* pSection = ini.GetSection("Strings"); + if (pSection) { + for (const auto& kv : *pSection) { + m_strings[CString(kv.first.c_str())] = CString(kv.second.c_str()); + } + } + TRACE("[Lang] Loaded disk file (override): %s\n", (LPCSTR)langFile); + } + } else { + // 无内嵌资源,直接从磁盘加载 + if (ini.LoadFile((LPCSTR)langFile)) { + const CIniParser::TKeyVal* pSection = ini.GetSection("Strings"); + if (pSection) { + for (const auto& kv : *pSection) { + m_strings[CString(kv.first.c_str())] = CString(kv.second.c_str()); + } + } + TRACE("[Lang] Loaded disk file: %s\n", (LPCSTR)langFile); + return true; + } + } + } + + return hasBuiltin || !m_strings.empty(); + } + + // 从内嵌资源加载语言数据 + bool LoadFromResource(const CString& langCode) + { + UINT resID = 0; + if (langCode == _T("en_US")) { + resID = IDR_LANG_EN_US; + } else if (langCode == _T("zh_TW")) { + resID = IDR_LANG_ZH_TW; + } else { + return false; // 无内嵌资源 + } + + HRSRC hRes = FindResource(NULL, MAKEINTRESOURCE(resID), RT_RCDATA); + if (!hRes) { + TRACE("[Lang] Resource not found: %d\n", resID); return false; } - // 使用 CIniParser 解析,无文件大小限制,且不 trim key + HGLOBAL hData = LoadResource(NULL, hRes); + if (!hData) { + TRACE("[Lang] Failed to load resource: %d\n", resID); + return false; + } + + const char* data = (const char*)LockResource(hData); + DWORD size = SizeofResource(NULL, hRes); + if (!data || size == 0) { + TRACE("[Lang] Empty resource: %d\n", resID); + return false; + } + + // 使用 CIniParser 从内存解析 CIniParser ini; - if (!ini.LoadFile((LPCSTR)langFile)) { + if (!ini.LoadFromMemory(data, size)) { + TRACE("[Lang] Failed to parse resource: %d\n", resID); return false; } @@ -194,6 +292,8 @@ public: } } + TRACE("[Lang] Loaded builtin resource: %s (%d strings)\n", + (LPCSTR)langCode, (int)m_strings.size()); return true; } @@ -625,24 +725,24 @@ protected: AppendData(&dlgTemplate, sizeof(DLGTEMPLATE)); AppendWord(0); // 菜单 AppendWord(0); // 窗口类 - AppendString(_T("选择语言 / Select Language")); + AppendString(_T("Select Language")); AlignToDword(); // 静态文本 - AddControl(0x0082, 15, 15, 40, 12, (WORD)-1, - SS_LEFT | WS_CHILD | WS_VISIBLE, _T("语言:")); + AddControl(0x0082, 15, 15, 50, 12, (WORD)-1, + SS_LEFT | WS_CHILD | WS_VISIBLE, _T("Language:")); // ComboBox - AddControl(0x0085, 55, 13, 130, 150, 1001, + AddControl(0x0085, 65, 13, 120, 150, 1001, CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL, _T("")); - // 确定按钮 + // OK 按钮 AddControl(0x0080, 45, 50, 50, 14, IDOK, - BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("确定")); + BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("OK")); - // 取消按钮 + // Cancel 按钮 AddControl(0x0080, 105, 50, 50, 14, IDCANCEL, - BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("取消")); + BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, _T("Cancel")); return (LPCDLGTEMPLATE)m_templateBuffer.data(); } @@ -703,8 +803,8 @@ protected: m_comboLang.SubclassDlgItem(1001, this); - // 添加简体中文 - int idx = m_comboLang.AddString(_T("简体中文")); + // 添加简体中文(显示为英语避免乱码) + int idx = m_comboLang.AddString(GetLanguageDisplayName(_T("zh_CN"))); m_langCodes.push_back(_T("zh_CN")); m_comboLang.SetItemData(idx, 0); diff --git a/server/2015Remote/resource.h b/server/2015Remote/resource.h index ba8079f..8df9e55 100644 --- a/server/2015Remote/resource.h +++ b/server/2015Remote/resource.h @@ -968,6 +968,11 @@ #define IDC_STATIC_TRIGGER_TYPE 2544 #define IDC_STATIC_TRIGGER_ACTION 2545 +// 内嵌语言资源 (RCDATA) +// 注意:避免与 IDB_BITMAP_TRIGGER(372) 和 IDB_BITMAP_WEBDESKTOP(373) 冲突 +#define IDR_LANG_EN_US 380 +#define IDR_LANG_ZH_TW 381 + // Next default values for new objects // #ifdef APSTUDIO_INVOKED