dsh-plc-decode 0.2.1 → 0.2.3
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 +115 -27
- package/README.md +38 -13
- package/assets/codesys-tools/dvp_decode.py +53 -4
- package/assets/codesys-tools/read_project.py +1 -1
- package/assets/decode-pack.mjs +2 -2
- package/assets/dvp-ladder.mjs +174 -0
- package/assets/ebpro-decode.mjs +305 -0
- package/assets/hmi-decode-workflow.md +7 -9
- package/assets/hmismart-decode.mjs +453 -452
- package/assets/hmismart-report.mjs +9 -14
- package/assets/lad-branches.mjs +147 -0
- package/assets/ladder-render.mjs +118 -0
- package/assets/ladder-workflow.md +80 -0
- package/assets/smart-ladder-auto.mjs +182 -0
- package/assets/stl-to-ladder.mjs +299 -0
- package/hmi.js +36 -183
- package/index.js +70 -5
- package/ladder.js +381 -0
- package/package.json +7 -6
- package/scripts/smoke.mjs +2 -2
- 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 +0 -45
- package/assets/fstudio-i18n-workflow.md +0 -165
- package/assets/nb-textlib-workflow.md +0 -93
- package/assets/wincc-text-i18n-workflow.md +0 -96
- package/assets/wincc_i18n.py +0 -504
- package/invariant.js +0 -10
package/assets/wincc_i18n.py
DELETED
|
@@ -1,504 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
# -*- coding: utf-8 -*-
|
|
3
|
-
"""WinCC flexible / SMART 项目文本导出表(.xls/.xlsx)多语言处理器。
|
|
4
|
-
|
|
5
|
-
项目文本导出表的格式契约(WinCC flexible 导出 → Excel):
|
|
6
|
-
- 第 1 行 $_Languages
|
|
7
|
-
- 第 2 行 语言名,如「4(2) 简体中文(中国大陆)」「9(2) 英语(英国)」;第 1 列=源语言,第 2 列=目标语言
|
|
8
|
-
- 第 3 行 $_Typ(Screens) / $_Attrib(MultiLanguage)
|
|
9
|
-
- 若干 // 注释行
|
|
10
|
-
- 之后每行:第 1 列=源文本(画面上显示什么就以什么为键),第 2 列=目标语言文本
|
|
11
|
-
- 文本可能被 <text>...</text> 包裹;换行是**字面反斜杠 n**(两个字符),不是真换行
|
|
12
|
-
- 目标列出现 $_ignore = 该源文本在工程里存在多份不同译文,导出时被替换;导入时该行被忽略,必须原样保留
|
|
13
|
-
|
|
14
|
-
三条硬规则(本脚本按它判定):
|
|
15
|
-
1. 第 1 列是匹配键——改了就对不上,绝不能动
|
|
16
|
-
2. $_ignore 行照样要给译文(它是多译文冲突被导出挡掉的,不是不用翻)
|
|
17
|
-
3. 一源一译:同一句源文本全表只能有一个译文,否则导回后工程里又变成多译文、下次导出又变 $_ignore
|
|
18
|
-
|
|
19
|
-
第四条是校对判据:源文本不含中文(纯编号/代号)而译文比源长 → 判 CODE_EXTRA,
|
|
20
|
-
即别行的功能词串到这一行了(Q10 top box 这类),应只留编号。
|
|
21
|
-
|
|
22
|
-
子命令:
|
|
23
|
-
scan --files a.xls b.xls ... --outdir DIR 列出每行的分型(未译/空译/忽略/编号/已有译文),出清单与报告
|
|
24
|
-
apply --files ... --dict 映射.tsv --outdir DIR 按映射写回目标列,出可直接导回的 xls(源是 xls 就出 xls)
|
|
25
|
-
check --src a.xls --out a_译文.xls 自检:行数/键列/忽略行/中文残留/一源一译
|
|
26
|
-
|
|
27
|
-
映射文件格式(TSV,制表符分隔,UTF-8,可带表头):
|
|
28
|
-
源文本<TAB>译文
|
|
29
|
-
换行写 \n(字面两个字符)
|
|
30
|
-
"""
|
|
31
|
-
|
|
32
|
-
import argparse
|
|
33
|
-
import json
|
|
34
|
-
import os
|
|
35
|
-
import re
|
|
36
|
-
import shutil
|
|
37
|
-
import subprocess
|
|
38
|
-
import sys
|
|
39
|
-
import tempfile
|
|
40
|
-
|
|
41
|
-
TEXT_RE = re.compile(r"^<text>(.*)</text>\s*$", re.S)
|
|
42
|
-
CJK_RE = re.compile(r"[\u4e00-\u9fff\u3000-\u303f\uff00-\uffef]")
|
|
43
|
-
# 日期/时间格式串(yyyy-M-d H:mm:ss 之类)两列本来就不一样,不能算串位
|
|
44
|
-
DATEFMT_RE = re.compile(r"^[yMdHmsAP:/.\s-]+$")
|
|
45
|
-
IGNORE = "$_ignore"
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
def looks_datefmt(s):
|
|
49
|
-
s = s.strip()
|
|
50
|
-
return len(s) >= 6 and bool(DATEFMT_RE.match(s))
|
|
51
|
-
|
|
52
|
-
# 项目文本导出表里非正文的头部行数上限(语言行/类型行/注释行)
|
|
53
|
-
HEADER_SCAN_LIMIT = 12
|
|
54
|
-
|
|
55
|
-
LO_KIT_CANDIDATES = [
|
|
56
|
-
os.environ.get("WINCC_LO_KIT", ""),
|
|
57
|
-
r"D:\DSH\Desktop\resources\app.asar.unpacked\dsh\node_modules\@deepseek-ai\libreoffice-kit\lib\cli.js",
|
|
58
|
-
]
|
|
59
|
-
NODE_CANDIDATES = [
|
|
60
|
-
os.environ.get("WINCC_LO_NODE", ""),
|
|
61
|
-
r"D:\DSH\Desktop\resources\runtime\primary-runtime\dependencies\node\bin\node.exe",
|
|
62
|
-
r"D:\DSH\home\dsh-runtimes\dsh-primary-runtime\dependencies\node\bin\node.exe",
|
|
63
|
-
]
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
# ───────────────────────── 文件格式 ─────────────────────────
|
|
67
|
-
|
|
68
|
-
def _find_lo_kit():
|
|
69
|
-
cli = next((p for p in LO_KIT_CANDIDATES if p and os.path.exists(p)), None)
|
|
70
|
-
node = next((p for p in NODE_CANDIDATES if p and os.path.exists(p)), None)
|
|
71
|
-
return (node, cli) if cli and node else (None, None)
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
def convert(src, dst):
|
|
75
|
-
"""借 LibreOffice Kit 转格式(xls <-> xlsx)。返回 dst。"""
|
|
76
|
-
node, cli = _find_lo_kit()
|
|
77
|
-
if not cli:
|
|
78
|
-
raise SystemExit(
|
|
79
|
-
f"需要 LibreOffice Kit 转换 {os.path.basename(src)},但没找到 cli.js。\n"
|
|
80
|
-
"可设环境变量 WINCC_LO_KIT=<cli.js 路径> / WINCC_LO_NODE=<node 路径>。"
|
|
81
|
-
)
|
|
82
|
-
if os.path.exists(dst):
|
|
83
|
-
os.remove(dst)
|
|
84
|
-
out = subprocess.run([node, cli, "convert", "--input", src, "--output", dst],
|
|
85
|
-
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
|
86
|
-
if out.returncode != 0 or not os.path.exists(dst):
|
|
87
|
-
raise SystemExit(f"转换失败({os.path.basename(src)} -> {os.path.basename(dst)}):\n{out.stdout}\n{out.stderr}")
|
|
88
|
-
return dst
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
def as_xlsx(path, workdir):
|
|
92
|
-
"""拿到一个可被 openpyxl 读的 xlsx 路径。"""
|
|
93
|
-
if path.lower().endswith(".xlsx"):
|
|
94
|
-
return path
|
|
95
|
-
os.makedirs(workdir, exist_ok=True)
|
|
96
|
-
return convert(path, os.path.join(workdir, os.path.splitext(os.path.basename(path))[0] + ".xlsx"))
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
# ───────────────────────── 解析 ─────────────────────────
|
|
100
|
-
|
|
101
|
-
def parse(path, workdir):
|
|
102
|
-
import openpyxl
|
|
103
|
-
xlsx = as_xlsx(path, workdir)
|
|
104
|
-
wb = openpyxl.load_workbook(xlsx, data_only=True)
|
|
105
|
-
ws = wb.worksheets[0]
|
|
106
|
-
|
|
107
|
-
rows = []
|
|
108
|
-
for r in ws.iter_rows(values_only=True):
|
|
109
|
-
# 整行保留:导入是按列对语言的,多余列必须原样带回去
|
|
110
|
-
rows.append(["" if v is None else str(v) for v in r])
|
|
111
|
-
for row in rows:
|
|
112
|
-
while len(row) < 2:
|
|
113
|
-
row.append("")
|
|
114
|
-
|
|
115
|
-
# 头部:直到出现第一行非注释、非表头的内容
|
|
116
|
-
body_start = 0
|
|
117
|
-
langs = [rows[1][0] if len(rows) > 1 else "", rows[1][1] if len(rows) > 1 else ""]
|
|
118
|
-
typ = ""
|
|
119
|
-
for i, row in enumerate(rows[:HEADER_SCAN_LIMIT]):
|
|
120
|
-
s = row[0].strip()
|
|
121
|
-
if s.startswith("$_Languages"):
|
|
122
|
-
continue
|
|
123
|
-
if s.startswith("$_Typ(") or s.startswith("$_Attrib("):
|
|
124
|
-
typ = typ or s
|
|
125
|
-
continue
|
|
126
|
-
if s.startswith("//"):
|
|
127
|
-
continue
|
|
128
|
-
if re.match(r"^\d+\(\d+\)", s):
|
|
129
|
-
continue
|
|
130
|
-
body_start = i
|
|
131
|
-
break
|
|
132
|
-
|
|
133
|
-
return {"path": path, "rows": rows, "body_start": body_start, "langs": langs, "typ": typ,
|
|
134
|
-
"xlsx": xlsx, "sheet": ws.title}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
def core_of(s):
|
|
138
|
-
"""去掉 <text> 包裹,返回 (是否包裹, 内容)。"""
|
|
139
|
-
m = TEXT_RE.match(s)
|
|
140
|
-
return (True, m.group(1)) if m else (False, s)
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def classify(src, dst):
|
|
144
|
-
"""给一行分型。返回 (type, 说明)。"""
|
|
145
|
-
if not src.strip():
|
|
146
|
-
return ("EMPTY_KEY", "源文本为空")
|
|
147
|
-
wrapped, core = core_of(src)
|
|
148
|
-
if dst == IGNORE:
|
|
149
|
-
return ("IGNORE", "多译文冲突,导出时已替换,导入会被忽略")
|
|
150
|
-
if not dst.strip():
|
|
151
|
-
return ("UNTRANSLATED", "目标列空")
|
|
152
|
-
_, core_b = core_of(dst)
|
|
153
|
-
if CJK_RE.search(core_b):
|
|
154
|
-
return ("UNTRANSLATED", "目标列还是中文" if core_b.strip() != core.strip() else "目标列=源文本")
|
|
155
|
-
if not CJK_RE.search(core) and core.strip() == core_b.strip():
|
|
156
|
-
return ("SAME_AS_SRC", "非中文文本,两列相同(编号/符号/日期格式)")
|
|
157
|
-
if not CJK_RE.search(core) and not looks_datefmt(core) and len(core_b.strip()) > len(core.strip()):
|
|
158
|
-
# 源是纯编号/代号,译文却更长 → 别行的功能词串过来了(Q10 top box 这种)
|
|
159
|
-
return ("CODE_EXTRA", "源无中文,译文多出内容(疑串位,应只留编号)")
|
|
160
|
-
return ("HAS_TRANS", "已有译文,需人工核错译/串位")
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def scan(p):
|
|
164
|
-
out = []
|
|
165
|
-
for idx in range(p["body_start"], len(p["rows"])):
|
|
166
|
-
row = p["rows"][idx]
|
|
167
|
-
src, dst = row[0], row[1]
|
|
168
|
-
if not src.strip() and not dst.strip():
|
|
169
|
-
continue
|
|
170
|
-
t, note = classify(src, dst)
|
|
171
|
-
out.append({"row": idx + 1, "src": src, "dst": dst, "type": t, "note": note})
|
|
172
|
-
return out
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
# ───────────────────────── 一源一译 ─────────────────────────
|
|
176
|
-
|
|
177
|
-
def conflicts(items):
|
|
178
|
-
"""同一源文本出现多个不同译文 → 导入后工程又变多译文。"""
|
|
179
|
-
seen = {}
|
|
180
|
-
for it in items:
|
|
181
|
-
if it["type"] == "IGNORE":
|
|
182
|
-
continue
|
|
183
|
-
_, core = core_of(it["src"])
|
|
184
|
-
if not core.strip():
|
|
185
|
-
continue
|
|
186
|
-
_, tgt = core_of(it["dst"])
|
|
187
|
-
if not tgt.strip():
|
|
188
|
-
continue
|
|
189
|
-
seen.setdefault(core, {}).setdefault(tgt, []).append(it["row"])
|
|
190
|
-
bad = {}
|
|
191
|
-
for core, m in seen.items():
|
|
192
|
-
if len(m) > 1:
|
|
193
|
-
bad[core] = {k: v for k, v in m.items()}
|
|
194
|
-
return bad
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
# ───────────────────────── 命令 ─────────────────────────
|
|
198
|
-
|
|
199
|
-
def cmd_scan(args):
|
|
200
|
-
os.makedirs(args.outdir, exist_ok=True)
|
|
201
|
-
work = os.path.join(args.outdir, "_work")
|
|
202
|
-
report = []
|
|
203
|
-
tsv_lines = ["文件\t行号\t分型\t源文本\t现有译文"]
|
|
204
|
-
stat = {}
|
|
205
|
-
for f in args.files:
|
|
206
|
-
p = parse(f, work)
|
|
207
|
-
items = scan(p)
|
|
208
|
-
name = os.path.basename(f)
|
|
209
|
-
c = {}
|
|
210
|
-
for it in items:
|
|
211
|
-
c[it["type"]] = c.get(it["type"], 0) + 1
|
|
212
|
-
tsv_lines.append("\t".join([name, str(it["row"]), it["type"], it["src"].replace("\t", " "), it["dst"].replace("\t", " ")]))
|
|
213
|
-
bad = conflicts(items)
|
|
214
|
-
stat[name] = c
|
|
215
|
-
report.append((name, p, items, bad))
|
|
216
|
-
|
|
217
|
-
list_path = os.path.join(args.outdir, "待译清单.tsv")
|
|
218
|
-
with open(list_path, "w", encoding="utf-8", newline="") as fh:
|
|
219
|
-
fh.write("\n".join(tsv_lines) + "\n")
|
|
220
|
-
|
|
221
|
-
md = ["# 项目文本分型报告", ""]
|
|
222
|
-
total_need = 0
|
|
223
|
-
for name, p, items, bad in report:
|
|
224
|
-
c = {}
|
|
225
|
-
for it in items:
|
|
226
|
-
c[it["type"]] = c.get(it["type"], 0) + 1
|
|
227
|
-
need = c.get("UNTRANSLATED", 0)
|
|
228
|
-
total_need += need
|
|
229
|
-
md.append(f"## {name}")
|
|
230
|
-
md.append("")
|
|
231
|
-
md.append(f"- 语言:{p['langs'][0]} → {p['langs'][1]}({p['typ']})")
|
|
232
|
-
md.append(f"- 正文行 {len(items)}:未译 {need} / 已有译文 {c.get('HAS_TRANS', 0)} / 忽略 {c.get('IGNORE', 0)} / 非中文同形 {c.get('SAME_AS_SRC', 0)} / 编号行挂词 {c.get('CODE_EXTRA', 0)}")
|
|
233
|
-
if bad:
|
|
234
|
-
md.append(f"- ⚠ 一源多译 {len(bad)} 组(导回后工程会产生多译文,必须统一):")
|
|
235
|
-
for core, m in list(bad.items())[:20]:
|
|
236
|
-
md.append(f" - {core!r} → " + ";".join(f"{k!r}(行{','.join(map(str, v))})" for k, v in m.items()))
|
|
237
|
-
md.append("")
|
|
238
|
-
md.insert(1, f"共 {len(args.files)} 个表,未译合计 {total_need} 条。清单:{list_path}")
|
|
239
|
-
md_path = os.path.join(args.outdir, "分型报告.md")
|
|
240
|
-
with open(md_path, "w", encoding="utf-8") as fh:
|
|
241
|
-
fh.write("\n".join(md) + "\n")
|
|
242
|
-
|
|
243
|
-
print("\n".join(md))
|
|
244
|
-
return 0
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
def load_dict(path):
|
|
248
|
-
"""映射:源文本 → 译文。支持 TSV(源\t译文)与 JSON({"源":"译文"})。"""
|
|
249
|
-
m = {}
|
|
250
|
-
if path.lower().endswith(".json"):
|
|
251
|
-
with open(path, encoding="utf-8") as fh:
|
|
252
|
-
raw = json.load(fh)
|
|
253
|
-
for k, v in raw.items():
|
|
254
|
-
m[k] = v
|
|
255
|
-
return m
|
|
256
|
-
with open(path, encoding="utf-8-sig") as fh:
|
|
257
|
-
for line in fh:
|
|
258
|
-
line = line.rstrip("\n").rstrip("\r")
|
|
259
|
-
if not line.strip() or line.startswith("#"):
|
|
260
|
-
continue
|
|
261
|
-
parts = line.split("\t")
|
|
262
|
-
if len(parts) < 2:
|
|
263
|
-
continue
|
|
264
|
-
k, v = parts[0], parts[1]
|
|
265
|
-
if k == "源文本":
|
|
266
|
-
continue
|
|
267
|
-
# 表里的换行是真换行(Excel 单元格内 Alt+Enter),映射文件一行放不下,
|
|
268
|
-
# 所以在映射里写 \n(字面两字符),读进来还原成真换行。
|
|
269
|
-
m[k.replace("\\n", "\n")] = v.replace("\\n", "\n")
|
|
270
|
-
return m
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
def write_book(rows, path, sheet_name="Sheet1"):
|
|
274
|
-
"""写 .xlsx(openpyxl)。"""
|
|
275
|
-
import openpyxl
|
|
276
|
-
wb = openpyxl.Workbook()
|
|
277
|
-
ws = wb.active
|
|
278
|
-
ws.title = sheet_name
|
|
279
|
-
for r in rows:
|
|
280
|
-
ws.append([("" if v is None else v) for v in r])
|
|
281
|
-
wb.save(path)
|
|
282
|
-
return path
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
def write_xls(src, old_rows, rows, path, sheet_name="Sheet1"):
|
|
286
|
-
"""写 Excel 97-2003 .xls。与源表同扩展名、同工作表名,导回软件时才认得出是哪类文本。
|
|
287
|
-
|
|
288
|
-
首选 Excel COM:拿源表复制一份,只改变化的格子——行数、列数、格式、其他列全原样
|
|
289
|
-
(xlwt 整表重建实测会吞行,394 行写完剩 393,之后整表错位)。
|
|
290
|
-
没装 Excel 才退到 xlwt,且写完必须回读核对行数。
|
|
291
|
-
"""
|
|
292
|
-
errs = []
|
|
293
|
-
for fn in (_write_xls_via_com, _write_xls_via_xlwt):
|
|
294
|
-
try:
|
|
295
|
-
return fn(src, old_rows, rows, path, sheet_name)
|
|
296
|
-
except Exception as exc: # noqa: BLE001 - 记录后试下一条路
|
|
297
|
-
errs.append(f"{fn.__name__}: {exc}")
|
|
298
|
-
raise SystemExit("写 .xls 两条路都不通:\n " + "\n ".join(errs))
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
def _find_powershell():
|
|
302
|
-
for name in ("powershell.exe", "pwsh.exe", "powershell", "pwsh"):
|
|
303
|
-
p = shutil.which(name)
|
|
304
|
-
if p:
|
|
305
|
-
return p
|
|
306
|
-
return None
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
_COM_SCRIPT = """
|
|
310
|
-
param([string]$Xls, [string]$Plan)
|
|
311
|
-
$ErrorActionPreference = 'Stop'
|
|
312
|
-
$xl = New-Object -ComObject Excel.Application
|
|
313
|
-
$xl.Visible = $false
|
|
314
|
-
$xl.DisplayAlerts = $false
|
|
315
|
-
try {
|
|
316
|
-
$wb = $xl.Workbooks.Open($Xls)
|
|
317
|
-
$ws = $wb.Worksheets.Item(1)
|
|
318
|
-
foreach ($line in Get-Content -LiteralPath $Plan -Encoding UTF8) {
|
|
319
|
-
if ($line.Trim() -eq '') { continue }
|
|
320
|
-
$i = $line.IndexOf("`t")
|
|
321
|
-
$r = [int]$line.Substring(0, $i)
|
|
322
|
-
$rest = $line.Substring($i + 1)
|
|
323
|
-
$j = $rest.IndexOf("`t")
|
|
324
|
-
$c = [int]$rest.Substring(0, $j)
|
|
325
|
-
$v = $rest.Substring($j + 1).Replace('\\n', "`n")
|
|
326
|
-
$ws.Cells.Item($r, $c).Value2 = $v
|
|
327
|
-
}
|
|
328
|
-
$wb.Save()
|
|
329
|
-
$wb.Close($true)
|
|
330
|
-
} finally {
|
|
331
|
-
$xl.Quit()
|
|
332
|
-
[void][System.Runtime.InteropServices.Marshal]::ReleaseComObject($xl)
|
|
333
|
-
}
|
|
334
|
-
"""
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
def _write_xls_via_com(src, old_rows, rows, path, sheet_name="Sheet1"):
|
|
338
|
-
"""复制源表 → 用 Excel COM 只改变化的格子 → 保存。行数与格式天然不变。"""
|
|
339
|
-
ps = _find_powershell()
|
|
340
|
-
if not ps:
|
|
341
|
-
raise RuntimeError("找不到 powershell")
|
|
342
|
-
plan = []
|
|
343
|
-
for i in range(max(len(old_rows), len(rows))):
|
|
344
|
-
a = old_rows[i] if i < len(old_rows) else []
|
|
345
|
-
b = rows[i] if i < len(rows) else []
|
|
346
|
-
for c in range(max(len(a), len(b))):
|
|
347
|
-
va = a[c] if c < len(a) else ""
|
|
348
|
-
vb = b[c] if c < len(b) else ""
|
|
349
|
-
if str(va) != str(vb):
|
|
350
|
-
plan.append("%d\t%d\t%s" % (i + 1, c + 1, str(vb).replace("\n", "\\n")))
|
|
351
|
-
if not os.path.exists(path) or os.path.abspath(path) != os.path.abspath(src):
|
|
352
|
-
shutil.copyfile(src, path)
|
|
353
|
-
if not plan:
|
|
354
|
-
return path
|
|
355
|
-
tmpdir = tempfile.mkdtemp(prefix="wincc_xls_")
|
|
356
|
-
plan_path = os.path.join(tmpdir, "plan.tsv")
|
|
357
|
-
with open(plan_path, "w", encoding="utf-8", newline="") as fh:
|
|
358
|
-
fh.write("\n".join(plan) + "\n")
|
|
359
|
-
script = os.path.join(tmpdir, "apply.ps1")
|
|
360
|
-
with open(script, "w", encoding="utf-8-sig") as fh:
|
|
361
|
-
fh.write(_COM_SCRIPT)
|
|
362
|
-
out = subprocess.run([ps, "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", script,
|
|
363
|
-
"-Xls", path, "-Plan", plan_path],
|
|
364
|
-
capture_output=True, text=True, encoding="utf-8", errors="replace")
|
|
365
|
-
if out.returncode != 0:
|
|
366
|
-
raise RuntimeError((out.stderr or out.stdout or "COM 写表失败").strip()[:400])
|
|
367
|
-
return path
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
def _write_xls_via_xlwt(src, old_rows, rows, path, sheet_name="Sheet1"):
|
|
371
|
-
"""退化路径:xlwt 整表重建。会让格式与未引用列走样,只在没有 Excel 时用。"""
|
|
372
|
-
import xlwt
|
|
373
|
-
book = xlwt.Workbook(encoding="utf-8")
|
|
374
|
-
# 工作表名必须与软件导出的一致(Screens / Others / Messages)——
|
|
375
|
-
# 写成默认的 Sheet1,导入时软件认不出表类型,会整份跳过。
|
|
376
|
-
sheet = book.add_sheet(sheet_name)
|
|
377
|
-
for r_i, r in enumerate(rows):
|
|
378
|
-
for c_i, v in enumerate(r):
|
|
379
|
-
# 空列也要写:原表有几列就保持几列,导入侧按列对语言
|
|
380
|
-
sheet.write(r_i, c_i, "" if v is None else str(v))
|
|
381
|
-
if os.path.exists(path):
|
|
382
|
-
os.remove(path)
|
|
383
|
-
book.save(path)
|
|
384
|
-
return path
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
def cmd_apply(args):
|
|
388
|
-
os.makedirs(args.outdir, exist_ok=True)
|
|
389
|
-
work = os.path.join(args.outdir, "_work")
|
|
390
|
-
mapping = load_dict(args.dict)
|
|
391
|
-
if not mapping:
|
|
392
|
-
raise SystemExit(f"映射文件没有内容: {args.dict}")
|
|
393
|
-
|
|
394
|
-
log = []
|
|
395
|
-
applied = 0
|
|
396
|
-
missing = 0
|
|
397
|
-
for f in args.files:
|
|
398
|
-
p = parse(f, work)
|
|
399
|
-
rows = [list(r) for r in p["rows"]]
|
|
400
|
-
for idx in range(p["body_start"], len(rows)):
|
|
401
|
-
src, dst = rows[idx][0], rows[idx][1]
|
|
402
|
-
if not src.strip():
|
|
403
|
-
continue
|
|
404
|
-
wrapped, core = core_of(src)
|
|
405
|
-
# $_ignore 行照样要填:它是「工程里已有多个不同译文」被导出挡掉的,
|
|
406
|
-
# 留着 $_ignore 等于这段文字在表里仍是源语言,导回去屏上也不变。
|
|
407
|
-
if core in mapping:
|
|
408
|
-
new = mapping[core]
|
|
409
|
-
rows[idx][1] = f"<text>{new}</text>" if wrapped else new
|
|
410
|
-
applied += 1
|
|
411
|
-
else:
|
|
412
|
-
# 没给译文的行:源是中文、目标也是中文/空 → 记下来
|
|
413
|
-
_, core_b = core_of(dst)
|
|
414
|
-
if CJK_RE.search(core) and (not core_b.strip() or CJK_RE.search(core_b)):
|
|
415
|
-
missing += 1
|
|
416
|
-
log.append(f"{os.path.basename(f)} 行{idx+1} 未给译文: {core!r}")
|
|
417
|
-
|
|
418
|
-
stem = os.path.splitext(os.path.basename(f))[0]
|
|
419
|
-
if f.lower().endswith(".xls"):
|
|
420
|
-
# 源是 .xls 就出 .xls:WinCC flexible 的导入对话框按扩展名过滤,给 xlsx 会选不到
|
|
421
|
-
out_path = os.path.join(args.outdir, stem + ".xls")
|
|
422
|
-
write_xls(f, p["rows"], rows, out_path, p["sheet"])
|
|
423
|
-
# 写完回读核对:行数与第 1 列必须与源表一致,否则整表会错位
|
|
424
|
-
back = parse(out_path, work)
|
|
425
|
-
if len(back["rows"]) != len(p["rows"]):
|
|
426
|
-
raise SystemExit(f"写出的 {stem}.xls 行数 {len(back['rows'])} ≠ 源表 {len(p['rows'])}")
|
|
427
|
-
for i in range(len(p["rows"])):
|
|
428
|
-
if back["rows"][i][0] != p["rows"][i][0]:
|
|
429
|
-
raise SystemExit(f"写出的 {stem}.xls 行{i+1} 键列与源表不一致")
|
|
430
|
-
else:
|
|
431
|
-
out_path = os.path.join(args.outdir, stem + ".xlsx")
|
|
432
|
-
write_book(rows, out_path, p["sheet"])
|
|
433
|
-
log.append(f"产出: {out_path}")
|
|
434
|
-
|
|
435
|
-
print(f"写回 {applied} 条;未给译文 {missing} 条。")
|
|
436
|
-
if log:
|
|
437
|
-
print("\n".join(log))
|
|
438
|
-
return 0
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
def cmd_check(args):
|
|
442
|
-
work = os.path.join(args.outdir or tempfile.gettempdir(), "_work")
|
|
443
|
-
src = parse(args.src, work)
|
|
444
|
-
out = parse(args.out, work)
|
|
445
|
-
problems = []
|
|
446
|
-
|
|
447
|
-
if len(src["rows"]) != len(out["rows"]):
|
|
448
|
-
problems.append(f"行数不一致:原 {len(src['rows'])} / 产 {len(out['rows'])}")
|
|
449
|
-
n = min(len(src["rows"]), len(out["rows"]))
|
|
450
|
-
for i in range(n):
|
|
451
|
-
a1 = src["rows"][i][0]
|
|
452
|
-
a2 = out["rows"][i][0]
|
|
453
|
-
if a1 != a2:
|
|
454
|
-
problems.append(f"行{i+1} 键列(第 1 列)被改动:{a1!r} → {a2!r}")
|
|
455
|
-
|
|
456
|
-
items = scan(out)
|
|
457
|
-
for it in items:
|
|
458
|
-
if it["type"] == "UNTRANSLATED":
|
|
459
|
-
problems.append(f"行{it['row']} 未译/{it['note']}:{it['src'][:40]!r}")
|
|
460
|
-
elif it["type"] == "IGNORE":
|
|
461
|
-
# 填了才算译完;还留着 $_ignore 就是目标列仍是源语言
|
|
462
|
-
problems.append(f"行{it['row']} 仍是 $_ignore(未填译文):{it['src'][:40]!r}")
|
|
463
|
-
elif it["type"] == "CODE_EXTRA":
|
|
464
|
-
problems.append(f"行{it['row']} 译文多出内容(疑串位,源无中文):{it['src'][:30]!r} → {it['dst'][:40]!r}")
|
|
465
|
-
bad = conflicts(items)
|
|
466
|
-
for core, m in bad.items():
|
|
467
|
-
problems.append("一源多译:" + repr(core) + " → " + ";".join(map(repr, m.keys())))
|
|
468
|
-
|
|
469
|
-
if problems:
|
|
470
|
-
print(f"✗ 自检发现 {len(problems)} 处问题:")
|
|
471
|
-
for x in problems:
|
|
472
|
-
print(" - " + x)
|
|
473
|
-
return 1
|
|
474
|
-
print(f"✓ 自检通过:{n} 行,键列未动,无未译、无 $_ignore 残留、无串位、无一源多译。")
|
|
475
|
-
return 0
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
def main(argv=None):
|
|
479
|
-
ap = argparse.ArgumentParser(description="WinCC flexible 项目文本导出表多语言处理")
|
|
480
|
-
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
481
|
-
|
|
482
|
-
s = sub.add_parser("scan", help="分型出清单")
|
|
483
|
-
s.add_argument("--files", nargs="+", required=True)
|
|
484
|
-
s.add_argument("--outdir", required=True)
|
|
485
|
-
s.set_defaults(func=cmd_scan)
|
|
486
|
-
|
|
487
|
-
a = sub.add_parser("apply", help="按映射写回")
|
|
488
|
-
a.add_argument("--files", nargs="+", required=True)
|
|
489
|
-
a.add_argument("--dict", required=True)
|
|
490
|
-
a.add_argument("--outdir", required=True)
|
|
491
|
-
a.set_defaults(func=cmd_apply)
|
|
492
|
-
|
|
493
|
-
c = sub.add_parser("check", help="自检产物")
|
|
494
|
-
c.add_argument("--src", required=True)
|
|
495
|
-
c.add_argument("--out", required=True)
|
|
496
|
-
c.add_argument("--outdir", default="")
|
|
497
|
-
c.set_defaults(func=cmd_check)
|
|
498
|
-
|
|
499
|
-
args = ap.parse_args(argv)
|
|
500
|
-
return args.func(args)
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
if __name__ == "__main__":
|
|
504
|
-
sys.exit(main())
|
package/invariant.js
DELETED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
const PACKAGE_NAME = 'dsh-plc-decode'
|
|
2
|
-
|
|
3
|
-
/** Cordis companion plugin name. */
|
|
4
|
-
export const name = 'dsh-plc-decode-invariant'
|
|
5
|
-
/** Service required before the companion can reserve package ownership. */
|
|
6
|
-
export const inject = ['invariants']
|
|
7
|
-
|
|
8
|
-
const install = () => {}
|
|
9
|
-
|
|
10
|
-
export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
|