返回 ppt-master
multilingual-text-smoke.md
根目录 / skills / ppt-master / scripts / docs / multilingual-text-smoke.md
1 # Multilingual Text Maintenance Smoke
2
3 Run this manual smoke from the repository root after changing Confirm UI
4 language handling, DrawingML text export, native tables/charts, notes, or
5 document metadata. It uses only temporary files and does not add an automated
6 test suite.
7
8 ```bash
9 PYTHONPATH="skills/ppt-master/scripts:skills/ppt-master/scripts/confirm_ui" python3 - <<'PY'
10 import json
11 import zipfile
12 from pathlib import Path
13 from tempfile import TemporaryDirectory
14 from xml.etree import ElementTree as ET
15
16 from pptx import Presentation
17
18 from confirm_ui.server import create_app
19 from language_tags import (
20 LanguageTagError,
21 language_uses_rtl,
22 normalize_language_tag,
23 )
24 from svg_to_pptx.drawingml.context import ConvertContext
25 from svg_to_pptx.drawingml.converter import convert_svg_to_slide_shapes
26 from svg_to_pptx.native_objects import _build_native_chart
27 from svg_to_pptx.native_objects.table import _build_native_table
28 from svg_to_pptx.pptx_package.builder import create_pptx_with_native_svg
29 from svg_to_pptx.pptx_package.cli import _declared_primary_language
30 from svg_to_pptx.pptx_package.notes import create_notes_slide_xml
31
32
33 def reject_language(value):
34 try:
35 normalize_language_tag(value)
36 except LanguageTagError:
37 return
38 raise AssertionError(f"invalid language accepted: {value}")
39
40
41 canonical = {
42 "ES_mx": "es-MX",
43 "RU_ru": "ru-RU",
44 "AR_sa": "ar-SA",
45 "HE_il": "he-IL",
46 "HI_in": "hi-IN",
47 "TH_th": "th-TH",
48 "KO_kr": "ko-KR",
49 "fil_ph": "fil-PH",
50 "zh_hans": "zh-Hans",
51 "de-CH-1901": "de-CH-1901",
52 "en-u-nu-latn": "en-u-nu-latn",
53 }
54 for raw, expected in canonical.items():
55 assert normalize_language_tag(raw) == expected
56 for raw in ("und", "zh", "en--US", "Arabic", "x-private"):
57 reject_language(raw)
58 assert language_uses_rtl("ar-Arab-SA")
59 assert not language_uses_rtl("ar-Latn-SA")
60 assert language_uses_rtl("en-Arab-US")
61
62 samples = {
63 "en-US": "Summary 2026",
64 "zh-Hans": "年度总结 2026",
65 "ja-JP": "年間まとめ 2026",
66 "ko-KR": "연간 요약 2026",
67 "es-ES": "Resumen 2026",
68 "ru-RU": "Итоги 2026",
69 "ar-SA": "ملخص 2026",
70 "he-IL": "סיכום 2026",
71 "hi-IN": "सारांश 2026",
72 "th-TH": "สรุป 2026",
73 }
74 rtl_languages = {"ar-SA", "he-IL"}
75
76 with TemporaryDirectory(prefix="ppt-master-multilingual-smoke-") as tmp:
77 root = Path(tmp)
78
79 # Confirm UI canonicalizes Stage 1 and persists the same project language.
80 project = root / "confirm-project"
81 confirm = project / "confirm_ui"
82 confirm.mkdir(parents=True)
83 recommendation = {
84 "stage": "stage1",
85 "lang": "en",
86 "primary_language": "AR_sa",
87 "audience": {"value": "Team"},
88 "communication_intent": {"value": "Explain"},
89 "audience_outcome": {"value": "Understand"},
90 "core_message": {"value": "Result"},
91 "delivery_context": {"value": "Meeting"},
92 "artifact_afterlife": {"value": ""},
93 "content_divergence": {"value": ""},
94 "recommend": {"canvas": "ppt169"},
95 }
96 (confirm / "recommendations.stage1.json").write_text(
97 json.dumps(recommendation),
98 encoding="utf-8",
99 )
100 app = create_app(str(project), idle_timeout=0)
101 app.testing = True
102 client = app.test_client()
103 response = client.get("/api/recommendations")
104 assert response.status_code == 200
105 assert response.get_json()["primary_language"] == "ar-SA"
106 response = client.post(
107 "/api/confirm",
108 json={
109 "stage": "stage1",
110 "primary_language": "ar-SA",
111 "canvas": "ppt169",
112 "audience": "Team",
113 "communication_intent": "Explain",
114 "audience_outcome": "Understand",
115 "core_message": "Result",
116 "delivery_context": "Meeting",
117 "artifact_afterlife": "",
118 "content_divergence": "",
119 },
120 )
121 assert response.status_code == 200
122 result = json.loads((confirm / "result.json").read_text(encoding="utf-8"))
123 assert result["primary_language"] == "ar-SA"
124
125 # The execution lock is the only export-time project-language source.
126 lock_project = root / "lock-project"
127 lock_project.mkdir()
128 lock_template = """# Execution Lock
129
130 ## communication
131 {language}- audience: team
132 - objective: explain
133 - core_message: result
134 """
135 lock = lock_project / "spec_lock.md"
136 lock.write_text(
137 lock_template.format(language="- primary_language: ES_mx\n"),
138 encoding="utf-8",
139 )
140 assert _declared_primary_language(lock_project) == "es-MX"
141 lock.write_text(lock_template.format(language=""), encoding="utf-8")
142 assert _declared_primary_language(lock_project) is None
143
144 for index, (language, sample) in enumerate(samples.items(), 1):
145 stem = f"slide-{index}"
146 svg = root / f"{stem}.svg"
147 svg.write_text(
148 '<svg xmlns="http://www.w3.org/2000/svg" '
149 'viewBox="0 0 1280 720">'
150 '<rect x="0" y="0" width="1280" height="720" fill="#FFFFFF"/>'
151 f'<text x="100" y="140" font-size="42" '
152 f'font-family="Arial" fill="#111111">{sample}</text>'
153 "</svg>",
154 encoding="utf-8",
155 )
156
157 slide_xml, *_ = convert_svg_to_slide_shapes(
158 svg,
159 index,
160 verbose=False,
161 primary_language=language,
162 )
163 assert f'lang="{language}"' in slide_xml
164 assert ("rtl=\"1\"" in slide_xml) == (language in rtl_languages)
165 assert ("<a:rtl val=\"1\"/>" in slide_xml) == (
166 language in rtl_languages
167 )
168
169 ascii_svg = root / f"{stem}-ascii.svg"
170 ascii_svg.write_text(
171 '<svg xmlns="http://www.w3.org/2000/svg" '
172 'viewBox="0 0 1280 720">'
173 '<text x="100" y="140" font-size="42">2026 AI</text>'
174 "</svg>",
175 encoding="utf-8",
176 )
177 ascii_xml, *_ = convert_svg_to_slide_shapes(
178 ascii_svg,
179 index + 20,
180 verbose=False,
181 primary_language=language,
182 )
183 assert f'lang="{language}"' in ascii_xml
184 assert 'rtl="1"' not in ascii_xml
185 assert "<a:rtl val=\"1\"/>" not in ascii_xml
186
187 context = ConvertContext(primary_language=language)
188 marker = ET.Element("g")
189 table = _build_native_table(
190 marker,
191 context,
192 {
193 "x": 10,
194 "y": 10,
195 "width": 600,
196 "height": 180,
197 "columns": [sample, "2026 AI"],
198 "rows": [["A", "B"]],
199 "style": {"font_family": "Arial"},
200 },
201 )
202 assert f'lang="{language}"' in table.xml
203 assert all(slot in table.xml for slot in ("<a:latin ", "<a:ea ", "<a:cs "))
204
205 chart_context = ConvertContext(primary_language=language)
206 _build_native_chart(
207 marker,
208 chart_context,
209 {
210 "x": 10,
211 "y": 220,
212 "width": 600,
213 "height": 300,
214 "type": "column",
215 "title": sample,
216 "categories": ["A", "B"],
217 "series": [{"name": sample, "values": [1, 2]}],
218 "style": {"font_family": "Arial"},
219 "show_legend": True,
220 },
221 )
222 chart_xml = next(
223 value.decode("utf-8")
224 for part, value in chart_context.package_files.items()
225 if part.startswith("ppt/charts/chart") and not part.endswith(".rels")
226 )
227 assert f'<c:lang val="{language}"/>' in chart_xml
228 assert f'lang="{language}"' in chart_xml
229
230 notes_xml = create_notes_slide_xml(
231 1,
232 sample + "\n2026 AI",
233 language,
234 )
235 assert f'lang="{language}"' in notes_xml
236 assert ("rtl=\"1\"" in notes_xml) == (language in rtl_languages)
237
238 output = root / f"{index}.pptx"
239 assert create_pptx_with_native_svg(
240 [svg],
241 output,
242 canvas_format="ppt169",
243 verbose=False,
244 transition=None,
245 notes={stem: sample + "\n2026 AI"},
246 pptx_structure="flat",
247 structure_name="multilingual-smoke",
248 primary_language=language,
249 )
250 with zipfile.ZipFile(output) as archive:
251 assert archive.testzip() is None
252 packaged_slide = archive.read(
253 "ppt/slides/slide1.xml"
254 ).decode("utf-8")
255 packaged_notes = archive.read(
256 "ppt/notesSlides/notesSlide1.xml"
257 ).decode("utf-8")
258 core = archive.read("docProps/core.xml").decode("utf-8")
259 assert f'lang="{language}"' in packaged_slide
260 assert f'lang="{language}"' in packaged_notes
261 assert f"<dc:language>{language}</dc:language>" in core
262 assert len(Presentation(str(output)).slides) == 1
263
264 # A legacy lock with no language field keeps the previous per-run path.
265 legacy_svg = root / "legacy.svg"
266 legacy_svg.write_text(
267 '<svg xmlns="http://www.w3.org/2000/svg" '
268 'viewBox="0 0 1280 720">'
269 '<text x="100" y="140" font-size="42">한국어 2026</text>'
270 "</svg>",
271 encoding="utf-8",
272 )
273 legacy_output = root / "legacy.pptx"
274 assert create_pptx_with_native_svg(
275 [legacy_svg],
276 legacy_output,
277 canvas_format="ppt169",
278 verbose=False,
279 transition=None,
280 pptx_structure="flat",
281 structure_name="legacy-smoke",
282 )
283 with zipfile.ZipFile(legacy_output) as archive:
284 assert archive.testzip() is None
285 legacy_slide = archive.read(
286 "ppt/slides/slide1.xml"
287 ).decode("utf-8")
288 assert 'lang="ko-KR"' in legacy_slide
289
290 print("Multilingual text smoke: passed")
291 PY
292 ```
293
294 Expected output:
295
296 ```text
297 Multilingual text smoke: passed
298 ```
299
300 The RTL contract is paragraph `a:pPr rtl="1"` plus run-level `a:rtl` only
301 when the run contains strong RTL characters. Do not use `rtlCol`; it controls
302 column order, not paragraph direction.
303
303 lines MARKDOWN