Feature: Embed language resources, disk files act as optional patches

This commit is contained in:
yuanyuanxiang
2026-05-05 13:22:47 +02:00
parent 773c78ac0f
commit f11fc93ba8
5 changed files with 235 additions and 61 deletions

View File

@@ -50,47 +50,71 @@ public:
char line[4096]; char line[4096];
while (fgets(line, sizeof(line), f)) { while (fgets(line, sizeof(line), f)) {
// 去除行尾换行符 ParseLine(line, currentSection);
size_t len = strlen(line);
while (len > 0 && (line[len - 1] == '\n' || line[len - 1] == '\r'))
line[--len] = '\0';
if (len == 0)
continue;
// 跳过注释
if (line[0] == ';' || line[0] == '#')
continue;
// 检测 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 处理
}
} }
// 不在任何 section 内则跳过 fclose(f);
if (currentSection.empty()) return true;
continue;
// 解析 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;
} }
// 从内存加载 INI 数据,返回是否成功
// 用于加载嵌入的资源数据
bool LoadFromMemory(const char* data, size_t size)
{
Clear();
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);
}
// 跳过换行符
p = lineEnd;
while (p < end && (*p == '\n' || *p == '\r'))
p++;
}
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); fclose(f);
@@ -138,6 +162,52 @@ public:
private: private:
TSections m_sections; 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 \\ \" 转为对应的控制字符 // 反转义:将字面量 \n \r \t \\ \" 转为对应的控制字符
static std::string Unescape(const std::string& s) static std::string Unescape(const std::string& s)
{ {

View File

@@ -526,14 +526,13 @@ BOOL CMy2015RemoteApp::InitInstance()
SetChineseThreadLocale(); SetChineseThreadLocale();
// 加载语言包(必须在显示任何文本之前) // 加载语言包(必须在显示任何文本之前)
// 内嵌资源支持 en_US 和 zh_TW无需外部文件
auto lang = THIS_CFG.GetStr("settings", "Language", "en_US"); auto lang = THIS_CFG.GetStr("settings", "Language", "en_US");
auto langDir = THIS_CFG.GetStr("settings", "LangDir", "./lang"); auto langDir = THIS_CFG.GetStr("settings", "LangDir", "./lang");
langDir = langDir.empty() ? "./lang" : langDir; langDir = langDir.empty() ? "./lang" : langDir;
if (PathFileExists(langDir.c_str())) { g_Lang.Init(langDir.c_str()); // 初始化目录(用于磁盘补丁文件)
g_Lang.Init(langDir.c_str()); g_Lang.Load(lang.c_str()); // 加载语言(优先内嵌资源,再覆盖磁盘文件)
g_Lang.Load(lang.c_str()); Mprintf("语言: %s, 目录: %s\n", lang.c_str(), langDir.c_str());
Mprintf("语言包目录已经指定[%s], 语言数量: %d\n", langDir.c_str(), g_Lang.GetLanguageCount());
}
// 创建并显示启动画面 // 创建并显示启动画面
CSplashDlg* pSplash = new CSplashDlg(); CSplashDlg* pSplash = new CSplashDlg();

Binary file not shown.

View File

@@ -1,11 +1,13 @@
#pragma once #pragma once
#include <map> #include <map>
#include <set>
#include <string> #include <string>
#include <vector> #include <vector>
#include <locale.h> #include <locale.h>
#include <afxwin.h> #include <afxwin.h>
#include "common/IniParser.h" #include "common/IniParser.h"
#include "resource.h" // 用于内嵌语言资源 ID
// 设置线程区域为简体中文 // 设置线程区域为简体中文
// 这样 MBCS 程序在非中文系统上创建对话框时,也能正确解码 RC 资源中的 GBK 中文 // 这样 MBCS 程序在非中文系统上创建对话框时,也能正确解码 RC 资源中的 GBK 中文
@@ -60,12 +62,18 @@ public:
CreateDirectory(m_langDir, NULL); CreateDirectory(m_langDir, NULL);
} }
// 获取可用的语言列表 // 获取可用的语言列表(包括内嵌语言)
std::vector<CString> GetAvailableLanguages() std::vector<CString> GetAvailableLanguages()
{ {
std::vector<CString> langs; std::vector<CString> langs;
CString searchPath = m_langDir + _T("\\*.ini"); std::set<CString> langSet; // 用于去重
// 1. 添加内嵌语言(始终可用)
langSet.insert(_T("en_US"));
langSet.insert(_T("zh_TW"));
// 2. 扫描磁盘上的语言文件
CString searchPath = m_langDir + _T("\\*.ini");
WIN32_FIND_DATA fd; WIN32_FIND_DATA fd;
HANDLE hFind = FindFirstFile(searchPath, &fd); HANDLE hFind = FindFirstFile(searchPath, &fd);
if (hFind != INVALID_HANDLE_VALUE) { if (hFind != INVALID_HANDLE_VALUE) {
@@ -73,30 +81,43 @@ public:
CString filename(fd.cFileName); CString filename(fd.cFileName);
int dotPos = filename.ReverseFind(_T('.')); int dotPos = filename.ReverseFind(_T('.'));
if (dotPos > 0) { if (dotPos > 0) {
langs.push_back(filename.Left(dotPos)); langSet.insert(filename.Left(dotPos));
} }
} while (FindNextFile(hFind, &fd)); } while (FindNextFile(hFind, &fd));
FindClose(hFind); FindClose(hFind);
} }
// 转为 vector 返回
for (const auto& lang : langSet) {
langs.push_back(lang);
}
return langs; return langs;
} }
// 检查语言文件编码是否为 ANSI // 检查语言文件编码是否为 ANSI
// 返回 false 表示文件不存在或编码不是 ANSI检测 BOM 和 UTF-8 无 BOM // 返回 false 表示编码不是 ANSI检测 BOM 和 UTF-8 无 BOM
// 内嵌语言en_US, zh_TW直接返回 true
bool CheckEncoding(const CString& langCode) bool CheckEncoding(const CString& langCode)
{ {
// 中文模式无需检查
if (langCode == _T("zh_CN") || langCode.IsEmpty()) { if (langCode == _T("zh_CN") || langCode.IsEmpty()) {
TRACE("[LangEnc] zh_CN or empty, skip check\n"); TRACE("[LangEnc] zh_CN or empty, skip check\n");
return true; 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"); CString langFile = m_langDir + _T("\\") + langCode + _T(".ini");
TRACE("[LangEnc] Checking: %s\n", (LPCSTR)langFile); TRACE("[LangEnc] Checking: %s\n", (LPCSTR)langFile);
FILE* f = nullptr; FILE* f = nullptr;
if (fopen_s(&f, (LPCSTR)langFile, "rb") != 0 || !f) { if (fopen_s(&f, (LPCSTR)langFile, "rb") != 0 || !f) {
TRACE("[LangEnc] fopen failed\n"); TRACE("[LangEnc] fopen failed\n");
return false; return false; // 非内嵌语言必须有磁盘文件
} }
// 读取文件内容(最多检测前 4KB 即可判断) // 读取文件内容(最多检测前 4KB 即可判断)
@@ -164,26 +185,103 @@ public:
} }
// 加载语言文件 // 加载语言文件
// 优先从内嵌资源加载,然后用磁盘文件覆盖(如果存在)
bool Load(const CString& langCode) bool Load(const CString& langCode)
{ {
m_strings.clear(); m_strings.clear();
m_currentLang = langCode; m_currentLang = langCode;
// 如果是中文,不需要加载翻译 // 中文模式:检查是否有补丁文件
if (langCode == _T("zh_CN") || langCode.IsEmpty()) { 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; return true;
} }
CString langFile = m_langDir + _T("\\") + langCode + _T(".ini"); // 1. 先从内嵌资源加载(英语和繁体中文)
bool hasBuiltin = LoadFromResource(langCode);
// 检查文件是否存在 // 2. 再从磁盘文件加载(覆盖内嵌翻译)
if (GetFileAttributes(langFile) == INVALID_FILE_ATTRIBUTES) { 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; 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; CIniParser ini;
if (!ini.LoadFile((LPCSTR)langFile)) { if (!ini.LoadFromMemory(data, size)) {
TRACE("[Lang] Failed to parse resource: %d\n", resID);
return false; return false;
} }
@@ -194,6 +292,8 @@ public:
} }
} }
TRACE("[Lang] Loaded builtin resource: %s (%d strings)\n",
(LPCSTR)langCode, (int)m_strings.size());
return true; return true;
} }
@@ -625,24 +725,24 @@ protected:
AppendData(&dlgTemplate, sizeof(DLGTEMPLATE)); AppendData(&dlgTemplate, sizeof(DLGTEMPLATE));
AppendWord(0); // 菜单 AppendWord(0); // 菜单
AppendWord(0); // 窗口类 AppendWord(0); // 窗口类
AppendString(_T("选择语言 / Select Language")); AppendString(_T("Select Language"));
AlignToDword(); AlignToDword();
// 静态文本 // 静态文本
AddControl(0x0082, 15, 15, 40, 12, (WORD)-1, AddControl(0x0082, 15, 15, 50, 12, (WORD)-1,
SS_LEFT | WS_CHILD | WS_VISIBLE, _T("语言:")); SS_LEFT | WS_CHILD | WS_VISIBLE, _T("Language:"));
// ComboBox // 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("")); CBS_DROPDOWNLIST | WS_CHILD | WS_VISIBLE | WS_TABSTOP | WS_VSCROLL, _T(""));
// 确定按钮 // OK 按钮
AddControl(0x0080, 45, 50, 50, 14, IDOK, 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, 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(); return (LPCDLGTEMPLATE)m_templateBuffer.data();
} }
@@ -703,8 +803,8 @@ protected:
m_comboLang.SubclassDlgItem(1001, this); 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_langCodes.push_back(_T("zh_CN"));
m_comboLang.SetItemData(idx, 0); m_comboLang.SetItemData(idx, 0);

View File

@@ -968,6 +968,11 @@
#define IDC_STATIC_TRIGGER_TYPE 2544 #define IDC_STATIC_TRIGGER_TYPE 2544
#define IDC_STATIC_TRIGGER_ACTION 2545 #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 // Next default values for new objects
// //
#ifdef APSTUDIO_INVOKED #ifdef APSTUDIO_INVOKED