dsh-plc-decode 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +11 -2
- package/README.md +42 -4
- package/assets/FStudio/346/226/207/346/234/254/345/272/223/345/257/274/345/205/245/346/240/274/345/274/217.md +45 -0
- package/assets/fstudio-tables.py +77 -0
- package/assets/fstudio_i18n.py +608 -0
- package/assets/hmismart-decode.mjs +452 -0
- package/assets/hmismart-diff.mjs +206 -0
- package/assets/hmismart-report.mjs +192 -0
- package/assets/mcgs-decode.mjs +140 -0
- package/assets/nb-decode.mjs +107 -0
- package/assets/wincc_i18n.py +504 -0
- package/assets/zzuser-fstudio-i18n-workflow.md +165 -0
- package/assets/zzuser-hmi-decode-workflow.md +53 -0
- package/assets/zzuser-nb-textlib-workflow.md +93 -0
- package/assets/zzuser-wincc-text-i18n-workflow.md +96 -0
- package/cordis.patch.yml +3 -1
- package/hmi.js +580 -0
- package/index.js +18 -11
- package/package.json +13 -4
- package/scripts/smoke.mjs +7 -2
|
@@ -0,0 +1,608 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
r"""FStudio / Flexem 触摸屏多语言:扫元件文本 → 出中英对照表 → 出可导入文本库表。
|
|
3
|
+
|
|
4
|
+
四个子命令:
|
|
5
|
+
scan <工程目录或.fsprj> <out.json> 只扫描,出 JSON
|
|
6
|
+
pair <scan.json> <对照表.xlsx> [词典.py] 出「中文|英文|画面|原英文槽」对照表
|
|
7
|
+
make <工程目录或.fsprj> <输出目录> [词典.py] 一步到位:扫描 + 对照表 + 可导入表
|
|
8
|
+
retag <已导出的TextLib表> <输出.xlsx> <词典> [语种键] [最少状态数]
|
|
9
|
+
换语种列:把导出表的第三语种列
|
|
10
|
+
(如韩语)整列换成目标语种(如巴葡),
|
|
11
|
+
其余列原样保留 → 可直接导回文本库
|
|
12
|
+
给「最少状态数」(如 2)时,状态不够的
|
|
13
|
+
条目按首行补足状态行(Guid 留空、Status 递增、
|
|
14
|
+
文字与首行同)
|
|
15
|
+
|
|
16
|
+
可导入表列名与 FStudio 导出的 TextLib 表逐字符一致:
|
|
17
|
+
Guid | Name | Status | 1-Chinese (Simplified, PRC)-2052 | 2-English (United States)-1033
|
|
18
|
+
给了第三语种词典(.py 的 ELEMENT_KO/ELEMENT_PT/LONG_*,或 .json 的 "ko"/"pt")时,末尾再加一列,
|
|
19
|
+
列名按语种键取自 LANG_HEADS:
|
|
20
|
+
3-Korean (Korea)-1042 / 3-Portuguese (Brazil)-1046 / 3-Portuguese (Portugal)-2070 / 3-Russian (Russia)-1049 …
|
|
21
|
+
"""
|
|
22
|
+
import collections
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
import uuid
|
|
28
|
+
import xml.etree.ElementTree as ET
|
|
29
|
+
|
|
30
|
+
NS = "{http://schemas.flexem.com/fs/model/1}"
|
|
31
|
+
CJK = re.compile(r"[\u4e00-\u9fff]")
|
|
32
|
+
HEAD_ZH = "1-Chinese (Simplified, PRC)-2052"
|
|
33
|
+
HEAD_EN = "2-English (United States)-1033"
|
|
34
|
+
HEAD_KO = "3-Korean (Korea)-1042"
|
|
35
|
+
|
|
36
|
+
# 第三语种:词典键 → 导入表列名(列名必须与 FStudio 导出的 TextLib 表逐字符一致)
|
|
37
|
+
LANG_HEADS = {
|
|
38
|
+
"ko": "3-Korean (Korea)-1042",
|
|
39
|
+
"pt": "3-Portuguese (Brazil)-1046",
|
|
40
|
+
"pt-pt": "3-Portuguese (Portugal)-2070",
|
|
41
|
+
"ru": "3-Russian (Russia)-1049",
|
|
42
|
+
"es": "3-Spanish (Spain)-3082",
|
|
43
|
+
"vi": "3-Vietnamese (Vietnam)-1066",
|
|
44
|
+
"th": "3-Thai (Thailand)-1054",
|
|
45
|
+
"ar": "3-Arabic (Saudi Arabia)-1025",
|
|
46
|
+
}
|
|
47
|
+
# 词典里找第三语种的顺序(都给了就按这个优先级取第一个)
|
|
48
|
+
LANG_ORDER = ["pt", "pt-pt", "ko", "ru", "es", "vi", "th", "ar"]
|
|
49
|
+
_LANG_ATTRS = {"pt-pt": "PT_PT", "pt": "PT", "ko": "KO", "ru": "RU", "es": "ES",
|
|
50
|
+
"vi": "VI", "th": "TH", "ar": "AR"}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ────────────────────────── 工程解析 ──────────────────────────
|
|
54
|
+
|
|
55
|
+
def project_dir(p):
|
|
56
|
+
"""传入 .fsprj 或工程目录,返回工程目录。"""
|
|
57
|
+
if p.lower().endswith(".fsprj"):
|
|
58
|
+
return os.path.dirname(os.path.abspath(p))
|
|
59
|
+
return os.path.abspath(p)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def scan_elements(proj):
|
|
63
|
+
"""静态文本元件:每个元件按语种槽取 LabelContent(1 中文 / 2 英文 / 3 俄语)。"""
|
|
64
|
+
frame = os.path.join(project_dir(proj), "HMI", "Frame")
|
|
65
|
+
out = []
|
|
66
|
+
for dirpath, _dirs, files in os.walk(frame):
|
|
67
|
+
for fn in files:
|
|
68
|
+
if not fn.lower().endswith(".wcfg"):
|
|
69
|
+
continue
|
|
70
|
+
try:
|
|
71
|
+
root = ET.parse(os.path.join(dirpath, fn)).getroot()
|
|
72
|
+
except Exception:
|
|
73
|
+
continue
|
|
74
|
+
win = (root.findtext(NS + "Name") or "").strip()
|
|
75
|
+
for g in root.iter(NS + "GraphicsBase"):
|
|
76
|
+
etype = g.get("{http://www.w3.org/2001/XMLSchema-instance}type") or ""
|
|
77
|
+
labs = collect_labels(g)
|
|
78
|
+
if not labs:
|
|
79
|
+
continue
|
|
80
|
+
out.append({"win": win, "file": fn, "type": etype,
|
|
81
|
+
"gid": g.findtext(NS + "UniqueId") or "",
|
|
82
|
+
"tlib": g.findtext(NS + "NormalInfo/" + NS + "TextLibId") or "",
|
|
83
|
+
"labels": labs})
|
|
84
|
+
return out
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def collect_labels(g):
|
|
88
|
+
"""一个元件里所有按语种分槽的文字。
|
|
89
|
+
|
|
90
|
+
三条路都要收(2026-09-22 修两轮:
|
|
91
|
+
① 原先只收静态文本,漏掉按钮/开关的一整套标签;
|
|
92
|
+
② 第二版只从 TagStatus 往下找,漏掉直接挂在 Statuses/Items 下的那种):
|
|
93
|
+
① 静态文本:StaticTextContentInfo 里的 LanguageSN + LabelContent
|
|
94
|
+
② 按钮/开关/指示灯:凡 TagStatusLanguage(不管父层是 TagStatus 还是 Statuses/Items)
|
|
95
|
+
③ 少数元件:TitleSettingLanguage
|
|
96
|
+
返回 [{lang, text, status}],lang 为语种号(1 中文 / 2 英文 / 3 俄语)。
|
|
97
|
+
"""
|
|
98
|
+
labs = []
|
|
99
|
+
for ci in g.iter(NS + "StaticTextContentInfo"):
|
|
100
|
+
sn = ci.findtext(NS + "LanguageSN")
|
|
101
|
+
tx = (ci.findtext(NS + "LabelContent") or "").strip()
|
|
102
|
+
if sn is not None:
|
|
103
|
+
labs.append({"lang": sn, "text": tx, "status": ""})
|
|
104
|
+
# 不依赖父层:凡是 TagStatusLanguage 都收(父层可能是 TagStatus,也可能直接是 Statuses/Items)
|
|
105
|
+
for tl in g.iter(NS + "TagStatusLanguage"):
|
|
106
|
+
lid = tl.findtext(NS + "LanguageId")
|
|
107
|
+
tx = (tl.findtext(NS + "Text") or "").strip()
|
|
108
|
+
if lid is not None:
|
|
109
|
+
labs.append({"lang": lid, "text": tx, "status": ""})
|
|
110
|
+
# 少数元件把标签放在 TitleSettingContentInfos 下
|
|
111
|
+
for tl in g.iter(NS + "TitleSettingLanguage"):
|
|
112
|
+
lid = tl.findtext(NS + "LanguageId")
|
|
113
|
+
tx = (tl.findtext(NS + "Text") or "").strip()
|
|
114
|
+
if lid is not None:
|
|
115
|
+
labs.append({"lang": lid, "text": tx, "status": ""})
|
|
116
|
+
return labs
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def scan_library(proj):
|
|
120
|
+
"""文本库现有条目(Global\\TextLibConfigInfo.cfg)。"""
|
|
121
|
+
cfg = os.path.join(project_dir(proj), "Global", "TextLibConfigInfo.cfg")
|
|
122
|
+
if not os.path.exists(cfg):
|
|
123
|
+
return []
|
|
124
|
+
root = ET.parse(cfg).getroot()
|
|
125
|
+
items = []
|
|
126
|
+
for it in root.findall(NS + "TextItemInfos/" + NS + "TextItemInfo"):
|
|
127
|
+
langs = collections.defaultdict(set)
|
|
128
|
+
for st in it.findall(NS + "TextStatusInfos/" + NS + "TextStatusInfo"):
|
|
129
|
+
for lt in st.findall(NS + "LanguageTextInfos/" + NS + "LanguageTextInfo"):
|
|
130
|
+
sn = lt.findtext(NS + "LanguageSN")
|
|
131
|
+
tx = (lt.findtext(NS + "LanguageText") or "").strip()
|
|
132
|
+
if tx and sn:
|
|
133
|
+
langs[sn].add(tx)
|
|
134
|
+
items.append({"name": it.findtext(NS + "Name"),
|
|
135
|
+
"langs": {k: sorted(v) for k, v in langs.items()}})
|
|
136
|
+
return items
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def texts_of(e, lang):
|
|
140
|
+
"""某个元件里指定语种的全部文字(去重,保序)。"""
|
|
141
|
+
out = []
|
|
142
|
+
for l in e.get("labels", []):
|
|
143
|
+
if str(l.get("lang")) == str(lang):
|
|
144
|
+
t = (l.get("text") or "").strip()
|
|
145
|
+
if t and t not in out:
|
|
146
|
+
out.append(t)
|
|
147
|
+
return out
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def build_pairs(elems):
|
|
151
|
+
"""元件里中文标签 ↔ 英文标签 的对应(原工程自带的译文,作为译法参考)。"""
|
|
152
|
+
pair = collections.defaultdict(collections.Counter)
|
|
153
|
+
for e in elems:
|
|
154
|
+
zs, es = texts_of(e, 1), texts_of(e, 2)
|
|
155
|
+
for zh, en in zip(zs, es):
|
|
156
|
+
if zh and en:
|
|
157
|
+
pair[zh][en] += 1
|
|
158
|
+
return {k: v.most_common(1)[0][0] for k, v in pair.items()}
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
# ────────────────────────── 词典 ──────────────────────────
|
|
162
|
+
|
|
163
|
+
def load_dict(path, lang=None):
|
|
164
|
+
"""词典,两种写法:
|
|
165
|
+
· 旧(单语):{中文: 英文} 的 json,或含 ELEMENT_EN/LONG_EN 的 python
|
|
166
|
+
· 多语:json {"en": {...}, "pt": {...}},或 python 里再加 ELEMENT_PT/LONG_PT
|
|
167
|
+
返回 (英表, lookup, rename, 第三语种表, 第三语种键);rename 是项目级「中文改写」表。
|
|
168
|
+
lang 显式给了就用它(平表也当该语种);没给时按 LANG_ORDER 在词典里找第一个第三语种。"""
|
|
169
|
+
if not path or not os.path.exists(path):
|
|
170
|
+
return {}, None, {}, {}, ""
|
|
171
|
+
if path.endswith(".json"):
|
|
172
|
+
data = json.load(open(path, encoding="utf-8"))
|
|
173
|
+
if isinstance(data, dict) and any(isinstance(v, dict) for v in data.values()):
|
|
174
|
+
en = data.get("en") or {}
|
|
175
|
+
key = lang if lang in data else next((k for k in LANG_ORDER if k in data), "")
|
|
176
|
+
return en, None, {}, (data.get(key) or {}) if key else {}, key
|
|
177
|
+
# 平表:给了 lang 就当该语种,否则当英文表
|
|
178
|
+
if lang:
|
|
179
|
+
return {}, None, {}, data, lang
|
|
180
|
+
return data, None, {}, {}, ""
|
|
181
|
+
import importlib.util
|
|
182
|
+
spec = importlib.util.spec_from_file_location("i18n_dict", path)
|
|
183
|
+
mod = importlib.util.module_from_spec(spec)
|
|
184
|
+
spec.loader.exec_module(mod)
|
|
185
|
+
table = {}
|
|
186
|
+
for nm in ("ELEMENT_EN", "LONG_EN"):
|
|
187
|
+
table.update(getattr(mod, nm, None) or {})
|
|
188
|
+
key = lang if lang else next(
|
|
189
|
+
(k for k in LANG_ORDER if getattr(mod, "ELEMENT_" + _LANG_ATTRS[k], None)), "")
|
|
190
|
+
third = {}
|
|
191
|
+
if key:
|
|
192
|
+
for nm in ("ELEMENT_" + _LANG_ATTRS[key], "LONG_" + _LANG_ATTRS[key]):
|
|
193
|
+
third.update(getattr(mod, nm, None) or {})
|
|
194
|
+
return table, getattr(mod, "lookup", None), (getattr(mod, "RENAME", None) or {}), third, key
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def apply_rename(text, rename):
|
|
198
|
+
"""按项目词典里的 RENAME 把中文旧写法换成规范写法(如 常按→长按)。"""
|
|
199
|
+
if not rename or not text:
|
|
200
|
+
return text
|
|
201
|
+
for old, new in rename.items():
|
|
202
|
+
if old in text:
|
|
203
|
+
text = text.replace(old, new)
|
|
204
|
+
return text
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def translate(text, table, lookup):
|
|
208
|
+
if text in table:
|
|
209
|
+
return table[text]
|
|
210
|
+
key = (text or "").replace("\r\n", "\n")
|
|
211
|
+
if key in table:
|
|
212
|
+
return table[key]
|
|
213
|
+
if lookup:
|
|
214
|
+
return lookup(text) or ""
|
|
215
|
+
return ""
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
# ────────────────────────── 输出 ──────────────────────────
|
|
219
|
+
|
|
220
|
+
# 对照表里第三语种列的中文标题
|
|
221
|
+
LANG_LABELS = {"ko": "韩文", "pt": "葡文(巴)", "pt-pt": "葡文(葡)", "ru": "俄文",
|
|
222
|
+
"es": "西文", "vi": "越文", "th": "泰文", "ar": "阿文"}
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def write_pair_table(rows, out_xlsx, ko=None, lang="ko"):
|
|
226
|
+
"""对照表:序号|中文|英文|(第三语种)|出现次数|所在画面|元件里原来的英文槽|英文来源。
|
|
227
|
+
ko({中文: 第三语种})非空时多一列,缺的标黄;lang 决定列标题。"""
|
|
228
|
+
from openpyxl import Workbook
|
|
229
|
+
from openpyxl.styles import Alignment, Font, PatternFill, Border, Side
|
|
230
|
+
from openpyxl.utils import get_column_letter
|
|
231
|
+
has_ko = bool(ko)
|
|
232
|
+
head = ["序号", "中文", "英文"] + ([LANG_LABELS.get(lang, lang)] if has_ko else []) + \
|
|
233
|
+
["出现次数", "所在画面", "元件里原来的英文槽", "英文来源"]
|
|
234
|
+
ncol = len(head)
|
|
235
|
+
i_en, i_ko, i_slot, i_src = 3, (4 if has_ko else 0), ncol - 2, ncol
|
|
236
|
+
wb = Workbook()
|
|
237
|
+
ws = wb.active
|
|
238
|
+
ws.title = "对照表"
|
|
239
|
+
thin = Side(style="thin", color="BFBFBF")
|
|
240
|
+
border = Border(left=thin, right=thin, top=thin, bottom=thin)
|
|
241
|
+
hf = PatternFill("solid", fgColor="1F4E79")
|
|
242
|
+
hfont = Font(color="FFFFFF", bold=True)
|
|
243
|
+
yellow = PatternFill("solid", fgColor="FFF2CC")
|
|
244
|
+
ws.append(head)
|
|
245
|
+
for c in range(1, ncol + 1):
|
|
246
|
+
cell = ws.cell(row=1, column=c)
|
|
247
|
+
cell.fill, cell.font, cell.border = hf, hfont, border
|
|
248
|
+
cell.alignment = Alignment(horizontal="center", vertical="center")
|
|
249
|
+
wrap = {2, 3, i_slot, i_src} | ({i_ko} if has_ko else set())
|
|
250
|
+
for idx, (zh, en, n, wins, slot, src_note) in enumerate(rows, 1):
|
|
251
|
+
vals = [idx, zh, en] + ([ko.get(zh, "")] if has_ko else []) + [n, wins, slot, src_note]
|
|
252
|
+
ws.append(vals)
|
|
253
|
+
r = ws.max_row
|
|
254
|
+
for c in range(1, ncol + 1):
|
|
255
|
+
ws.cell(row=r, column=c).border = border
|
|
256
|
+
ws.cell(row=r, column=c).alignment = Alignment(vertical="top", wrap_text=(c in wrap))
|
|
257
|
+
if not en:
|
|
258
|
+
ws.cell(row=r, column=i_en).fill = yellow
|
|
259
|
+
ws.cell(row=r, column=i_src).fill = yellow
|
|
260
|
+
if has_ko and not ko.get(zh, ""):
|
|
261
|
+
ws.cell(row=r, column=i_ko).fill = yellow
|
|
262
|
+
widths = [6, 34, 44] + ([36] if has_ko else []) + [10, 34, 34, 18]
|
|
263
|
+
for i, w in enumerate(widths, 1):
|
|
264
|
+
ws.column_dimensions[get_column_letter(i)].width = w
|
|
265
|
+
ws.freeze_panes = "A2"
|
|
266
|
+
ws.auto_filter.ref = "A1:%s%d" % (get_column_letter(ncol), ws.max_row)
|
|
267
|
+
wb.save(out_xlsx)
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def write_import_table(rows, out_xlsx, sheet="TextLib", ko=None, lang="ko"):
|
|
271
|
+
"""rows: (中文, 采用的英文, 次数, 画面, 原英文槽, 是否新补/替换);ko: {中文: 第三语种}
|
|
272
|
+
FStudio 可导入格式:每行一个条目、Guid 唯一、Status=0。ko 非空时带第三语种列(列名按 lang)。"""
|
|
273
|
+
from openpyxl import Workbook
|
|
274
|
+
from openpyxl.styles import Alignment, Font, PatternFill, Border, Side
|
|
275
|
+
from openpyxl.utils import get_column_letter
|
|
276
|
+
has_ko = bool(ko)
|
|
277
|
+
head = ["Guid", "Name", "Status", HEAD_ZH, HEAD_EN] + \
|
|
278
|
+
([LANG_HEADS.get(lang, HEAD_KO)] if has_ko else [])
|
|
279
|
+
ncol = len(head)
|
|
280
|
+
wb = Workbook()
|
|
281
|
+
ws = wb.active
|
|
282
|
+
ws.title = sheet
|
|
283
|
+
thin = Side(style="thin", color="BFBFBF")
|
|
284
|
+
border = Border(left=thin, right=thin, top=thin, bottom=thin)
|
|
285
|
+
hf = PatternFill("solid", fgColor="1F4E79")
|
|
286
|
+
hfont = Font(color="FFFFFF", bold=True)
|
|
287
|
+
yellow = PatternFill("solid", fgColor="FFF2CC")
|
|
288
|
+
ws.append(head)
|
|
289
|
+
for c in range(1, ncol + 1):
|
|
290
|
+
cell = ws.cell(row=1, column=c)
|
|
291
|
+
cell.fill, cell.font, cell.border = hf, hfont, border
|
|
292
|
+
cell.alignment = Alignment(horizontal="center", vertical="center")
|
|
293
|
+
for row in rows:
|
|
294
|
+
zh, en = row[0], row[1]
|
|
295
|
+
name = re.sub(r"\s+", " ", zh.replace("\r\n", " ").replace("\n", " ")).strip()[:50] or "text"
|
|
296
|
+
vals = [str(uuid.uuid4()), name, 0, zh, en] + ([ko.get(zh, "")] if has_ko else [])
|
|
297
|
+
ws.append(vals)
|
|
298
|
+
r = ws.max_row
|
|
299
|
+
for c in range(1, ncol + 1):
|
|
300
|
+
ws.cell(row=r, column=c).border = border
|
|
301
|
+
ws.cell(row=r, column=c).alignment = Alignment(vertical="top", wrap_text=(c in (2, 4, 5, 6)))
|
|
302
|
+
ws.cell(row=r, column=3).number_format = "0"
|
|
303
|
+
if not en:
|
|
304
|
+
ws.cell(row=r, column=5).fill = yellow
|
|
305
|
+
if has_ko and not ko.get(zh, ""):
|
|
306
|
+
ws.cell(row=r, column=6).fill = yellow
|
|
307
|
+
widths = [38, 40, 8, 46, 52] + ([40] if has_ko else [])
|
|
308
|
+
for i, w in enumerate(widths, 1):
|
|
309
|
+
ws.column_dimensions[get_column_letter(i)].width = w
|
|
310
|
+
ws.freeze_panes = "A2"
|
|
311
|
+
wb.save(out_xlsx)
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
# ────────────────────────── 主流程 ──────────────────────────
|
|
315
|
+
|
|
316
|
+
def aggregate(elems, rename=None):
|
|
317
|
+
"""把元件文字聚成 {中文: {n, wins, slot}};slot 记元件里原有的英文(若有)。
|
|
318
|
+
rename:项目级中文改写(旧写法→规范写法),聚合前先套用。"""
|
|
319
|
+
agg = collections.OrderedDict()
|
|
320
|
+
for e in elems:
|
|
321
|
+
zs = [apply_rename(t, rename) for t in texts_of(e, 1) if CJK.search(t)]
|
|
322
|
+
es = texts_of(e, 2)
|
|
323
|
+
if not zs:
|
|
324
|
+
continue
|
|
325
|
+
# 中文与英文在元件里按顺序成组:第 i 个中文对应第 i 个英文
|
|
326
|
+
for i, zh in enumerate(zs):
|
|
327
|
+
a = agg.setdefault(zh, {"n": 0, "wins": [], "slot": ""})
|
|
328
|
+
a["n"] += 1
|
|
329
|
+
if e["win"] not in a["wins"]:
|
|
330
|
+
a["wins"].append(e["win"])
|
|
331
|
+
if not a["slot"] and i < len(es):
|
|
332
|
+
a["slot"] = es[i]
|
|
333
|
+
return agg
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
# 判定为“原件英文有明显失误、必须换成定版”的条目(其余一律沿用原件英文)
|
|
337
|
+
FORCE_FIX = {
|
|
338
|
+
"标记取料位置", "标记放料位置", "标记取料过顶位置", "标记放料过顶位置",
|
|
339
|
+
"标记放料吹气位置", "标记回零位置", "标记待料位置", # 原为 back to/zero,词不达意
|
|
340
|
+
"点进", "点退", "点动", # 原为 point/feed
|
|
341
|
+
"回零", "正向回零", "反向回零", # 原为 back to/zero / point/feed
|
|
342
|
+
"返回", "上一页", "下一页", # 大小写/缩写统一
|
|
343
|
+
"吸料真空", "吸料真空检测", # 原件填的是数字 8
|
|
344
|
+
"拨号", # Dail 拼写错
|
|
345
|
+
"保存绝对值", "非保存绝对值", # 原为 point/feed
|
|
346
|
+
"操作界面", # operation/interface 不是行业说法
|
|
347
|
+
"加热开", "加热关", # heat on/off → Heater on/off
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def merge_rows(agg, table, lookup):
|
|
352
|
+
"""定译口径(2026-09-22 用户最终定:全用定版译文)。
|
|
353
|
+
· 有词典译文 → 一律用定版(覆盖元件里原有的英文);
|
|
354
|
+
· 词典没有 → 退回元件原有英文;再没有 → 空(标黄)。
|
|
355
|
+
返回 (zh, 采用的英文, 次数, 画面, 原英文槽, 来源标记)。"""
|
|
356
|
+
rows = []
|
|
357
|
+
for zh, a in sorted(agg.items(), key=lambda kv: (-kv[1]["n"], kv[0])):
|
|
358
|
+
# 不做任何"同义/错别字合并":元件上怎么写就怎么出条目,
|
|
359
|
+
# 否则元件在文本库里会找不到对应条目(2026-09-22 踩过:档箱/挡箱 两条只出一条)
|
|
360
|
+
slot = (a.get("slot") or "").strip()
|
|
361
|
+
mine = translate(zh, table, lookup)
|
|
362
|
+
# 防呆:译文里若混着汉字,等于没译(如“班组计数 on”),一律作废,让缺译清单暴露出来
|
|
363
|
+
if mine and CJK.search(mine):
|
|
364
|
+
mine = ""
|
|
365
|
+
# 兜底:原件英文槽若本身混着汉字,同样不算译文
|
|
366
|
+
if slot and CJK.search(slot):
|
|
367
|
+
slot = ""
|
|
368
|
+
if mine:
|
|
369
|
+
en = mine
|
|
370
|
+
src_note = "定版(覆盖原件英文)" if slot else "定版(原件无英文)"
|
|
371
|
+
else:
|
|
372
|
+
en = slot
|
|
373
|
+
src_note = "退用原件英文" if slot else "缺译文"
|
|
374
|
+
rows.append((zh, en, a["n"], "、".join(a["wins"]), slot, src_note))
|
|
375
|
+
return rows
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def collect(proj, table, lookup, rename=None):
|
|
379
|
+
elems = scan_elements(proj)
|
|
380
|
+
return elems, merge_rows(aggregate(elems, rename), table, lookup)
|
|
381
|
+
|
|
382
|
+
|
|
383
|
+
def cmd_scan(args):
|
|
384
|
+
proj, out = args[0], args[1]
|
|
385
|
+
data = {"elems": scan_elements(proj), "lib": scan_library(proj)}
|
|
386
|
+
data["pairs"] = build_pairs(data["elems"])
|
|
387
|
+
json.dump(data, open(out, "w", encoding="utf-8"), ensure_ascii=False)
|
|
388
|
+
print("元件=%d 文本库条目=%d -> %s" % (len(data["elems"]), len(data["lib"]), out))
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def cmd_pair(args):
|
|
392
|
+
scan_json, out = args[0], args[1]
|
|
393
|
+
d = json.load(open(scan_json, encoding="utf-8"))
|
|
394
|
+
table, lookup, rename, third, lang = load_dict(args[2] if len(args) > 2 else None)
|
|
395
|
+
elems = d["elems"]
|
|
396
|
+
agg = aggregate(elems, rename)
|
|
397
|
+
rows = merge_rows(agg, table, lookup)
|
|
398
|
+
write_pair_table(rows, out, third, lang or "ko")
|
|
399
|
+
noen = [r for r in rows if not r[1]]
|
|
400
|
+
nothird = [r for r in rows if third and not third.get(r[0])]
|
|
401
|
+
print("对照表=%d 条(无英文 %d,无第三语种 %d)-> %s" % (len(rows), len(noen), len(nothird), out))
|
|
402
|
+
for r in noen[:20]:
|
|
403
|
+
print(" 缺英文:", r[0][:40])
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def cmd_make(args):
|
|
407
|
+
proj, outdir = args[0], args[1]
|
|
408
|
+
dpath = args[2] if len(args) > 2 else None
|
|
409
|
+
os.makedirs(outdir, exist_ok=True)
|
|
410
|
+
table, lookup, rename, third, lang = load_dict(dpath)
|
|
411
|
+
elems, rows = collect(proj, table, lookup, rename)
|
|
412
|
+
pair_x = os.path.join(outdir, "元件多语言对照表.xlsx")
|
|
413
|
+
imp_x = os.path.join(outdir, "文本库导入表.xlsx")
|
|
414
|
+
write_pair_table(rows, pair_x, third, lang or "ko")
|
|
415
|
+
write_import_table(rows, imp_x, ko=third, lang=lang or "ko")
|
|
416
|
+
noen = [r for r in rows if not r[1]]
|
|
417
|
+
nothird = [r for r in rows if third and not third.get(r[0])]
|
|
418
|
+
print("元件=%d;对照表=%d 条;无英文=%d;无第三语种=%d;语种=%s" % (
|
|
419
|
+
len(elems), len(rows), len(noen), len(nothird),
|
|
420
|
+
"中/英/%s" % (LANG_LABELS.get(lang, lang)) if third else "中/英"))
|
|
421
|
+
print("对照表 ->", pair_x)
|
|
422
|
+
print("导入表 ->", imp_x)
|
|
423
|
+
if noen:
|
|
424
|
+
print("--- 这些还没英文,补进词典后重跑 ---")
|
|
425
|
+
for r in noen[:30]:
|
|
426
|
+
print(" ", r[0][:50])
|
|
427
|
+
if nothird:
|
|
428
|
+
print("--- 这些还没第三语种译文,补进词典后重跑 ---")
|
|
429
|
+
for r in nothird[:30]:
|
|
430
|
+
print(" ", r[0][:50])
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
# ────────────────────────── 换语种列(已导出的 TextLib 表)──────────────────────────
|
|
434
|
+
|
|
435
|
+
def read_lib_table(path):
|
|
436
|
+
"""读 FStudio 导出的 TextLib 表(.xls 走 xlrd,.xlsx 走 openpyxl)。
|
|
437
|
+
返回 (表名, 表头, 行列表);数字单元格转整数,文本**原样保留**(软件导出的 Name 列常带首尾空格,
|
|
438
|
+
strip 掉会让前几列与源表对不上、失去「原样」保证)。
|
|
439
|
+
注:表头与中文列的空值判断用 .strip() 只看空不空,落盘的值不动。"""
|
|
440
|
+
if path.lower().endswith(".xls"):
|
|
441
|
+
import xlrd
|
|
442
|
+
b = xlrd.open_workbook(path)
|
|
443
|
+
sh = b.sheet_by_index(0)
|
|
444
|
+
head = [str(sh.cell_value(0, c)).strip() for c in range(sh.ncols)]
|
|
445
|
+
rows = []
|
|
446
|
+
for r in range(1, sh.nrows):
|
|
447
|
+
vals = []
|
|
448
|
+
for c in range(sh.ncols):
|
|
449
|
+
v = sh.cell_value(r, c)
|
|
450
|
+
if isinstance(v, float) and v == int(v):
|
|
451
|
+
v = int(v)
|
|
452
|
+
vals.append("" if v is None else (v if isinstance(v, int) else str(v)))
|
|
453
|
+
if any(str(x).strip() for x in vals):
|
|
454
|
+
rows.append(vals)
|
|
455
|
+
return sh.name, head, rows
|
|
456
|
+
from openpyxl import load_workbook
|
|
457
|
+
wb = load_workbook(path, data_only=True)
|
|
458
|
+
ws = wb[wb.sheetnames[0]]
|
|
459
|
+
head = [(str(c.value).strip() if c.value is not None else "") for c in ws[1]]
|
|
460
|
+
rows = []
|
|
461
|
+
for r in ws.iter_rows(min_row=2, values_only=True):
|
|
462
|
+
vals = []
|
|
463
|
+
for v in r:
|
|
464
|
+
if v is None:
|
|
465
|
+
vals.append("")
|
|
466
|
+
elif isinstance(v, float) and v == int(v):
|
|
467
|
+
vals.append(int(v))
|
|
468
|
+
elif isinstance(v, int):
|
|
469
|
+
vals.append(v)
|
|
470
|
+
else:
|
|
471
|
+
vals.append(str(v))
|
|
472
|
+
if any(str(x).strip() for x in vals):
|
|
473
|
+
rows.append(vals)
|
|
474
|
+
return ws.title, head, rows
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def write_lib_table(sheet, head, rows, out_xlsx):
|
|
478
|
+
"""按 FStudio 导出的样子写回:表头原样、Status 走数字、其余列文本。
|
|
479
|
+
输出按扩展名走:.xls(需 xlwt)或 .xlsx(openpyxl)。"""
|
|
480
|
+
if out_xlsx.lower().endswith(".xls"):
|
|
481
|
+
import xlwt
|
|
482
|
+
wb = xlwt.Workbook(encoding="utf-8")
|
|
483
|
+
ws = wb.add_sheet(sheet or "TextLib")
|
|
484
|
+
for c, h in enumerate(head):
|
|
485
|
+
ws.write(0, c, h)
|
|
486
|
+
for r, row in enumerate(rows, 1):
|
|
487
|
+
for c, v in enumerate(row):
|
|
488
|
+
ws.write(r, c, v if isinstance(v, int) else ("" if v is None else str(v)))
|
|
489
|
+
wb.save(out_xlsx)
|
|
490
|
+
return
|
|
491
|
+
from openpyxl import Workbook
|
|
492
|
+
from openpyxl.styles import Alignment, Font, PatternFill, Border, Side
|
|
493
|
+
from openpyxl.utils import get_column_letter
|
|
494
|
+
wb = Workbook()
|
|
495
|
+
ws = wb.active
|
|
496
|
+
ws.title = sheet or "TextLib"
|
|
497
|
+
thin = Side(style="thin", color="BFBFBF")
|
|
498
|
+
border = Border(left=thin, right=thin, top=thin, bottom=thin)
|
|
499
|
+
hf = PatternFill("solid", fgColor="1F4E79")
|
|
500
|
+
hfont = Font(color="FFFFFF", bold=True)
|
|
501
|
+
ws.append(head)
|
|
502
|
+
for c in range(1, len(head) + 1):
|
|
503
|
+
cell = ws.cell(row=1, column=c)
|
|
504
|
+
cell.fill, cell.font, cell.border = hf, hfont, border
|
|
505
|
+
cell.alignment = Alignment(horizontal="center", vertical="center")
|
|
506
|
+
for row in rows:
|
|
507
|
+
ws.append(row)
|
|
508
|
+
r = ws.max_row
|
|
509
|
+
for c in range(1, len(head) + 1):
|
|
510
|
+
cell = ws.cell(row=r, column=c)
|
|
511
|
+
cell.border = border
|
|
512
|
+
cell.alignment = Alignment(vertical="top", wrap_text=(c != 3))
|
|
513
|
+
ws.cell(row=r, column=3).number_format = "0"
|
|
514
|
+
widths = [38, 40, 8, 46, 52, 46]
|
|
515
|
+
for i, w in enumerate(widths[:len(head)], 1):
|
|
516
|
+
ws.column_dimensions[get_column_letter(i)].width = w
|
|
517
|
+
ws.freeze_panes = "A2"
|
|
518
|
+
wb.save(out_xlsx)
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def expand_status(rows, min_status):
|
|
522
|
+
"""把状态数不足 min_status 的条目补足:新增行 Guid 留空、Status 递增、各语种文字与**首行相同**。
|
|
523
|
+
|
|
524
|
+
判组按 FStudio 导出结构:首行带 Guid、后续状态行 Guid 为空。
|
|
525
|
+
(工程里成对词的条目本来就是两个状态各写各的字;单状态条目补的第二个状态就与首行同字,
|
|
526
|
+
软件导出的表里也是这么写的,不自己造词。)
|
|
527
|
+
返回 (新行列表, 补了几行)。"""
|
|
528
|
+
if not min_status or min_status < 2:
|
|
529
|
+
return rows, 0
|
|
530
|
+
out, added, i = [], 0, 0
|
|
531
|
+
while i < len(rows):
|
|
532
|
+
head = rows[i]
|
|
533
|
+
grp = [head]
|
|
534
|
+
j = i + 1
|
|
535
|
+
while j < len(rows) and not str(rows[j][0]).strip():
|
|
536
|
+
grp.append(rows[j])
|
|
537
|
+
j += 1
|
|
538
|
+
out.extend(grp)
|
|
539
|
+
for k in range(max(0, min_status - len(grp))):
|
|
540
|
+
new = list(head)
|
|
541
|
+
new[0] = ""
|
|
542
|
+
new[2] = len(grp) + k
|
|
543
|
+
out.append(new)
|
|
544
|
+
added += 1
|
|
545
|
+
i = j
|
|
546
|
+
return out, added
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
def cmd_retag(args):
|
|
550
|
+
"""retag <已导出TextLib表> <输出.xlsx> <词典> [语种键] [最少状态数]
|
|
551
|
+
第三语种列整列换成词典给的语种,行序/Guid/Status/中英文列原样 → 可直接导回文本库。
|
|
552
|
+
给了「最少状态数」(如 2)时,状态不够的条目按其首行补足状态行。"""
|
|
553
|
+
src, out = args[0], args[1]
|
|
554
|
+
dpath = args[2] if len(args) > 2 else None
|
|
555
|
+
lang_want = args[3] if len(args) > 3 else None
|
|
556
|
+
min_status = int(args[4]) if len(args) > 4 and str(args[4]).strip().isdigit() else 0
|
|
557
|
+
table, lookup, _rename, third, lang = load_dict(dpath, lang_want)
|
|
558
|
+
lang = lang_want or lang
|
|
559
|
+
if not third:
|
|
560
|
+
raise SystemExit("词典里没找到第三语种译文(.json 写 \"pt\"/\"ko\",.py 写 ELEMENT_PT/ELEMENT_KO);"
|
|
561
|
+
"语种键:%s" % (lang_want or "(未指定)"))
|
|
562
|
+
head_new = LANG_HEADS.get(lang)
|
|
563
|
+
if not head_new:
|
|
564
|
+
raise SystemExit("未知语种键 %s(可选: %s)" % (lang, "、".join(LANG_HEADS)))
|
|
565
|
+
sheet, head, rows = read_lib_table(src)
|
|
566
|
+
rows, added = expand_status(rows, min_status)
|
|
567
|
+
# 列位置:0 Guid / 1 Name / 2 Status / 3 中文 / 4 第二语种 / 5 第三语种
|
|
568
|
+
out_head = list(head[:5]) + [head_new]
|
|
569
|
+
out_rows, miss = [], []
|
|
570
|
+
for r in rows:
|
|
571
|
+
zh = str(r[3]).strip() if len(r) > 3 else ""
|
|
572
|
+
txt = third.get(zh, "")
|
|
573
|
+
if not txt and lookup:
|
|
574
|
+
txt = lookup(zh) or ""
|
|
575
|
+
if not txt:
|
|
576
|
+
miss.append(zh)
|
|
577
|
+
out_rows.append(list(r[:5]) + [txt])
|
|
578
|
+
if miss:
|
|
579
|
+
for t in list(dict.fromkeys(miss))[:30]:
|
|
580
|
+
print(" 缺:", repr(t[:40]))
|
|
581
|
+
raise SystemExit("有 %d 行没译文,未输出(先补词典)" % len(miss))
|
|
582
|
+
write_lib_table(sheet, out_head, out_rows, out)
|
|
583
|
+
print("源表: %s(%d 行 × %d 列,表名 %s)" % (src, len(rows) - added, len(head), sheet))
|
|
584
|
+
print("目标语种: %s -> 列名 %s" % (lang, head_new))
|
|
585
|
+
if min_status:
|
|
586
|
+
print("补状态行: 最少 %d 个状态,本次补了 %d 行" % (min_status, added))
|
|
587
|
+
print("输出行数 %d;缺译 0" % len(out_rows))
|
|
588
|
+
print("输出 ->", out)
|
|
589
|
+
|
|
590
|
+
|
|
591
|
+
def main():
|
|
592
|
+
if len(sys.argv) < 2:
|
|
593
|
+
raise SystemExit(__doc__)
|
|
594
|
+
cmd, args = sys.argv[1], sys.argv[2:]
|
|
595
|
+
if cmd == "scan":
|
|
596
|
+
cmd_scan(args)
|
|
597
|
+
elif cmd == "pair":
|
|
598
|
+
cmd_pair(args)
|
|
599
|
+
elif cmd == "make":
|
|
600
|
+
cmd_make(args)
|
|
601
|
+
elif cmd == "retag":
|
|
602
|
+
cmd_retag(args)
|
|
603
|
+
else:
|
|
604
|
+
raise SystemExit("未知子命令: %s" % cmd)
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
if __name__ == "__main__":
|
|
608
|
+
main()
|