"""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=, 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 of ' return '第 页 / 共 页' # ---- 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"(?≤≥~]+)$") 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 / 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">([^<>]+)", lambda m: ">" + self(m.group(1)) + f"", 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))))