Browse Source

优化多语言版本

callm 2 days ago
parent
commit
6b4e1fb4a7

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

@@ -5,3 +5,5 @@
 - 进一步在 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。
+- 提供两个辅助脚本:scripts/extract_db_templates.py(直接从 .sql 存储过程提取固定中文+动态变量的正则模板)、scripts/md_to_templates.py(从 outputs/db_error_keys.md 表格生成模板),方便后续批量维护。
+- 修复 I18n/Templates.xml 中多个模板正则的方括号未转义问题,导致形如 "岗位资源对应的工序不是工单途程[测试Route]中的工序!" 的提示被拆成零散中文 key 替换,出现中英夹杂。同步改进 LanguageManager.TranslateContainedKeys,增加残留中文回退机制,避免翻译不完整时界面混乱。

+ 1 - 1
UAS_MES_STW/FunctionCode/Make/Make_TestCollection.cs

@@ -792,7 +792,7 @@ namespace UAS_MES_NEW.Make
                 {
                     dt.Rows.Add(dr[i].ItemArray);
                 }
-                WaitRejectList.Items.Clear();
+                WaitRejectList.Items.Clear(); 
                 WaitRejectList.BeginUpdate();
                 for (int i = 0; i < dt.Rows.Count; i++)
                 {

+ 18 - 0
UAS_MES_STW/I18n/LanguageManager.cs

@@ -333,6 +333,7 @@ namespace UAS_MES_NEW.I18n
         /// 对一段混合文本进行翻译:扫描其中包含的所有中文 key,并替换为当前语言译文。
         /// 用于处理数据库返回的带变量前缀/后缀的中文提示(如 "SN:xxx 无工单归属记录,必须先归属工单")。
         /// 按 key 长度降序匹配,避免短 key 截断长 key。
+        /// 安全回退:替换后如果仍然残留明显中文(说明没完整翻译),则返回原文,避免中英夹杂。
         /// </summary>
         public static string TranslateContainedKeys(string text)
         {
@@ -356,9 +357,26 @@ namespace UAS_MES_NEW.I18n
                 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)
         {

+ 7 - 7
UAS_MES_STW/I18n/Templates.xml

@@ -2,7 +2,7 @@
 <!-- 数据库动态错误提示模板:中文正则模式 -> 多语言模板({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"/>
+  <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"/>
@@ -17,16 +17,16 @@
   <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} 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="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"/>
@@ -34,10 +34,10 @@
   <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} 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} 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"/>

+ 47 - 0
UAS_MES_STW/outputs/auto_templates.xml

@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- 由 scripts/md_to_templates.py 从 outputs/db_error_keys.md 自动生成 -->
+<!-- 请把 en/vi/id 属性中的占位符替换为真实翻译 -->
+<Templates>
+  <Template zh="岗位资源对应的工序[(.*?)]不存在或者未审核" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="岗位资源(.*?)错误,不存在" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="序列号:(.*?) 已被工单(.*?)绑定" en="{0}{1}" vi="{0}{1}" id="{0}{1}"/>
+  <Template zh="序列号:(.*?)已完工,不允许再进行归属" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="SN:(.*?) 在多个工单SN范围中,无法自动归属工单" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="SN:(.*?)已经完工不允许再归属" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="SN:(.*?) 无工单归属记录,必须先归属工单" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="序列号:(.*?) 归属工单号:(.*?)错误" en="{0}{1}" vi="{0}{1}" id="{0}{1}"/>
+  <Template zh="工单(.*?)已冻结,不允许采集" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="序列号(.*?)达到最大测试次数(.*?)次,不允许测试" en="{0}{1}" vi="{0}{1}" id="{0}{1}"/>
+  <Template zh="序列号:(.*?) 已转SN:(.*?),不允许使用TSN进行采集!" en="{0}{1}" vi="{0}{1}" id="{0}{1}"/>
+  <Template zh="序列号(.*?)维修完成,处于待QC确认状态,不允许采集" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="序列号:(.*?) 处于下地状态,不允许操作,请先取消下地!" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="序列号:(.*?) 已投产到工单[(.*?)]!" en="{0}{1}" vi="{0}{1}" id="{0}{1}"/>
+  <Template zh="岗位资源对应的工序不是工单途程[(.*?)]中的工序!" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="序列号:(.*?) 处于待维修状态,请先维修!" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="序列号:(.*?) 在返工工序:(.*?),当前资源不允许采集" en="{0}{1}" vi="{0}{1}" id="{0}{1}"/>
+  <Template zh="序列号:(.*?) 下一返工工序是:(.*?),当前资源不允许采集" en="{0}{1}" vi="{0}{1}" id="{0}{1}"/>
+  <Template zh="序列号:(.*?) 已完工,当前资源不允许采集" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="序列号:(.*?) 处于待维修状态,请进入维修界面进行操作" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="序列号:(.*?) 下一工序是:(.*?),当前资源不允许采集" en="{0}{1}" vi="{0}{1}" id="{0}{1}"/>
+  <Template zh="序列号:(.*?) 老化时长不足,剩余时长[(.*?)]分钟,不允许进行操作!" en="{0}{1}" vi="{0}{1}" id="{0}{1}"/>
+  <Template zh="工单:数量(.*?)抽检比例[(.*?)],应抽检数(.*?),当前抽检数量(.*?),请抽检足够数量" en="{0}{1}{2}{3}" vi="{0}{1}{2}{3}" id="{0}{1}{2}{3}"/>
+  <Template zh="工单(.*?)上线数超工单数" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="序列号:(.*?)错误,不在产品序列号导入SN中!" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="序列号:(.*?)错误,不在工单导入PSN中!" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="序列号(.*?)不在工单防呆起始结束范围内!" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="序列号(.*?)不在合同防呆起始结束范围内!" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="上料子件序列号:(.*?) 对应的半成品料号必须是:(.*?)" en="{0}{1}" vi="{0}{1}" id="{0}{1}"/>
+  <Template zh="序列号:(.*?) 处于下地状态,不允许归属!" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="序列号:(.*?) 已经有归属的下一工单[(.*?)]!" en="{0}{1}" vi="{0}{1}" id="{0}{1}"/>
+  <Template zh="序列号:(.*?) 前一工段中的合同号与待归属的工单的合同号不一致!" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="序列号:(.*?) 在前一工段中必须是完工状态!" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="序列号(.*?)在前一工段半成品必须在工单[(.*?)]用料表中!" en="{0}{1}" vi="{0}{1}" id="{0}{1}"/>
+  <Template zh="序列号:(.*?) 已投入工作中心(.*?),不允许归属!" en="{0}{1}" vi="{0}{1}" id="{0}{1}"/>
+  <Template zh="批次物料:(.*?),岗位备料不足" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="序列号:(.*?)未归属工单,不是工单第一道工序不能自动归属" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="工单:(.*?)状态不是已下放,不允许采集" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="工单(.*?)存在首件检查任务单超时:(.*?)未审核或者不合格,不允许操作" en="{0}{1}" vi="{0}{1}" id="{0}{1}"/>
+  <Template zh="当前所属上料工序,(.*?)序列号未进行上料" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="该产品是按线别+合同号换批,工单:(.*?)未维护合同号" en="{0}" vi="{0}" id="{0}"/>
+  <Template zh="AFTERSUCCESS批次物料[(.*?)],岗位备料剩余数不足生产[(.*?)]个产品,请及时进行备料采集" en="{0}{1}" vi="{0}{1}" id="{0}{1}"/>
+</Templates>

+ 185 - 0
UAS_MES_STW/scripts/extract_db_templates.py

@@ -0,0 +1,185 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+从 Oracle PL/SQL 存储过程源码中提取 v_o_errmsg 赋值语句,
+生成 I18n/Templates.xml 可用的正则模板条目。
+
+用法:
+    python extract_db_templates.py <input.sql> [<input2.sql> ...] -o output.xml
+
+生成的 XML 形如:
+    <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}"/>
+"""
+import argparse
+import re
+import os
+import sys
+from xml.sax.saxutils import escape
+
+
+def tokenize_assignment(expr: str):
+    """
+    把形如 'xxx'||var||'yyy'||var||'zzz' 的表达式拆成:
+    [('txt', 'xxx'), ('var', 'var'), ('txt', 'yyy'), ...]
+    """
+    tokens = []
+    i = 0
+    expr = expr.strip()
+    while i < len(expr):
+        # 跳过空白和 ||
+        if expr[i] in " \t\r\n":
+            i += 1
+            continue
+        if expr.startswith("||", i):
+            i += 2
+            continue
+
+        # 字符串字面量
+        if expr[i] == "'":
+            j = i + 1
+            literal = []
+            while j < len(expr):
+                if expr[j] == "'" and j + 1 < len(expr) and expr[j + 1] == "'":
+                    literal.append("'")
+                    j += 2
+                elif expr[j] == "'":
+                    j += 1
+                    break
+                else:
+                    literal.append(expr[j])
+                    j += 1
+            tokens.append(("txt", "".join(literal)))
+            i = j
+            continue
+
+        # 变量 / 函数调用 / 表达式
+        # 取到下一个 || 或字符串字面量或分号为止
+        j = i
+        while j < len(expr):
+            if expr[j] in " \t\r\n":
+                pass
+            elif expr.startswith("||", j):
+                break
+            elif expr[j] == "'":
+                break
+            elif expr[j] == ";":
+                break
+            j += 1
+        var_expr = expr[i:j].strip()
+        if var_expr:
+            tokens.append(("var", var_expr))
+        i = j
+
+    return tokens
+
+
+def build_template(tokens):
+    """
+    根据 token 序列生成中文正则模式和英文/越文/印尼文占位符模板。
+    """
+    fixed_parts = []
+    var_count = 0
+    has_variable = False
+
+    for kind, value in tokens:
+        if kind == "txt":
+            fixed_parts.append(re.escape(value))
+        else:
+            has_variable = True
+            fixed_parts.append(r"(.*?)")
+            var_count += 1
+
+    if not has_variable:
+        return None, None  # 纯静态提示,不需要模板
+
+    zh_pattern = "".join(fixed_parts)
+    placeholders = "{" + "} {".join(str(i) for i in range(var_count)) + "}"
+
+    # 默认英文模板:保留固定中文占位,后续需人工翻译
+    fixed_texts = [v for k, v in tokens if k == "txt"]
+    en_parts = []
+    var_idx = 0
+    for kind, value in tokens:
+        if kind == "txt":
+            # 这里先用原中文占位,实际使用时应替换为英文
+            en_parts.append(value)
+        else:
+            en_parts.append("{" + str(var_idx) + "}")
+            var_idx += 1
+    en_template = "".join(en_parts)
+
+    return zh_pattern, en_template
+
+
+def extract_assignments(sql_text: str):
+    """提取所有 v_o_errmsg := ... ; 语句。"""
+    # 去掉 PL/SQL 行注释
+    sql_text = re.sub(r"--.*?\n", "\n", sql_text)
+    # 去掉 /* */ 块注释
+    sql_text = re.sub(r"/\*.*?\*/", "", sql_text, flags=re.DOTALL)
+
+    # 匹配 v_o_errmsg:=...;(允许跨行)
+    pattern = re.compile(
+        r"v_o_errmsg\s*:=\s*(.*?);",
+        re.IGNORECASE | re.DOTALL,
+    )
+    return [m.group(1).strip() for m in pattern.finditer(sql_text)]
+
+
+def escape_attr(value: str) -> str:
+    """XML 属性转义。"""
+    return escape(value, {'"': "&quot;"})
+
+
+def main():
+    parser = argparse.ArgumentParser(description="从 PL/SQL 提取动态错误模板")
+    parser.add_argument("sql_files", nargs="+", help="输入的 .sql 文件路径")
+    parser.add_argument("-o", "--output", default="Templates.xml", help="输出 XML 文件")
+    args = parser.parse_args()
+
+    all_templates = []
+    seen = set()
+
+    for path in args.sql_files:
+        if not os.path.exists(path):
+            print(f"[WARN] File not found: {path}", file=sys.stderr)
+            continue
+        with open(path, "r", encoding="utf-8") as f:
+            text = f.read()
+
+        for expr in extract_assignments(text):
+            # 跳过纯函数调用,如 SQLERRM、dbms_utility.format_error_backtrace
+            if re.match(r"^[A-Za-z0-9_\(\)\.\s\|\|]+$", expr) and "'" not in expr:
+                continue
+            tokens = tokenize_assignment(expr)
+            zh_pattern, en_template = build_template(tokens)
+            if zh_pattern and zh_pattern not in seen:
+                seen.add(zh_pattern)
+                all_templates.append((zh_pattern, en_template))
+
+    lines = [
+        '<?xml version="1.0" encoding="utf-8"?>',
+        '<!-- 由 scripts/extract_db_templates.py 从存储过程自动生成 -->',
+        '<!-- 请把 en/vi/id 属性中的中文占位文本替换为真实翻译 -->',
+        '<Templates>',
+    ]
+    for zh, en in all_templates:
+        lines.append(
+            f'  <Template zh="{escape_attr(zh)}"'
+            f' en="{escape_attr(en)}"'
+            f' vi="{escape_attr(en)}"'
+            f' id="{escape_attr(en)}"/>'
+        )
+    lines.append("</Templates>")
+
+    with open(args.output, "w", encoding="utf-8") as f:
+        f.write("\n".join(lines))
+
+    print(f"Generated {len(all_templates)} templates -> {args.output}")
+
+
+if __name__ == "__main__":
+    main()

+ 97 - 0
UAS_MES_STW/scripts/md_to_templates.py

@@ -0,0 +1,97 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+"""
+读取 outputs/db_error_keys.md 第二章"含变量提示清单"表格,
+生成 I18n/Templates.xml 的正则模板条目。
+
+用法:
+    python scripts/md_to_templates.py
+
+输出默认写入 I18n/Templates.xml(可通过 -o 指定)。
+"""
+import argparse
+import re
+import os
+from xml.sax.saxutils import escape
+
+
+def escape_attr(value: str) -> str:
+    return escape(value, {'"': "&quot;"})
+
+
+def parse_markdown_table(md_path: str):
+    """解析 markdown 表格,返回 (zh_pattern, var_count) 列表。"""
+    with open(md_path, "r", encoding="utf-8") as f:
+        lines = f.readlines()
+
+    templates = []
+    in_table = False
+    header_seen = False
+
+    for line in lines:
+        stripped = line.strip()
+        if stripped.startswith("|") and "原始中文模板" in stripped:
+            in_table = True
+            header_seen = True
+            continue
+        if in_table:
+            if not stripped.startswith("|"):
+                break
+            if re.match(r"\|\s*[-:]+\s*\|", stripped):
+                continue
+            cols = [c.strip() for c in stripped.strip("|").split("|")]
+            if len(cols) >= 2:
+                zh_template = cols[1]
+                # 统计变量个数
+                var_count = len(re.findall(r"\{[0-9]+\}", zh_template))
+                if var_count > 0 and "{0}" in zh_template:
+                    # 把 {x} 替换为 (.*?)
+                    zh_pattern = re.sub(r"\{[0-9]+\}", r"(.*?)", zh_template)
+                    templates.append((zh_pattern, var_count))
+    return templates
+
+
+def build_translation_template(var_count: int):
+    """生成占位符翻译模板(后续需人工替换为真实翻译)。"""
+    return "".join("{" + str(i) + "}" for i in range(var_count))
+
+
+def main():
+    parser = argparse.ArgumentParser(description="从 db_error_keys.md 生成 Templates.xml")
+    parser.add_argument("-i", "--input", default="outputs/db_error_keys.md", help="输入 markdown 文件")
+    parser.add_argument("-o", "--output", default="I18n/Templates.xml", help="输出 XML 文件")
+    args = parser.parse_args()
+
+    if not os.path.exists(args.input):
+        print(f"[ERROR] Input file not found: {args.input}")
+        return
+
+    templates = parse_markdown_table(args.input)
+    if not templates:
+        print("[WARN] No templates found in the markdown table.")
+        return
+
+    lines = [
+        '<?xml version="1.0" encoding="utf-8"?>',
+        '<!-- 由 scripts/md_to_templates.py 从 outputs/db_error_keys.md 自动生成 -->',
+        '<!-- 请把 en/vi/id 属性中的占位符替换为真实翻译 -->',
+        '<Templates>',
+    ]
+    for zh_pattern, var_count in templates:
+        placeholder = build_translation_template(var_count)
+        lines.append(
+            f'  <Template zh="{escape_attr(zh_pattern)}"'
+            f' en="{escape_attr(placeholder)}"'
+            f' vi="{escape_attr(placeholder)}"'
+            f' id="{escape_attr(placeholder)}"/>'
+        )
+    lines.append("</Templates>")
+
+    with open(args.output, "w", encoding="utf-8") as f:
+        f.write("\n".join(lines))
+
+    print(f"Generated {len(templates)} templates -> {args.output}")
+
+
+if __name__ == "__main__":
+    main()