MPM Documentation Kit v1.3 (CW-034)

MPM Document Standard 1.3, mpmdoc build engine, templates for TN, INS, SOP,
BOM, SVY, TAP, DWG, TSB, PRO, SOW and WO (Chinese editions for TN and DWG),
glossary, MPM fonts and logos, and the mpm-technical-documents plugin 1.3.0.
Worked examples ship in the kit zip on Drive only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWatTMmqmGatgHjDZvWs5b
This commit is contained in:
Jason StedwellandClaude Opus 5.5 committed 2026-11-09 11:51:56 -06:00
commit 5b52636003
143 files changed
+4097

No files matched your search

+207
View File
@@ -0,0 +1,207 @@
"""Simplified Chinese (简体中文) editions for manufacturers. See MPM_Document_Standard.md §12.
A Chinese edition is a second PDF of the same document: same number, revision and geometry, body text
in Chinese, frame labels bilingual, file name ending `_ZH_RevX.Y.pdf`. The English edition governs.
It is built by `source/build_zh.py` next to the English `build.py`:
from mpmdoc import Doc, i18n
tr = i18n.Translator({...document terms...}) # master glossary: _STANDARDS/glossary_zh.md
doc = Doc(..., slug=<same as English>, lang="zh") # TN; Drawing(..., lang="zh") for DWG
...
tr.check() # fails the build on any untranslated figure text
What the engine does for lang="zh":
* embeds the MPM SC font (Noto Sans SC, _ASSETS/mpm/fonts/cjk/) subset to the characters used, and adds
it as the fallback to every font stack, so Latin text and numbers stay in the brand fonts
* prints its own labels in Chinese (frame labels bilingual), dates as 2026年11月9日
* adds the governing-language statement (Doc: under the masthead; Drawing: last general note on each sheet
unless already present)
* names the file {NUMBER}_{slug}_ZH_Rev{X.Y}.pdf and refuses to build unless the English edition at the
same revision is already in the project folder
"""
import base64
import io
import re
from pathlib import Path
from .brand import ASSETS
LANGS = ("en", "zh")
TYPES = {"zh": {"TN", "DWG"}} # types with a defined Chinese edition (standard §12)
SUFFIX = {"en": "", "zh": "_ZH"}
FAMILY = "MPM SC"
CJK_DIR = ASSETS / "fonts" / "cjk"
GLOSSARY = Path(__file__).resolve().parent.parent / "glossary_zh.md"
# type code -> (header label, kicker label)
TYPE_ZH = {"TN": ("技术说明", "技术说明"), "DWG": ("图纸", "图纸")}
MARK_ZH = {"internal": "内部资料", "confidential": "机密"}
# engine labels: English -> Chinese. Frame labels a reader at MPM must find their way by stay bilingual.
ZH = {
"Document": "文件编号", "Revision": "版本", "Date": "日期", "Effective": "生效日期",
"Draft": "草稿", "Released": "已发布",
"Revision history": "修订记录 · Revision history",
"Fig": "图", "FIG": "图", "Table": "表", "TABLE": "表",
"Result": "结论", "Before you start": "开始之前",
"Assumptions": "假设条件", "Open items": "待办事项", "Assumptions and open items": "假设条件与待办事项",
"None.": "无。",
"Prepared by": "编制", "Checked by": "校对", "Prepared and approved by": "编制及批准",
"PASS": "合格", "FAIL": "不合格", "CHECK": "需核查",
"TBC": "待确认", "To verify": "待核实",
# drawing frame
"Schedule": "部件表 · Schedule", "General notes": "总说明 · General notes",
"Tag": "编号", "Designation": "名称", "Description": "说明", "Qty": "数量",
"Project": "项目", "Client": "客户 Client", "Location": "地点 Location", "Scale": "比例 Scale",
"Drawn": "制图 Drawn", "Checked": "校对 Checked", "Approved": "审核 Approved",
"Dwg no.": "图号 Dwg no.", "Sheet": "图页 Sheet", "Rev": "版本 Rev", "Date ": "日期 Date",
"Dwg no. · Sheet": "图号 · 图页 · 中文版", "SCALE": "比例", "N.T.S.": "不按比例", "As shown": "见图",
"Irondale, AL": "Irondale, AL, USA",
"Do not distribute without authorization.": "未经授权不得分发。",
"Proprietary to Message Point Media": "MPM 专有资料",
}
def L(lang, key):
return key if lang == "en" else ZH.get(key, key)
def show_date(d, lang="en"):
from .units import show_date as _en, parse_date
if lang == "en":
return _en(d)
d = parse_date(d)
return f"{d.year}年{d.month}月{d.day}日"
def governing_note(number, rev):
return f"本文件为 {number} Rev {rev} 英文版的中文译本;如有歧义,以英文版为准。"
def page_of(lang):
if lang == "en":
return 'Page <span class="pageNumber"></span> of <span class="totalPages"></span>'
return '第 <span class="pageNumber"></span> 页 / 共 <span class="totalPages"></span> 页'
# ---- fonts -------------------------------------------------------------------------------
_CJK = re.compile(r"[⺀-鿿 -〿＀-￯㄀-ㄯ]")
def has_cjk(s):
return bool(_CJK.search(s))
def font_css(text):
"""@font-face rules for MPM SC, subset to the CJK characters in `text` (woff2, inlined)."""
from fontTools.ttLib import TTFont
from fontTools.subset import Subsetter, Options
chars = {ord(c) for c in text if _CJK.match(c)} | {ord(c) for c in ":;,。()、"}
rules = []
for style, weights in (("Regular", "100 549"), ("Bold", "550 900")):
src = CJK_DIR / f"NotoSansSC-{style}.ttf"
if not src.exists():
raise FileNotFoundError(f"{src} missing: run _ASSETS/mpm/fonts/cjk/make_cjk.py (or stage it)")
f = TTFont(src)
opts = Options(); opts.flavor = "woff2"; opts.layout_features = ["*"]; opts.notdef_outline = True
sub = Subsetter(opts); sub.populate(unicodes=chars); sub.subset(f)
buf = io.BytesIO(); f.flavor = "woff2"; f.save(buf)
b64 = base64.b64encode(buf.getvalue()).decode()
rules.append(f"@font-face{{font-family:'{FAMILY}';font-weight:{weights};font-style:normal;"
f"src:url(data:font/woff2;base64,{b64}) format('woff2')}}")
return "".join(rules)
_FACE = re.compile(r"(@font-face\s*\{[^}]*\})")
def add_fallback(text):
"""Append MPM SC to every font stack (CSS declarations and SVG font-family attributes),
leaving @font-face rules alone."""
def css(m):
v = m.group(1)
return m.group(0) if FAMILY in v else f"font-family:{v.rstrip()},'{FAMILY}'"
def attr(m):
v = m.group(1)
return m.group(0) if FAMILY in v else f"font-family=\"{v},'{FAMILY}'\""
parts = _FACE.split(text)
for i, p in enumerate(parts):
if i % 2 == 0:
p = re.sub(r"font-family:\s*([^;}\"]+)", css, p)
p = re.sub(r'font-family="([^"]+)"', attr, p)
parts[i] = p
return "".join(parts)
# ---- translation ---------------------------------------------------------------------------
def load_glossary(path=GLOSSARY):
"""Master MPM terms: `| English | 中文 | note |` rows in _STANDARDS/glossary_zh.md.
Keys are matched case-insensitively against whole strings."""
g = {}
if not Path(path).exists():
return g
for line in Path(path).read_text().splitlines():
if not line.startswith("|") or line.startswith("|---") or line.startswith("| English"):
continue
cells = [c.strip() for c in line.strip().strip("|").split("|")]
if len(cells) >= 2 and cells[0] and cells[1]:
g[cells[0].upper()] = cells[1]
return g
UNITS = re.compile(r"(?<![A-Za-z])(mm|cm|m|in|ft|W|kW|Wh|V|VAC|VDC|A|mA|Hz|kHz|kg|g|lb|lbs|N|N·m|°C|°F|nits|cd/m²|Mbps|ms|h|min|s|px|IP\d\d)(?![A-Za-z])")
KEEP = re.compile(r"^([A-Z]{1,3}-?\d+[A-Z]?|[A-Z]{1,3}\d+(\.\d+)?|LM-[\w.-]+|[\d\s.,:;×xX\[\]\"'/+\-−–°Ø()%=<>≤≥~]+)$")
class Translator:
"""Translates figure and table text for a Chinese edition.
tr = i18n.Translator({"VIEWED FROM PARKING LOT": "从停车场方向看"},
suffixes=[(" POST CL TO CL", " 立柱中心距")], keep=r"^LM-")
svg_zh = tr.svg(svg_en) # every <text>/<tspan> run
s_zh = tr("BRACKET") # one string
tr.check() # raise, listing every English string left untranslated
Lookup order: the document's own table (exact), then the master glossary (whole string,
case-insensitive), then suffix rules for dimension labels ("3840 ACTIVE FACE"). Numbers, tags
(S-1, M3), part numbers and strings already in Chinese pass through."""
def __init__(self, table=None, suffixes=(), keep=None, glossary=True):
self.table = dict(table or {})
self.gloss = load_glossary() if glossary else {}
self.suffixes = list(suffixes)
self.keep = re.compile(keep) if keep else None
self.missing = []
self._longest = sorted(self.gloss.items(), key=lambda kv: -len(kv[0]))
def __call__(self, s):
if s in self.table:
return self.table[s]
t = s.strip()
if not t or has_cjk(t):
return s
if t.upper() in self.gloss:
return self.gloss[t.upper()]
if t[:1].isdigit() or t[:1] in "−-[":
for en, zh in self.suffixes:
if s.endswith(en):
return s[: -len(en)] + zh
for en, zh in self._longest: # "3840 ACTIVE FACE": number + glossary term, longest first
if t.upper().endswith(" " + en):
return s[: len(s) - len(en)] + zh
if KEEP.match(UNITS.sub("", t)):
return s
if KEEP.match(t) or (self.keep and self.keep.search(t)):
return s
self.missing.append(s)
return s
def svg(self, svg):
return re.sub(r">([^<>]+)</(text|tspan)>", lambda m: ">" + self(m.group(1)) + f"</{m.group(2)}>", svg)
def check(self):
if self.missing:
raise SystemExit("Untranslated text (add to the document table or glossary_zh.md): "
+ repr(sorted(set(self.missing))))