callm 2 дней назад
Родитель
Сommit
b7f2125762

+ 7 - 0
UAS_MES_STW/.workbuddy/memory/2026-10-20.md

@@ -0,0 +1,7 @@
+# 2026-10-20
+
+- 确认 UAS_MES_STW 项目已集成多语言版本(I18n/LanguageManager.cs + I18n/Lang.xml,支持 zh/en/vi/id)。
+- 修改 DataOperate/DataHelper.cs 与 DataOperate/AccessDBHelper.cs 的 CallProcedure,对返回参数数组的最后一个元素(通常是数据库返回的中文提示 v_o_errmsg)调用 LanguageManager.T() 进行多语言翻译。
+- 进一步在 LanguageManager.cs 增加 TranslateContainedKeys 方法,处理数据库返回的带变量前缀的混合中文提示(如 "SN:xxx 无工单归属记录,必须先归属工单");并在 Lang.xml 中补充对应错误提示的翻译条目。
+- 梳理用户提供的三个存储过程(CS_CHECKSTEPATTRIBUTE、CS_CHECKSTEPSNANDMACODE、CS_SETSTEPRESULT)中的中文错误提示,生成 outputs/db_error_keys.md,区分纯中文 key 与含变量提示,并给出后续维护建议。
+- 按方案 A 实现动态变量模板翻译:新增 I18n/Templates.xml 嵌入资源,LanguageManager.cs 加载正则模板并提供 TranslateTemplate 方法;CallProcedure 先用模板匹配,未命中再回退到 TranslateContainedKeys。UAS_MES_STW.csproj 已同步加入 Templates.xml。

+ 13 - 0
UAS_MES_STW/DataOperate/AccessDBHelper.cs

@@ -6,6 +6,7 @@ using System.Linq;
 using System.Text;
 using System.Windows;
 using System.Windows.Forms;
+using UAS_MES_NEW.I18n;
 
 namespace UAS_MES_NEW.DataOperate
 {
@@ -841,6 +842,18 @@ namespace UAS_MES_NEW.DataOperate
             }
             for (int i = 0; i < command.Parameters.Count; i++)
                 param[i] = command.Parameters[i].Value.ToString();
+
+            // 最后一个参数通常为数据库返回的中文提示信息(如 v_o_errmsg),按当前语言翻译。
+            // 先用正则模板匹配带变量的动态提示;未匹配再按包含的中文 key 做片段替换。
+            if (param.Length > 0)
+            {
+                string original = param[param.Length - 1];
+                string result = LanguageManager.TranslateTemplate(original);
+                if (result == original)
+                    result = LanguageManager.TranslateContainedKeys(original);
+                param[param.Length - 1] = result;
+            }
+
             command.Dispose();
         }
 

+ 23 - 0
UAS_MES_STW/DataOperate/DataHelper.cs

@@ -3,6 +3,7 @@ using System;
 using System.Collections.Generic;
 using System.Data;
 using System.Text;
+using UAS_MES_NEW.I18n;
 using UAS_MES_NEW.PublicMethod;
 
 namespace UAS_MES_NEW.DataOperate
@@ -971,6 +972,17 @@ namespace UAS_MES_NEW.DataOperate
             for (int i = 0; i < command.Parameters.Count; i++)
                 param[i] = command.Parameters[i].Value.ToString();
 
+            // 最后一个参数通常为数据库返回的中文提示信息(如 v_o_errmsg),按当前语言翻译。
+            // 先用正则模板匹配带变量的动态提示;未匹配再按包含的中文 key 做片段替换。
+            if (param.Length > 0)
+            {
+                string original = param[param.Length - 1];
+                string result = LanguageManager.TranslateTemplate(original);
+                if (result == original)
+                    result = LanguageManager.TranslateContainedKeys(original);
+                param[param.Length - 1] = result;
+            }
+
         }
 
 
@@ -999,6 +1011,17 @@ namespace UAS_MES_NEW.DataOperate
             for (int i = 0; i < command.Parameters.Count; i++)
                 param[i] = command.Parameters[i].Value.ToString();
 
+            // 最后一个参数通常为数据库返回的中文提示信息(如 v_o_errmsg),按当前语言翻译。
+            // 先用正则模板匹配带变量的动态提示;未匹配再按包含的中文 key 做片段替换。
+            if (param.Length > 0)
+            {
+                string original = param[param.Length - 1];
+                string result = LanguageManager.TranslateTemplate(original);
+                if (result == original)
+                    result = LanguageManager.TranslateContainedKeys(original);
+                param[param.Length - 1] = result;
+            }
+
         }
 
         /// <summary>

+ 1 - 0
UAS_MES_STW/I18n/Lang.xml

@@ -727,5 +727,6 @@
 <Item key="英文名称" en="English Name" vi="Tên tiếng Anh" id="Nama Inggris"/>
 <Item key="越南文名称" en="Vietnamese Name" vi="Tên tiếng Việt" id="Nama Vietnam"/>
 <Item key="印尼文名称" en="Indonesian Name" vi="Tên Indonesia" id="Nama Indonesia"/>
+<Item key="无工单归属记录,必须先归属工单" en="No WO affiliation record; please bind to a WO first." vi="Không có hồ sơ liên kết WO; vui lòng gán vào WO trước." id="Tidak ada catatan kepemilikan WO; harus mengikat WO terlebih dahulu."/>
 </Items>
 </Langs>

+ 145 - 0
UAS_MES_STW/I18n/LanguageManager.cs

@@ -3,7 +3,9 @@ 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;
@@ -24,10 +26,18 @@ namespace UAS_MES_NEW.I18n
         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>();
@@ -51,6 +61,7 @@ namespace UAS_MES_NEW.I18n
         static LanguageManager()
         {
             LoadResources();
+            LoadTemplates();
             SupportedLanguages = new Dictionary<string, string>(_langNames);
             if (!_dict.ContainsKey(_currentCulture))
                 _currentCulture = _defaultCulture;
@@ -143,6 +154,99 @@ namespace UAS_MES_NEW.I18n
             }
         }
 
+        /// <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
         {
@@ -225,6 +329,47 @@ namespace UAS_MES_NEW.I18n
             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);
+            }
+            return result;
+        }
+
+        /// <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() 之后调用。

+ 51 - 0
UAS_MES_STW/I18n/Templates.xml

@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 数据库动态错误提示模板:中文正则模式 -> 多语言模板({0}、{1}... 为变量占位符) -->
+<Templates>
+  <!-- CS_CHECKSTEPATTRIBUTE -->
+  <Template zh="岗位资源对应的工序[(.*?)不存在或者未审核" en="The process [{0}] corresponding to the station resource does not exist or is not audited" vi="Công đoạn [{0}] tương ứng với tài nguyên trạm không tồn tại hoặc chưa được kiểm tra" id="Proses [{0}] yang sesuai dengan sumber daya stasiun tidak ada atau belum diaudit"/>
+
+  <!-- CS_CHECKSTEPSNANDMACODE -->
+  <Template zh="岗位资源(.*?)错误,不存在" en="Station resource {0} error: does not exist" vi="Lỗi tài nguyên trạm {0}: không tồn tại" id="Kesalahan sumber daya stasiun {0}: tidak ada"/>
+  <Template zh="序列号:(.*?) 已被工单(.*?)绑定" en="SN:{0} has already been bound to WO {1}" vi="SN:{0} đã được gán vào WO {1}" id="SN:{0} sudah terikat ke WO {1}"/>
+  <Template zh="序列号:(.*?)已完工,不允许再进行归属" en="SN:{0} has been completed; rebinding is not allowed" vi="SN:{0} đã hoàn thành, không cho phép gán lại" id="SN:{0} sudah selesai; pengikatan ulang tidak diizinkan"/>
+  <Template zh="SN:(.*?) 在多个工单SN范围中,无法自动归属工单" en="SN:{0} exists in multiple WO SN ranges; automatic binding is not possible" vi="SN:{0} tồn tại trong nhiều phạm vi SN của WO; không thể tự động gán" id="SN:{0} ada dalam beberapa rentang SN WO; pengikatan otomatis tidak mungkin"/>
+  <Template zh="SN:(.*?)已经完工不允许再归属" en="SN:{0} has been completed; binding is not allowed" vi="SN:{0} đã hoàn thành, không cho phép gán" id="SN:{0} sudah selesai; pengikatan tidak diizinkan"/>
+  <Template zh="SN:(.*?) 无工单归属记录,必须先归属工单" en="SN:{0} has no WO affiliation record; please bind to a WO first" vi="SN:{0} không có hồ sơ liên kết WO; vui lòng gán vào WO trước" id="SN:{0} tidak ada catatan kepemilikan WO; harus mengikat WO terlebih dahulu"/>
+  <Template zh="序列号:(.*?) 归属工单号:(.*?)错误" en="SN:{0} binding WO No. {1} error" vi="SN:{0} gán số WO {1} bị lỗi" id="SN:{0} mengikat nomor WO {1} eror"/>
+  <Template zh="工单(.*?)已冻结,不允许采集" en="WO {0} has been frozen; collection is not allowed" vi="WO {0} đã bị đóng băng, không cho phép thu thập" id="WO {0} telah dibekukan; pengumpulan tidak diizinkan"/>
+  <Template zh="序列号(.*?)达到最大测试次数(.*?)次,不允许测试" en="SN:{0} has reached the max test count of {1}; testing is not allowed" vi="SN:{0} đã đạt đến số lần kiểm tra tối đa {1}; không cho phép kiểm tra" id="SN:{0} telah mencapai jumlah tes maksimum {1}; pengujian tidak diizinkan"/>
+  <Template zh="序列号:(.*?) 已转SN:(.*?),不允许使用TSN进行采集!" en="SN:{0} has been converted to SN:{1}; TSN collection is not allowed" vi="SN:{0} đã được chuyển đổi sang SN:{1}; không cho phép thu thập TSN" id="SN:{0} telah dikonversi ke SN:{1}; pengumpulan TSN tidak diizinkan"/>
+  <Template zh="序列号(.*?)维修完成,处于待QC确认状态,不允许采集" en="SN:{0} repair completed; awaiting QC confirmation, collection is not allowed" vi="SN:{0} sửa chữa hoàn tất; đang chờ xác nhận QC, không cho phép thu thập" id="SN:{0} perbaikan selesai; menunggu konfirmasi QC, pengumpulan tidak diizinkan"/>
+  <Template zh="序列号:(.*?) 处于下地状态,不允许操作,请先取消下地!" en="SN:{0} is in floor-drop status; please cancel floor-drop first" vi="SN:{0} đang ở trạng thái hạ xuống; vui lòng hủy hạ xuống trước" id="SN:{0} dalam status floor-drop; silakan batalkan floor-drop terlebih dahulu"/>
+  <Template zh="序列号:(.*?) 已投产到工单[(.*?)!" en="SN:{0} has already been put into production for WO {1}" vi="SN:{0} đã được đưa vào sản xuất cho WO {1}" id="SN:{0} sudah diproduksi untuk WO {1}"/>
+  <Template zh="岗位资源对应的工序不是工单途程[(.*?)中的工序!" en="The process corresponding to the station resource is not in the WO route {0}" vi="Công đoạn tương ứng với tài nguyên trạm không nằm trong tuyến đường WO {0}" id="Proses yang sesuai dengan sumber daya stasiun tidak berada dalam rute WO {0}"/>
+  <Template zh="序列号:(.*?) 处于待维修状态,请先维修!" en="SN:{0} is awaiting repair; please repair first" vi="SN:{0} đang chờ sửa chữa; vui lòng sửa chữa trước" id="SN:{0} menunggu perbaikan; silakan perbaiki terlebih dahulu"/>
+  <Template zh="序列号:(.*?) 在返工工序:(.*?),当前资源不允许采集" en="SN:{0} is in rework process {1}; current resource does not allow collection" vi="SN:{0} đang ở công đoạn tái chế {1}; tài nguyên hiện tại không cho phép thu thập" id="SN:{0} dalam proses rework {1}; sumber daya saat ini tidak mengizinkan pengumpulan"/>
+  <Template zh="序列号:(.*?) 下一返工工序是:(.*?),当前资源不允许采集" en="SN:{0} next rework process is {1}; current resource does not allow collection" vi="SN:{0} công đoạn tái chế tiếp theo là {1}; tài nguyên hiện tại không cho phép thu thập" id="SN:{0} proses rework berikutnya adalah {1}; sumber daya saat ini tidak mengizinkan pengumpulan"/>
+  <Template zh="序列号:(.*?) 已完工,当前资源不允许采集" en="SN:{0} has been completed; current resource does not allow collection" vi="SN:{0} đã hoàn thành; tài nguyên hiện tại không cho phép thu thập" id="SN:{0} sudah selesai; sumber daya saat ini tidak mengizinkan pengumpulan"/>
+  <Template zh="序列号:(.*?) 处于待维修状态,请进入维修界面进行操作" en="SN:{0} is awaiting repair; please enter the repair interface" vi="SN:{0} đang chờ sửa chữa; vui lòng vào giao diện sửa chữa" id="SN:{0} menunggu perbaikan; silakan masuk antarmuka perbaikan"/>
+  <Template zh="序列号:(.*?) 下一工序是:(.*?),当前资源不允许采集" en="SN:{0} next process is {1}; current resource does not allow collection" vi="SN:{0} công đoạn tiếp theo là {1}; tài nguyên hiện tại không cho phép thu thập" id="SN:{0} proses berikutnya adalah {1}; sumber daya saat ini tidak mengizinkan pengumpulan"/>
+  <Template zh="序列号:(.*?) 老化时长不足,剩余时长[(.*?)分钟,不允许进行操作!" en="SN:{0} aging time insufficient; remaining {1} minutes; operation not allowed" vi="SN:{0} thời gian lão hóa không đủ; còn lại {1} phút; không cho phép thao tác" id="SN:{0} waktu aging tidak cukup; tersisa {1} menit; operasi tidak diizinkan"/>
+  <Template zh="工单:数量(.*?)抽检比例[(.*?)],应抽检数(.*?),当前抽检数量(.*?),请抽检足够数量" en="WO:{0} sampling rate [{1}], required {2}, current {3}; please sample enough" vi="WO:{0} tỷ lệ lấy mẫu [{1}], yêu cầu {2}, hiện tại {3}; vui lòng lấy mẫu đủ" id="WO:{0} tingkat sampling [{1}], diperlukan {2}, saat ini {3}; silakan ambil sampel yang cukup"/>
+  <Template zh="工单(.*?)上线数超工单数" en="WO {0} online quantity exceeds WO quantity" vi="Số lượng trực tuyến WO {0} vượt quá số lượng WO" id="Jumlah online WO {0} melebihi jumlah WO"/>
+  <Template zh="序列号:(.*?)错误,不在产品序列号导入SN中!" en="SN:{0} error: not in imported product SN list" vi="SN:{0} lỗi: không trong danh sách SN sản phẩm đã nhập" id="SN:{0} eror: tidak dalam daftar SN produk yang diimpor"/>
+  <Template zh="序列号:(.*?)错误,不在工单导入PSN中!" en="SN:{0} error: not in imported WO PSN list" vi="SN:{0} lỗi: không trong danh sách PSN của WO đã nhập" id="SN:{0} eror: tidak dalam daftar PSN WO yang diimpor"/>
+  <Template zh="序列号(.*?)不在工单防呆起始结束范围内!" en="SN:{0} is not within WO poka-yoke start/end range" vi="SN:{0} không nằm trong phạm vi bắt đầu/kết thúc poka-yoke của WO" id="SN:{0} tidak dalam rentang awal/akhir poka-yoke WO"/>
+  <Template zh="序列号(.*?)不在合同防呆起始结束范围内!" en="SN:{0} is not within contract poka-yoke start/end range" vi="SN:{0} không nằm trong phạm vi bắt đầu/kết thúc poka-yoke của hợp đồng" id="SN:{0} tidak dalam rentang awal/akhir poka-yoke kontrak"/>
+  <Template zh="上料子件序列号:(.*?) 对应的半成品料号必须是:(.*?)" en="Feeding child SN:{0} corresponding semi-finished part No. must be {1}" vi="SN con cấp liệu {0}: mã bán thành phẩm tương ứng phải là {1}" id="SN anak pemberian bahan {0}: nomor bagian semi-jadi yang sesuai harus {1}"/>
+  <Template zh="序列号:(.*?) 处于下地状态,不允许归属!" en="SN:{0} is in floor-drop status; binding is not allowed" vi="SN:{0} đang ở trạng thái hạ xuống; không cho phép gán" id="SN:{0} dalam status floor-drop; pengikatan tidak diizinkan"/>
+  <Template zh="序列号:(.*?) 已经有归属的下一工单[(.*?)!" en="SN:{0} already has a next bound WO {1}" vi="SN:{0} đã có WO tiếp theo được gán {1}" id="SN:{0} sudah memiliki WO berikutnya yang terikat {1}"/>
+  <Template zh="序列号:(.*?) 前一工段中的合同号与待归属的工单的合同号不一致!" en="SN:{0} contract No. in previous section is inconsistent with the WO to be bound" vi="Số hợp đồng của SN:{0} ở công đoạn trước không nhất quán với WO sẽ được gán" id="SN:{0} nomor kontrak di bagian sebelumnya tidak konsisten dengan WO yang akan diikat"/>
+  <Template zh="序列号:(.*?) 在前一工段中必须是完工状态!" en="SN:{0} must be in completed status in previous section" vi="SN:{0} phải ở trạng thái hoàn thành ở công đoạn trước" id="SN:{0} harus dalam status selesai di bagian sebelumnya"/>
+  <Template zh="序列号(.*?)在前一工段半成品必须在工单[(.*?)用料表中!" en="SN:{0} semi-finished product in previous section must be in WO {1} material list" vi="SN:{0} bán thành phẩm ở công đoạn trước phải nằm trong danh sách vật liệu WO {1}" id="SN:{0} produk semi-jadi di bagian sebelumnya harus dalam daftar material WO {1}"/>
+  <Template zh="序列号:(.*?) 已投入工作中心(.*?),不允许归属!" en="SN:{0} has already been put into work center {1}; binding is not allowed" vi="SN:{0} đã được đưa vào trung tâm sản xuất {1}; không cho phép gán" id="SN:{0} sudah dimasukkan ke work center {1}; pengikatan tidak diizinkan"/>
+  <Template zh="批次物料:(.*?),岗位备料不足" en="Batch material {0} station stock insufficient" vi="Vật liệu lô {0} tồn kho trạm không đủ" id="Material batch {0} stok stasiun tidak cukup"/>
+  <Template zh="序列号:(.*?)未归属工单,不是工单第一道工序不能自动归属" en="SN:{0} not bound to WO; auto-binding is only allowed for the first process" vi="SN:{0} chưa được gán vào WO; chỉ cho phép tự động gán ở công đoạn đầu tiên" id="SN:{0} belum terikat ke WO; pengikatan otomatis hanya diizinkan untuk proses pertama"/>
+  <Template zh="工单:(.*?)状态不是已下放,不允许采集" en="WO:{0} status is not released; collection is not allowed" vi="Trạng thái WO:{0} không phải đã phát hành; không cho phép thu thập" id="Status WO:{0} bukan released; pengumpulan tidak diizinkan"/>
+
+  <!-- CS_SETSTEPRESULT -->
+  <Template zh="工单(.*?)存在首件检查任务单超时:(.*?)未审核或者不合格,不允许操作" en="WO {0} first-piece inspection task timeout: {1} not audited or unqualified; operation not allowed" vi="WO {0} kiểm tra first-piece quá hạn: {1} chưa được kiểm tra hoặc không đạt; không cho phép thao tác" id="WO {0} tugas inspeksi first-piece lewat waktu: {1} belum diaudit atau tidak lolos; operasi tidak diizinkan"/>
+  <Template zh="当前所属上料工序,(.*?)序列号未进行上料" en="Current feeding process; SN {0} has not been fed" vi="Công đoạn cấp liệu hiện tại; SN {0} chưa được cấp liệu" id="Proses pemberian bahan saat ini; SN {0} belum diberi bahan"/>
+  <Template zh="该产品是按线别+合同号换批,工单:(.*?)未维护合同号" en="This product switches batches by line + contract No.; WO {0} has no contract No. maintained" vi="Sản phẩm này đổi lô theo dây chuyền + số hợp đồng; WO {0} chưa bảo trì số hợp đồng" id="Produk ini mengganti batch berdasarkan jalur + nomor kontrak; WO {0} belum memelihara nomor kontrak"/>
+  <Template zh="AFTERSUCCESS批次物料[(.*?)],岗位备料剩余数不足生产[(.*?)个产品,请及时进行备料采集" en="AFTERSUCCESS batch material {0} station stock remaining is insufficient for {1} products; please feed material in time" vi="Vật liệu lô AFTERSUCCESS {0} số lượng tồn trạm còn lại không đủ cho {1} sản phẩm; vui lòng cấp liệu kịp thời" id="Material batch AFTERSUCCESS {0} sisa stok stasiun tidak cukup untuk {1} produk; silakan beri bahan tepat waktu"/>
+</Templates>

+ 0 - 0
UAS_MES_STW/Properties/licenses.licx


+ 1 - 1
UAS_MES_STW/UAS_MES_STW.csproj

@@ -1495,6 +1495,7 @@
     <Compile Include="PublicMethod\ReadDataFromSerialPort.cs" />
     <Compile Include="I18n\LanguageManager.cs" />
     <EmbeddedResource Include="I18n\Lang.xml" />
+    <EmbeddedResource Include="I18n\Templates.xml" />
     <EmbeddedResource Include="CustomControl\AccordionMenu\AccordionMenu.resx">
       <DependentUpon>AccordionMenu.cs</DependentUpon>
     </EmbeddedResource>
@@ -2071,7 +2072,6 @@
     <EmbeddedResource Include="Main.resx">
       <DependentUpon>Main.cs</DependentUpon>
     </EmbeddedResource>
-    <EmbeddedResource Include="Properties\licenses.licx" />
     <EmbeddedResource Include="Properties\Resources.resx">
       <Generator>ResXFileCodeGenerator</Generator>
       <SubType>Designer</SubType>

+ 117 - 0
UAS_MES_STW/outputs/db_error_keys.md

@@ -0,0 +1,117 @@
+# 数据库存储过程中文错误提示梳理
+
+> 来源存储过程:CS_CHECKSTEPATTRIBUTE、CS_CHECKSTEPSNANDMACODE、CS_SETSTEPRESULT
+> 整理时间:2026-10-20
+
+## 一、可直接加入 Lang.xml 的纯中文提示
+
+以下提示不含变量,可直接作为 key 使用。
+
+### CS_CHECKSTEPATTRIBUTE
+
+```xml
+<Item key="页面caller不能为空" en="Page caller cannot be empty" vi="Caller trang không được để trống" id="Caller halaman tidak boleh kosong"/>
+<Item key="岗位资源不存在或者未审核" en="Station resource does not exist or is not audited" vi="Tài nguyên trạm không tồn tại hoặc chưa được kiểm tra" id="Sumber daya stasiun tidak ada atau belum diaudit"/>
+<Item key="岗位资源错误,必须是上料工序" en="Station resource error: must be a feeding process" vi="Lỗi tài nguyên trạm: phải là công đoạn cấp liệu" id="Kesalahan sumber daya stasiun: harus proses pemberian bahan"/>
+<Item key="岗位资源错误,必须是测试工序" en="Station resource error: must be a test process" vi="Lỗi tài nguyên trạm: phải là công đoạn kiểm tra" id="Kesalahan sumber daya stasiun: harus proses pengujian"/>
+<Item key="岗位资源错误,必须是维修工序" en="Station resource error: must be a repair process" vi="Lỗi tài nguyên trạm: phải là công đoạn sửa chữa" id="Kesalahan sumber daya stasiun: harus proses perbaikan"/>
+<Item key="岗位资源错误,必须是老化工序" en="Station resource error: must be an aging process" vi="Lỗi tài nguyên trạm: phải là công đoạn lão hóa" id="Kesalahan sumber daya stasiun: harus proses aging"/>
+<Item key="岗位资源错误,必须是序列号转换工序" en="Station resource error: must be an SN conversion process" vi="Lỗi tài nguyên trạm: phải là công đoạn chuyển đổi SN" id="Kesalahan sumber daya stasiun: harus proses konversi SN"/>
+<Item key="岗位资源错误,必须是OQC工序" en="Station resource error: must be an OQC process" vi="Lỗi tài nguyên trạm: phải là công đoạn OQC" id="Kesalahan sumber daya stasiun: harus proses OQC"/>
+<Item key="岗位资源错误,必须是包装工序" en="Station resource error: must be a packing process" vi="Lỗi tài nguyên trạm: phải là công đoạn đóng gói" id="Kesalahan sumber daya stasiun: harus proses packing"/>
+```
+
+### CS_CHECKSTEPSNANDMACODE
+
+```xml
+<Item key="序列号不能为空" en="SN cannot be empty" vi="Số serial không được để trống" id="SN tidak boleh kosong"/>
+<Item key="岗位资源不存在或者未审核" en="Station resource does not exist or is not audited" vi="Tài nguyên trạm không tồn tại hoặc chưa được kiểm tra" id="Sumber daya stasiun tidak ada atau belum diaudit"/>
+<Item key="岗位资源属性错误,无归属工作中心" en="Station resource attribute error: no work center assigned" vi="Lỗi thuộc tính tài nguyên trạm: không có trung tâm sản xuất được gán" id="Kesalahan atribut sumber daya stasiun: tidak ada work center yang ditetapkan"/>
+<Item key="机台无本产品的上料记录,请切换本产品后再采集" en="No feeding record for this product on the machine; please switch to this product before collecting" vi="Không có bản ghi cấp liệu cho sản phẩm này trên máy; vui lòng chuyển sang sản phẩm này trước khi thu thập" id="Tidak ada record pemberian bahan untuk produk ini di mesin; silakan beralih ke produk ini sebelum koleksi"/>
+```
+
+### CS_SETSTEPRESULT
+
+```xml
+<Item key="SN或工单号不正确" en="SN or WO No. is incorrect" vi="SN hoặc số lệnh sản xuất không chính xác" id="SN atau nomor WO tidak benar"/>
+<Item key="SN采集资源所属工序不正确" en="SN collection resource process is incorrect" vi="Công đoạn tài nguyên thu thập SN không chính xác" id="Proses sumber daya pengumpulan SN tidak benar"/>
+<Item key="当前工序为序列号烧录转换工序,SN对应的工单导入了MAC清单,SN必须烧录MAC信息" en="Current process is SN burn-in conversion; WO has imported MAC list, SN must burn MAC info" vi="Công đoạn hiện tại là chuyển đổi đốt SN; lệnh sản xuất đã nhập danh sách MAC, SN phải đốt thông tin MAC" id="Proses saat ini adalah konversi burn SN; WO telah mengimpor daftar MAC, SN harus membakar info MAC"/>
+<Item key="当前工序为序列号烧录转换工序,SN对应的工单导入了IMEI清单,SN必须烧录IMEI信息" en="Current process is SN burn-in conversion; WO has imported IMEI list, SN must burn IMEI info" vi="Công đoạn hiện tại là chuyển đổi đốt SN; lệnh sản xuất đã nhập danh sách IMEI, SN phải đốt thông tin IMEI" id="Proses saat ini adalah konversi burn SN; WO telah mengimpor daftar IMEI, SN harus membakar info IMEI"/>
+<Item key="当前工序为序列号烧录转换工序,SN对应的产品维护了关联采集信息,SN必须采集相关信息" en="Current process is SN burn-in conversion; product has related collection info, SN must collect related info" vi="Công đoạn hiện tại là chuyển đổi đốt SN; sản phẩm có thông tin thu thập liên quan, SN phải thu thập thông tin liên quan" id="Proses saat ini adalah konversi burn SN; produk memiliki info koleksi terkait, SN harus mengumpulkan info terkait"/>
+```
+
+## 二、含变量提示(无法直接作为固定 key)
+
+这些提示在数据库里把变量值拼接进去了,例如 `序列号:xxx 已被工单yyy绑定`。由于 xxx/yyy 每次都变,无法写死 key,建议按以下方式处理。
+
+### 推荐方案 A:数据库返回模板 + 客户端填充
+
+把存储过程改为返回类似 `ERR_SN_ALREADY_BOUND|SN:xxx|WO:yyy` 的结构,客户端按模板翻译:
+
+```xml
+<Item key="ERR_SN_ALREADY_BOUND" en="SN:{0} has already been bound to WO {1}" vi="SN:{0} đã được gán vào WO {1}" id="SN:{0} sudah terikat ke WO {1}"/>
+```
+
+### 推荐方案 B:拆成固定中文片段(当前代码可部分支持)
+
+在 `Lang.xml` 里只维护固定中文片段,借助 `TranslateContainedKeys` 做子串替换。但翻译质量取决于片段拆分是否合理,例如:
+
+```xml
+<Item key="已被工单" en="has already been bound to WO" vi="đã được gán vào WO" id="sudah terikat ke WO"/>
+<Item key="绑定" en="bound" vi="đã gán" id="terikat"/>
+```
+
+这种方式语句会不自然,**仅建议作为临时过渡**。
+
+### 含变量提示清单
+
+| 存储过程 | 原始中文模板(变量用 {x} 表示) | 说明 |
+|---|---|---|
+| CS_CHECKSTEPATTRIBUTE | 岗位资源对应的工序[{0}]不存在或者未审核 | {0}=工序号 |
+| CS_CHECKSTEPSNANDMACODE | 岗位资源{0}错误,不存在 | {0}=sourcecode |
+| CS_CHECKSTEPSNANDMACODE | 序列号:{0} 已被工单{1}绑定 | {0}=SN, {1}=工单号 |
+| CS_CHECKSTEPSNANDMACODE | 序列号:{0}已完工,不允许再进行归属 | {0}=SN |
+| CS_CHECKSTEPSNANDMACODE | SN:{0} 在多个工单SN范围中,无法自动归属工单 | {0}=SN |
+| CS_CHECKSTEPSNANDMACODE | SN:{0}已经完工不允许再归属 | {0}=SN |
+| CS_CHECKSTEPSNANDMACODE | SN:{0} 无工单归属记录,必须先归属工单 | {0}=SN |
+| CS_CHECKSTEPSNANDMACODE | 序列号:{0} 归属工单号:{1}错误 | {0}=SN, {1}=工单号 |
+| CS_CHECKSTEPSNANDMACODE | 工单{0}已冻结,不允许采集 | {0}=工单号 |
+| CS_CHECKSTEPSNANDMACODE | 序列号{0}达到最大测试次数{1}次,不允许测试 | {0}=SN, {1}=次数 |
+| CS_CHECKSTEPSNANDMACODE | 序列号:{0} 已转SN:{1},不允许使用TSN进行采集! | {0}=原SN, {1}=新SN |
+| CS_CHECKSTEPSNANDMACODE | 序列号{0}维修完成,处于待QC确认状态,不允许采集 | {0}=SN |
+| CS_CHECKSTEPSNANDMACODE | 序列号:{0} 处于下地状态,不允许操作,请先取消下地! | {0}=SN |
+| CS_CHECKSTEPSNANDMACODE | 序列号:{0} 已投产到工单[{1}]! | {0}=SN, {1}=工单号 |
+| CS_CHECKSTEPSNANDMACODE | 岗位资源对应的工序不是工单途程[{0}]中的工序! | {0}=途程号 |
+| CS_CHECKSTEPSNANDMACODE | 序列号:{0} 处于待维修状态,请先维修! | {0}=SN |
+| CS_CHECKSTEPSNANDMACODE | 序列号:{0} 在返工工序:{1},当前资源不允许采集 | {0}=SN, {1}=工序号 |
+| CS_CHECKSTEPSNANDMACODE | 序列号:{0} 下一返工工序是:{1},当前资源不允许采集 | {0}=SN, {1}=工序号 |
+| CS_CHECKSTEPSNANDMACODE | 序列号:{0} 已完工,当前资源不允许采集 | {0}=SN |
+| CS_CHECKSTEPSNANDMACODE | 序列号:{0} 处于待维修状态,请进入维修界面进行操作 | {0}=SN |
+| CS_CHECKSTEPSNANDMACODE | 序列号:{0} 下一工序是:{1},当前资源不允许采集 | {0}=SN, {1}=工序号 |
+| CS_CHECKSTEPSNANDMACODE | 序列号:{0} 老化时长不足,剩余时长[{1}]分钟,不允许进行操作! | {0}=SN, {1}=分钟数 |
+| CS_CHECKSTEPSNANDMACODE | 工单:数量{0}抽检比例[{1}],应抽检数{2},当前抽检数量{3},请抽检足够数量 | {0}=工单号, {1}=比例, {2}=应抽检数, {3}=当前抽检数 |
+| CS_CHECKSTEPSNANDMACODE | 工单{0}上线数超工单数 | {0}=工单号 |
+| CS_CHECKSTEPSNANDMACODE | 序列号:{0}错误,不在产品序列号导入SN中! | {0}=SN |
+| CS_CHECKSTEPSNANDMACODE | 序列号:{0}错误,不在工单导入PSN中! | {0}=SN |
+| CS_CHECKSTEPSNANDMACODE | 序列号{0}不在工单防呆起始结束范围内! | {0}=SN |
+| CS_CHECKSTEPSNANDMACODE | 序列号{0}不在合同防呆起始结束范围内! | {0}=SN |
+| CS_CHECKSTEPSNANDMACODE | 上料子件序列号:{0} 对应的半成品料号必须是:{1} | {0}=SN, {1}=料号 |
+| CS_CHECKSTEPSNANDMACODE | 序列号:{0} 处于下地状态,不允许归属! | {0}=SN |
+| CS_CHECKSTEPSNANDMACODE | 序列号:{0} 已经有归属的下一工单[{1}]! | {0}=SN, {1}=工单号 |
+| CS_CHECKSTEPSNANDMACODE | 序列号:{0} 前一工段中的合同号与待归属的工单的合同号不一致! | {0}=SN |
+| CS_CHECKSTEPSNANDMACODE | 序列号:{0} 在前一工段中必须是完工状态! | {0}=SN |
+| CS_CHECKSTEPSNANDMACODE | 序列号{0}在前一工段半成品必须在工单[{1}]用料表中! | {0}=SN, {1}=工单号 |
+| CS_CHECKSTEPSNANDMACODE | 序列号:{0} 已投入工作中心{1},不允许归属! | {0}=SN, {1}=工作中心 |
+| CS_CHECKSTEPSNANDMACODE | 批次物料:{0},岗位备料不足 | {0}=物料号 |
+| CS_CHECKSTEPSNANDMACODE | 序列号:{0}未归属工单,不是工单第一道工序不能自动归属 | {0}=SN |
+| CS_CHECKSTEPSNANDMACODE | 工单:{0}状态不是已下放,不允许采集 | {0}=工单号 |
+| CS_SETSTEPRESULT | 工单{0}存在首件检查任务单超时:{1}未审核或者不合格,不允许操作 | {0}=工单号, {1}=结果 |
+| CS_SETSTEPRESULT | 当前所属上料工序,{0}序列号未进行上料 | {0}=SN |
+| CS_SETSTEPRESULT | 该产品是按线别+合同号换批,工单:{0}未维护合同号 | {0}=工单号 |
+| CS_SETSTEPRESULT | AFTERSUCCESS批次物料[{0}],岗位备料剩余数不足生产[{1}]个产品,请及时进行备料采集 | {0}=物料号, {1}=数量 |
+
+## 三、后续建议
+
+1. **先把第一章的纯中文条目复制到 `I18n/Lang.xml`**,立即生效。
+2. **含变量提示建议走方案 A**:改存储过程返回错误码 + 参数,客户端用模板翻译。这样最干净,后续新增错误码也只需要加一条字典。
+3. 如果暂时不想改存储过程,可以先用方案 B 顶一顶,但翻译质量会打折。