using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Globalization; using System.IO; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using System.Threading; using System.Windows.Forms; using System.Xml; namespace UAS_MES_NEW.I18n { /// /// 多语言管理器(全球化基础设施,支持 中文/英文/越南文/印度文)。 /// 设计要点: /// 1) 字典的 key 直接使用「中文源文」,因此无需为每个控件手工起名; /// 2) T(key) 查询当前语言→默认语言(zh)→原文(带回退),缺失时显示原文,界面永不空白; /// 3) Localize(Form/Control) 递归遍历控件树(含菜单、页签、网格列头、DevExpress Caption), /// 并记录每个控件首次的中文文本,使运行时切换语言可正确来回还原。 /// public static class LanguageManager { // 嵌入资源逻辑名:根命名空间.I18n.Lang.xml private static readonly string ResourceName = typeof(LanguageManager).Namespace + ".Lang.xml"; // 嵌入资源逻辑名:根命名空间.I18n.Templates.xml private static readonly string TemplatesResourceName = typeof(LanguageManager).Namespace + ".Templates.xml"; // code -> (中文源文 -> 译文) private static readonly Dictionary> _dict = new Dictionary>(); // 动态错误模板列表(中文正则 -> 各语言模板) private static readonly List _templates = new List(); // code -> 显示名(如 中文 / English) private static readonly Dictionary _langNames = new Dictionary(); private static string _currentCulture = "zh"; private static string _defaultCulture = "zh"; /// 支持的语言:code -> 显示名。顺序与 Lang.xml 中一致。 public static Dictionary SupportedLanguages { get; private set; } /// 语言切换后触发,供已打开的窗体重新本地化自身。 public static event EventHandler LanguageChanged; // 记录控件/菜单项首次出现的中文文本,用于语言来回切换时还原源文 private static readonly Dictionary _originals = new Dictionary(); private static readonly Dictionary _originalsCap = new Dictionary(); private static readonly HashSet
_subscribed = new HashSet(); static LanguageManager() { LoadResources(); LoadTemplates(); SupportedLanguages = new Dictionary(_langNames); if (!_dict.ContainsKey(_currentCulture)) _currentCulture = _defaultCulture; } private static void LoadResources() { try { Assembly asm = Assembly.GetExecutingAssembly(); Stream s = asm.GetManifestResourceStream(ResourceName); if (s == null) { // 兜底:按名称模糊匹配,避免资源命名差异导致加载失败 foreach (string rn in asm.GetManifestResourceNames()) { if (rn.EndsWith("Lang.xml", StringComparison.OrdinalIgnoreCase)) { s = asm.GetManifestResourceStream(rn); break; } } } if (s == null) return; XmlDocument doc = new XmlDocument(); doc.Load(s); XmlElement root = doc.DocumentElement; if (root == null) return; foreach (XmlNode langNode in root.SelectNodes("Language")) { XmlElement le = langNode as XmlElement; if (le == null) continue; string code = le.GetAttribute("code"); if (string.IsNullOrEmpty(code)) continue; _langNames[code] = le.GetAttribute("name"); if (!_dict.ContainsKey(code)) _dict[code] = new Dictionary(StringComparer.Ordinal); } // 新格式:每个 一行四语 XmlNodeList items = root.SelectNodes("Items/Item"); if (items != null && items.Count > 0) { foreach (XmlNode itemNode in items) { XmlElement ie = itemNode as XmlElement; if (ie == null) continue; string key = ie.GetAttribute("key"); if (string.IsNullOrEmpty(key)) continue; foreach (string code in new[] { "en", "vi", "id", "zh" }) { string val = ie.GetAttribute(code); if (!string.IsNullOrEmpty(val) && _dict.ContainsKey(code)) _dict[code][key] = val; } } } else { // 兼容旧格式:译文 foreach (XmlNode langNode in root.SelectNodes("Language")) { XmlElement le = langNode as XmlElement; if (le == null) continue; string code = le.GetAttribute("code"); if (string.IsNullOrEmpty(code)) continue; var map = new Dictionary(StringComparer.Ordinal); foreach (XmlNode itemNode in le.SelectNodes("Item")) { XmlElement ie = itemNode as XmlElement; if (ie == null) continue; string key = ie.GetAttribute("key"); if (!string.IsNullOrEmpty(key)) map[key] = ie.InnerText; } _dict[code] = map; } } XmlAttribute def = root.Attributes["Default"]; if (def != null && _dict.ContainsKey(def.Value)) _defaultCulture = def.Value; } catch { // 加载失败时 T() 会回退为 key,界面仍可运行 } } /// /// 加载动态错误模板(I18n/Templates.xml 嵌入资源)。 /// 模板用于把数据库返回的“中文正则模式”替换为带变量的多语言文本。 /// private static void LoadTemplates() { try { Assembly asm = Assembly.GetExecutingAssembly(); Stream s = asm.GetManifestResourceStream(TemplatesResourceName); if (s == null) { foreach (string rn in asm.GetManifestResourceNames()) { if (rn.EndsWith("Templates.xml", StringComparison.OrdinalIgnoreCase)) { s = asm.GetManifestResourceStream(rn); break; } } } if (s == null) return; XmlDocument doc = new XmlDocument(); doc.Load(s); XmlElement root = doc.DocumentElement; if (root == null) return; foreach (XmlNode node in root.SelectNodes("Template")) { XmlElement te = node as XmlElement; if (te == null) continue; string zhPattern = te.GetAttribute("zh"); if (string.IsNullOrEmpty(zhPattern)) continue; var translations = new Dictionary(StringComparer.Ordinal); foreach (string code in new[] { "en", "vi", "id", "zh" }) { string val = te.GetAttribute(code); if (!string.IsNullOrEmpty(val)) translations[code] = val; } _templates.Add(new MessageTemplate(zhPattern, translations)); } } catch { // 模板加载失败不影响主程序运行 } } /// /// 动态模板翻译:把数据库返回的带变量中文提示翻译成当前语言。 /// 匹配成功后,用正则捕获组填充模板中的 {0}、{1}... 占位符。 /// public static string TranslateTemplate(string text) { if (string.IsNullOrEmpty(text) || _currentCulture == _defaultCulture) return text; foreach (var tmpl in _templates) { Match m = tmpl.Regex.Match(text); if (!m.Success) continue; if (!tmpl.Translations.TryGetValue(_currentCulture, out string format)) { if (!tmpl.Translations.TryGetValue(_defaultCulture, out format)) continue; } // 用捕获组填充 {0}、{1}... string[] groups = new string[m.Groups.Count - 1]; for (int i = 1; i < m.Groups.Count; i++) groups[i - 1] = m.Groups[i].Value; try { return string.Format(format, groups); } catch { return format; } } return text; } private class MessageTemplate { public Regex Regex { get; } public Dictionary Translations { get; } public MessageTemplate(string zhPattern, Dictionary translations) { Regex = new Regex(zhPattern, RegexOptions.Compiled); Translations = translations; } } /// 当前语言代码(如 zh / en / vi / hi)。 public static string CurrentCulture { get { return _currentCulture; } } /// /// 程序启动时调用:读取已保存的语言并应用(不重复持久化)。 /// 应在显示任何窗体之前调用。 /// public static void Initialize() { string saved = LoadSavedCulture(); if (string.IsNullOrEmpty(saved) || !_dict.ContainsKey(saved)) saved = _defaultCulture; ApplyCulture(saved, false); } /// 切换语言并持久化到用户配置。 public static void SetCulture(string cultureCode) { SetCulture(cultureCode, true); } /// 切换语言。persist=true 时写入用户配置文件。 public static void SetCulture(string cultureCode, bool persist) { if (string.IsNullOrEmpty(cultureCode) || !_dict.ContainsKey(cultureCode)) cultureCode = _defaultCulture; ApplyCulture(cultureCode, persist); } private static void ApplyCulture(string cultureCode, bool persist) { _currentCulture = cultureCode; try { CultureInfo ci = new CultureInfo(cultureCode); Thread.CurrentThread.CurrentUICulture = ci; Thread.CurrentThread.CurrentCulture = ci; } catch { // 无效的 culture 不影响界面文本切换 } if (persist) SaveCulture(cultureCode); LanguageChanged?.Invoke(null, EventArgs.Empty); } /// /// 获取当前语言的翻译文本。key 通常为中文源文。 /// 查找顺序:当前语言 -> 默认语言(zh) -> (key 去前后空格再试) -> fallback -> key。 /// 缺失时返回原文,保证界面永不空白。 /// public static string T(string key, string fallback = null) { if (string.IsNullOrEmpty(key)) return key; if (_dict.TryGetValue(_currentCulture, out var map) && map.TryGetValue(key, out var val) && !string.IsNullOrEmpty(val)) return val; if (_dict.TryGetValue(_defaultCulture, out var dmap) && dmap.TryGetValue(key, out var dval) && !string.IsNullOrEmpty(dval)) return dval; // 去前后空格再试一次(兼容「姓名 : 」这类带尾随空格的标签) string tk = key.Trim(); if (tk != key) { if (_dict.TryGetValue(_currentCulture, out var m2) && m2.TryGetValue(tk, out var v2) && !string.IsNullOrEmpty(v2)) return v2; if (_dict.TryGetValue(_defaultCulture, out var dm2) && dm2.TryGetValue(tk, out var dv2) && !string.IsNullOrEmpty(dv2)) return dv2; } return fallback ?? key; } /// /// 对一段混合文本进行翻译:扫描其中包含的所有中文 key,并替换为当前语言译文。 /// 用于处理数据库返回的带变量前缀/后缀的中文提示(如 "SN:xxx 无工单归属记录,必须先归属工单")。 /// 按 key 长度降序匹配,避免短 key 截断长 key。 /// 安全回退:替换后如果仍然残留明显中文(说明没完整翻译),则返回原文,避免中英夹杂。 /// public static string TranslateContainedKeys(string text) { if (string.IsNullOrEmpty(text)) return text; if (_currentCulture == _defaultCulture) return text; if (!_dict.TryGetValue(_currentCulture, out var map)) return text; // 取出所有非空且翻译不同于原文的中文 key,按长度降序排列, // 这样长句子优先匹配,短词不会先把长句子的组成部分替换掉。 var keys = map .Where(kv => !string.IsNullOrEmpty(kv.Value) && kv.Value != kv.Key && ContainsChinese(kv.Key)) .OrderByDescending(kv => kv.Key.Length) .ToList(); string result = text; foreach (var kv in keys) { if (result.Contains(kv.Key)) result = result.Replace(kv.Key, kv.Value); } // 如果替换后仍残留明显中文,说明片段替换不完整,回退原文避免界面混乱。 if (CountChinese(result) > 2) return text; return result; } /// 统计字符串中中文字符数量。 private static int CountChinese(string text) { if (string.IsNullOrEmpty(text)) return 0; int count = 0; foreach (char c in text) { if (c >= 0x4E00 && c <= 0x9FFF) count++; } return count; } /// 判断字符串中是否包含中文字符。 private static bool ContainsChinese(string text) { if (string.IsNullOrEmpty(text)) return false; foreach (char c in text) { if (c >= 0x4E00 && c <= 0x9FFF) return true; } return false; } /// /// 本地化整个窗体:遍历控件树翻译静态文本,并订阅 LanguageChanged /// 以便运行时切换语言时自动刷新。可在窗体构造函数 InitializeComponent() 之后调用。 /// public static void Localize(Form form) { if (form == null) return; if (!_subscribed.Contains(form)) { _subscribed.Add(form); form.FormClosed += (s, e) => { _subscribed.Remove(form); }; LanguageChanged += (s, e) => { try { Localize((Control)form); } catch { } }; } Localize((Control)form); } /// /// 本地化一个控件及其所有子控件(递归)。常用于用户控件或局部刷新。 /// public static void Localize(Control root) { if (root == null) return; try { TranslateText(root); TranslateCaption(root); // DataGridView 列头 if (root is DataGridView dgv) { foreach (DataGridViewColumn col in dgv.Columns) { try { if (!_originals.ContainsKey(col)) _originals[col] = col.HeaderText ?? ""; string src = _originals[col]; if (!string.IsNullOrEmpty(src)) col.HeaderText = T(src); } catch { } } } // 菜单/工具栏(MenuStrip、ToolStrip、StatusStrip 均是 Control,但其项不是 Control) if (root is ToolStrip ts) { foreach (ToolStripItem item in ts.Items) LocalizeItem(item); } } catch { } foreach (Control child in root.Controls) Localize(child); } private static void LocalizeItem(ToolStripItem item) { if (item == null) return; try { if (!_originals.ContainsKey(item)) _originals[item] = item.Text ?? ""; string src = _originals[item]; if (!string.IsNullOrEmpty(src)) item.Text = T(src); } catch { } try { if (item is ToolStripDropDownItem dd) { foreach (ToolStripItem sub in dd.DropDownItems) LocalizeItem(sub); } } catch { } } private static void TranslateText(Control ctl) { try { // 标记为 NoTranslate 的控件(如语言选择下拉框本身)不参与翻译, // 避免翻译其 Text 后触发 SelectedIndexChanged 造成语言来回切换 if ("NoTranslate".Equals(ctl.Tag as string, StringComparison.Ordinal)) return; if (!_originals.ContainsKey(ctl)) _originals[ctl] = ctl.Text ?? ""; string src = _originals[ctl]; if (!string.IsNullOrEmpty(src)) ctl.Text = T(src); } catch { } } // DevExpress 等控件用 Caption 而非 Text(如 BarItem、部分状态栏项)。 // 仅当 Caption 存在且与 Text 不同(避免重复翻译)时处理。 private static void TranslateCaption(Control ctl) { try { if ("NoTranslate".Equals(ctl.Tag as string, StringComparison.Ordinal)) return; PropertyInfo prop = ctl.GetType().GetProperty("Caption", typeof(string)); if (prop == null || !prop.CanRead || !prop.CanWrite) return; string cap = (string)prop.GetValue(ctl) ?? ""; if (string.IsNullOrEmpty(cap) || cap == ctl.Text) return; if (!_originalsCap.ContainsKey(ctl)) _originalsCap[ctl] = cap; string src = _originalsCap[ctl]; if (!string.IsNullOrEmpty(src)) prop.SetValue(ctl, T(src)); } catch { } } // ---- 便捷对话框:自动用当前语言翻译文本与标题 ---- // 用法:把代码里的 MessageBox.Show("请选择数据库!", "提示", ...) // 改为 LanguageManager.ShowError("请选择数据库!", "提示", ...) // 其中字符串即中文源文(key),与 Localize 机制一致。 public static DialogResult ShowInfo(string textKey, string titleKey = "提示") { return MessageBox.Show(T(textKey), T(titleKey), MessageBoxButtons.OK, MessageBoxIcon.Information); } public static DialogResult ShowWarning(string textKey, string titleKey = "提示") { return MessageBox.Show(T(textKey), T(titleKey), MessageBoxButtons.OK, MessageBoxIcon.Warning); } public static DialogResult ShowError(string textKey, string titleKey = "错误") { return MessageBox.Show(T(textKey), T(titleKey), MessageBoxButtons.OK, MessageBoxIcon.Error); } public static DialogResult ShowQuestion(string textKey, string titleKey = "提示") { return MessageBox.Show(T(textKey), T(titleKey), MessageBoxButtons.YesNo, MessageBoxIcon.Question); } // ---- 持久化:%LocalAppData%\UAS_MES_NEW\lang.cfg ---- private static string ConfigPath { get { try { string dir = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "UAS_MES_NEW"); if (!Directory.Exists(dir)) Directory.CreateDirectory(dir); return Path.Combine(dir, "lang.cfg"); } catch { return "lang.cfg"; } } } private static void SaveCulture(string code) { try { File.WriteAllText(ConfigPath, code); } catch { /* 忽略写入失败 */ } } private static string LoadSavedCulture() { try { if (File.Exists(ConfigPath)) return File.ReadAllText(ConfigPath).Trim(); } catch { /* 忽略读取失败 */ } return null; } } }