| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564 |
- 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
- {
- /// <summary>
- /// 多语言管理器(全球化基础设施,支持 中文/英文/越南文/印度文)。
- /// 设计要点:
- /// 1) 字典的 key 直接使用「中文源文」,因此无需为每个控件手工起名;
- /// 2) T(key) 查询当前语言→默认语言(zh)→原文(带回退),缺失时显示原文,界面永不空白;
- /// 3) Localize(Form/Control) 递归遍历控件树(含菜单、页签、网格列头、DevExpress Caption),
- /// 并记录每个控件首次的中文文本,使运行时切换语言可正确来回还原。
- /// </summary>
- 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<string, Dictionary<string, string>> _dict =
- new Dictionary<string, Dictionary<string, string>>();
- // 动态错误模板列表(中文正则 -> 各语言模板)
- private static readonly List<MessageTemplate> _templates =
- new List<MessageTemplate>();
- // code -> 显示名(如 中文 / English)
- private static readonly Dictionary<string, string> _langNames =
- new Dictionary<string, string>();
- private static string _currentCulture = "zh";
- private static string _defaultCulture = "zh";
- /// <summary>支持的语言:code -> 显示名。顺序与 Lang.xml 中一致。</summary>
- public static Dictionary<string, string> SupportedLanguages { get; private set; }
- /// <summary>语言切换后触发,供已打开的窗体重新本地化自身。</summary>
- public static event EventHandler LanguageChanged;
- // 记录控件/菜单项首次出现的中文文本,用于语言来回切换时还原源文
- private static readonly Dictionary<object, string> _originals =
- new Dictionary<object, string>();
- private static readonly Dictionary<object, string> _originalsCap =
- new Dictionary<object, string>();
- private static readonly HashSet<Form> _subscribed = new HashSet<Form>();
- static LanguageManager()
- {
- LoadResources();
- LoadTemplates();
- SupportedLanguages = new Dictionary<string, string>(_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<string, string>(StringComparer.Ordinal);
- }
- // 新格式:每个 <Item key="中文" en=".." vi=".." hi=".."/> 一行四语
- 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
- {
- // 兼容旧格式:<Language code=".."><Item key="..">译文</Item></Language>
- 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<string, string>(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,界面仍可运行
- }
- }
- /// <summary>
- /// 加载动态错误模板(I18n/Templates.xml 嵌入资源)。
- /// 模板用于把数据库返回的“中文正则模式”替换为带变量的多语言文本。
- /// </summary>
- 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<string, string>(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
- {
- // 模板加载失败不影响主程序运行
- }
- }
- /// <summary>
- /// 动态模板翻译:把数据库返回的带变量中文提示翻译成当前语言。
- /// 匹配成功后,用正则捕获组填充模板中的 {0}、{1}... 占位符。
- /// </summary>
- 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<string, string> Translations { get; }
- public MessageTemplate(string zhPattern, Dictionary<string, string> translations)
- {
- Regex = new Regex(zhPattern, RegexOptions.Compiled);
- Translations = translations;
- }
- }
- /// <summary>当前语言代码(如 zh / en / vi / hi)。</summary>
- public static string CurrentCulture
- {
- get { return _currentCulture; }
- }
- /// <summary>
- /// 程序启动时调用:读取已保存的语言并应用(不重复持久化)。
- /// 应在显示任何窗体之前调用。
- /// </summary>
- public static void Initialize()
- {
- string saved = LoadSavedCulture();
- if (string.IsNullOrEmpty(saved) || !_dict.ContainsKey(saved))
- saved = _defaultCulture;
- ApplyCulture(saved, false);
- }
- /// <summary>切换语言并持久化到用户配置。</summary>
- public static void SetCulture(string cultureCode)
- {
- SetCulture(cultureCode, true);
- }
- /// <summary>切换语言。persist=true 时写入用户配置文件。</summary>
- 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);
- }
- /// <summary>
- /// 获取当前语言的翻译文本。key 通常为中文源文。
- /// 查找顺序:当前语言 -> 默认语言(zh) -> (key 去前后空格再试) -> fallback -> key。
- /// 缺失时返回原文,保证界面永不空白。
- /// </summary>
- 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;
- }
- /// <summary>
- /// 对一段混合文本进行翻译:扫描其中包含的所有中文 key,并替换为当前语言译文。
- /// 用于处理数据库返回的带变量前缀/后缀的中文提示(如 "SN:xxx 无工单归属记录,必须先归属工单")。
- /// 按 key 长度降序匹配,避免短 key 截断长 key。
- /// 安全回退:替换后如果仍然残留明显中文(说明没完整翻译),则返回原文,避免中英夹杂。
- /// </summary>
- 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;
- }
- /// <summary>统计字符串中中文字符数量。</summary>
- 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;
- }
- /// <summary>判断字符串中是否包含中文字符。</summary>
- 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;
- }
- /// <summary>
- /// 本地化整个窗体:遍历控件树翻译静态文本,并订阅 LanguageChanged
- /// 以便运行时切换语言时自动刷新。可在窗体构造函数 InitializeComponent() 之后调用。
- /// </summary>
- 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);
- }
- /// <summary>
- /// 本地化一个控件及其所有子控件(递归)。常用于用户控件或局部刷新。
- /// </summary>
- 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;
- }
- }
- }
|