返回 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 (confirm / "template_options.json").write_text(
101 json.dumps(
102 {
103 "schema_version": 1,
104 "phase": "template",
105 "default_mode": "free_design",
106 "explicit_workspace_roots": [],
107 }
108 ),
109 encoding="utf-8",
110 )
111 app = create_app(str(project), idle_timeout=0)
112 app.testing = True
113 client = app.test_client()
114 response = client.get("/api/recommendations")
115 assert response.status_code == 200
116 assert response.get_json()["primary_language"] == "ar-SA"
117 response = client.post(
118 "/api/confirm",
119 json={
120 "stage": "stage1",
121 "template_selection": {
122 "mode": "free_design",
123 "selection_keys": [],
124 },
125 "primary_language": "ar-SA",
126 "canvas": "ppt169",
127 "audience": "Team",
128 "communication_intent": "Explain",
129 "audience_outcome": "Understand",
130 "core_message": "Result",
131 "delivery_context": "Meeting",
132 "artifact_afterlife": "",
133 "content_divergence": "",
134 },
135 )
136 assert response.status_code == 200
137 result = json.loads((confirm / "result.json").read_text(encoding="utf-8"))
138 assert result["primary_language"] == "ar-SA"
139
140 # The execution lock is the only export-time project-language source.
141 lock_project = root / "lock-project"
142 lock_project.mkdir()
143 lock_template = """# Execution Lock
144
145 ## communication
146 {language}- audience: team
147 - objective: explain
148 - core_message: result
149 """
150 lock = lock_project / "spec_lock.md"
151 lock.write_text(
152 lock_template.format(language="- primary_language: ES_mx\n"),
153 encoding="utf-8",
154 )
155 assert _declared_primary_language(lock_project) == "es-MX"
156 lock.write_text(lock_template.format(language=""), encoding="utf-8")
157 assert _declared_primary_language(lock_project) is None
158
159 for index, (language, sample) in enumerate(samples.items(), 1):
160 stem = f"slide-{index}"
161 svg = root / f"{stem}.svg"
162 svg.write_text(
163 '<svg xmlns="http://www.w3.org/2000/svg" '
164 'viewBox="0 0 1280 720">'
165 '<rect x="0" y="0" width="1280" height="720" fill="#FFFFFF"/>'
166 f'<text x="100" y="140" font-size="42" '
167 f'font-family="Arial" fill="#111111">{sample}</text>'
168 "</svg>",
169 encoding="utf-8",
170 )
171
172 slide_xml, *_ = convert_svg_to_slide_shapes(
173 svg,
174 index,
175 verbose=False,
176 primary_language=language,
177 )
178 assert f'lang="{language}"' in slide_xml
179 assert ("rtl=\"1\"" in slide_xml) == (language in rtl_languages)
180 assert ("<a:rtl val=\"1\"/>" in slide_xml) == (
181 language in rtl_languages
182 )
183
184 ascii_svg = root / f"{stem}-ascii.svg"
185 ascii_svg.write_text(
186 '<svg xmlns="http://www.w3.org/2000/svg" '
187 'viewBox="0 0 1280 720">'
188 '<text x="100" y="140" font-size="42">2026 AI</text>'
189 "</svg>",
190 encoding="utf-8",
191 )
192 ascii_xml, *_ = convert_svg_to_slide_shapes(
193 ascii_svg,
194 index + 20,
195 verbose=False,
196 primary_language=language,
197 )
198 assert f'lang="{language}"' in ascii_xml
199 assert 'rtl="1"' not in ascii_xml
200 assert "<a:rtl val=\"1\"/>" not in ascii_xml
201
202 context = ConvertContext(primary_language=language)
203 marker = ET.Element("g")
204 table = _build_native_table(
205 marker,
206 context,
207 {
208 "x": 10,
209 "y": 10,
210 "width": 600,
211 "height": 180,
212 "columns": [sample, "2026 AI"],
213 "rows": [["A", "B"]],
214 "style": {"font_family": "Arial"},
215 },
216 )
217 assert f'lang="{language}"' in table.xml
218 assert all(slot in table.xml for slot in ("<a:latin ", "<a:ea ", "<a:cs "))
219
220 chart_context = ConvertContext(primary_language=language)
221 _build_native_chart(
222 marker,
223 chart_context,
224 {
225 "x": 10,
226 "y": 220,
227 "width": 600,
228 "height": 300,
229 "type": "column",
230 "title": sample,
231 "categories": ["A", "B"],
232 "series": [{"name": sample, "values": [1, 2]}],
233 "style": {"font_family": "Arial"},
234 "show_legend": True,
235 },
236 )
237 chart_xml = next(
238 value.decode("utf-8")
239 for part, value in chart_context.package_files.items()
240 if part.startswith("ppt/charts/chart") and not part.endswith(".rels")
241 )
242 assert f'<c:lang val="{language}"/>' in chart_xml
243 assert f'lang="{language}"' in chart_xml
244
245 notes_xml = create_notes_slide_xml(
246 1,
247 sample + "\n2026 AI",
248 language,
249 )
250 assert f'lang="{language}"' in notes_xml
251 assert ("rtl=\"1\"" in notes_xml) == (language in rtl_languages)
252
253 output = root / f"{index}.pptx"
254 assert create_pptx_with_native_svg(
255 [svg],
256 output,
257 canvas_format="ppt169",
258 verbose=False,
259 transition=None,
260 notes={stem: sample + "\n2026 AI"},
261 pptx_structure="flat",
262 structure_name="multilingual-smoke",
263 primary_language=language,
264 )
265 with zipfile.ZipFile(output) as archive:
266 assert archive.testzip() is None
267 packaged_slide = archive.read(
268 "ppt/slides/slide1.xml"
269 ).decode("utf-8")
270 packaged_notes = archive.read(
271 "ppt/notesSlides/notesSlide1.xml"
272 ).decode("utf-8")
273 core = archive.read("docProps/core.xml").decode("utf-8")
274 assert f'lang="{language}"' in packaged_slide
275 assert f'lang="{language}"' in packaged_notes
276 assert f"<dc:language>{language}</dc:language>" in core
277 assert len(Presentation(str(output)).slides) == 1
278
279 # A legacy lock with no language field keeps the previous per-run path.
280 legacy_svg = root / "legacy.svg"
281 legacy_svg.write_text(
282 '<svg xmlns="http://www.w3.org/2000/svg" '
283 'viewBox="0 0 1280 720">'
284 '<text x="100" y="140" font-size="42">한국어 2026</text>'
285 "</svg>",
286 encoding="utf-8",
287 )
288 legacy_output = root / "legacy.pptx"
289 assert create_pptx_with_native_svg(
290 [legacy_svg],
291 legacy_output,
292 canvas_format="ppt169",
293 verbose=False,
294 transition=None,
295 pptx_structure="flat",
296 structure_name="legacy-smoke",
297 )
298 with zipfile.ZipFile(legacy_output) as archive:
299 assert archive.testzip() is None
300 legacy_slide = archive.read(
301 "ppt/slides/slide1.xml"
302 ).decode("utf-8")
303 assert 'lang="ko-KR"' in legacy_slide
304
305 print("Multilingual text smoke: passed")
306 PY
307 ```
308
309 Expected output:
310
311 ```text
312 Multilingual text smoke: passed
313 ```
314
315 The RTL contract is paragraph `a:pPr rtl="1"` plus run-level `a:rtl` only
316 when the run contains strong RTL characters. Do not use `rtlCol`; it controls
317 column order, not paragraph direction.
318
318 lines MARKDOWN