Files

9.9 KiB

Multilingual Text Maintenance Smoke

Run this manual smoke from the repository root after changing Confirm UI language handling, DrawingML text export, native tables/charts, notes, or document metadata. It uses only temporary files and does not add an automated test suite.

PYTHONPATH="skills/ppt-master/scripts:skills/ppt-master/scripts/confirm_ui" python3 - <<'PY'
import json
import zipfile
from pathlib import Path
from tempfile import TemporaryDirectory
from xml.etree import ElementTree as ET

from pptx import Presentation

from confirm_ui.server import create_app
from language_tags import (
    LanguageTagError,
    language_uses_rtl,
    normalize_language_tag,
)
from svg_to_pptx.drawingml.context import ConvertContext
from svg_to_pptx.drawingml.converter import convert_svg_to_slide_shapes
from svg_to_pptx.native_objects import _build_native_chart
from svg_to_pptx.native_objects.table import _build_native_table
from svg_to_pptx.pptx_package.builder import create_pptx_with_native_svg
from svg_to_pptx.pptx_package.cli import _declared_primary_language
from svg_to_pptx.pptx_package.notes import create_notes_slide_xml


def reject_language(value):
    try:
        normalize_language_tag(value)
    except LanguageTagError:
        return
    raise AssertionError(f"invalid language accepted: {value}")


canonical = {
    "ES_mx": "es-MX",
    "RU_ru": "ru-RU",
    "AR_sa": "ar-SA",
    "HE_il": "he-IL",
    "HI_in": "hi-IN",
    "TH_th": "th-TH",
    "KO_kr": "ko-KR",
    "fil_ph": "fil-PH",
    "zh_hans": "zh-Hans",
    "de-CH-1901": "de-CH-1901",
    "en-u-nu-latn": "en-u-nu-latn",
}
for raw, expected in canonical.items():
    assert normalize_language_tag(raw) == expected
for raw in ("und", "zh", "en--US", "Arabic", "x-private"):
    reject_language(raw)
assert language_uses_rtl("ar-Arab-SA")
assert not language_uses_rtl("ar-Latn-SA")
assert language_uses_rtl("en-Arab-US")

samples = {
    "en-US": "Summary 2026",
    "zh-Hans": "年度总结 2026",
    "ja-JP": "年間まとめ 2026",
    "ko-KR": "연간 요약 2026",
    "es-ES": "Resumen 2026",
    "ru-RU": "Итоги 2026",
    "ar-SA": "ملخص 2026",
    "he-IL": "סיכום 2026",
    "hi-IN": "सारांश 2026",
    "th-TH": "สรุป 2026",
}
rtl_languages = {"ar-SA", "he-IL"}

with TemporaryDirectory(prefix="ppt-master-multilingual-smoke-") as tmp:
    root = Path(tmp)

    # Confirm UI canonicalizes Stage 1 and persists the same project language.
    project = root / "confirm-project"
    confirm = project / "confirm_ui"
    confirm.mkdir(parents=True)
    recommendation = {
        "stage": "stage1",
        "lang": "en",
        "primary_language": "AR_sa",
        "audience": {"value": "Team"},
        "communication_intent": {"value": "Explain"},
        "audience_outcome": {"value": "Understand"},
        "core_message": {"value": "Result"},
        "delivery_context": {"value": "Meeting"},
        "artifact_afterlife": {"value": ""},
        "content_divergence": {"value": ""},
        "recommend": {"canvas": "ppt169"},
    }
    (confirm / "recommendations.stage1.json").write_text(
        json.dumps(recommendation),
        encoding="utf-8",
    )
    app = create_app(str(project), idle_timeout=0)
    app.testing = True
    client = app.test_client()
    response = client.get("/api/recommendations")
    assert response.status_code == 200
    assert response.get_json()["primary_language"] == "ar-SA"
    response = client.post(
        "/api/confirm",
        json={
            "stage": "stage1",
            "primary_language": "ar-SA",
            "canvas": "ppt169",
            "audience": "Team",
            "communication_intent": "Explain",
            "audience_outcome": "Understand",
            "core_message": "Result",
            "delivery_context": "Meeting",
            "artifact_afterlife": "",
            "content_divergence": "",
        },
    )
    assert response.status_code == 200
    result = json.loads((confirm / "result.json").read_text(encoding="utf-8"))
    assert result["primary_language"] == "ar-SA"

    # The execution lock is the only export-time project-language source.
    lock_project = root / "lock-project"
    lock_project.mkdir()
    lock_template = """# Execution Lock

## communication
{language}- audience: team
- objective: explain
- core_message: result
"""
    lock = lock_project / "spec_lock.md"
    lock.write_text(
        lock_template.format(language="- primary_language: ES_mx\n"),
        encoding="utf-8",
    )
    assert _declared_primary_language(lock_project) == "es-MX"
    lock.write_text(lock_template.format(language=""), encoding="utf-8")
    assert _declared_primary_language(lock_project) is None

    for index, (language, sample) in enumerate(samples.items(), 1):
        stem = f"slide-{index}"
        svg = root / f"{stem}.svg"
        svg.write_text(
            '<svg xmlns="http://www.w3.org/2000/svg" '
            'viewBox="0 0 1280 720">'
            '<rect x="0" y="0" width="1280" height="720" fill="#FFFFFF"/>'
            f'<text x="100" y="140" font-size="42" '
            f'font-family="Arial" fill="#111111">{sample}</text>'
            "</svg>",
            encoding="utf-8",
        )

        slide_xml, *_ = convert_svg_to_slide_shapes(
            svg,
            index,
            verbose=False,
            primary_language=language,
        )
        assert f'lang="{language}"' in slide_xml
        assert ("rtl=\"1\"" in slide_xml) == (language in rtl_languages)
        assert ("<a:rtl val=\"1\"/>" in slide_xml) == (
            language in rtl_languages
        )

        ascii_svg = root / f"{stem}-ascii.svg"
        ascii_svg.write_text(
            '<svg xmlns="http://www.w3.org/2000/svg" '
            'viewBox="0 0 1280 720">'
            '<text x="100" y="140" font-size="42">2026 AI</text>'
            "</svg>",
            encoding="utf-8",
        )
        ascii_xml, *_ = convert_svg_to_slide_shapes(
            ascii_svg,
            index + 20,
            verbose=False,
            primary_language=language,
        )
        assert f'lang="{language}"' in ascii_xml
        assert 'rtl="1"' not in ascii_xml
        assert "<a:rtl val=\"1\"/>" not in ascii_xml

        context = ConvertContext(primary_language=language)
        marker = ET.Element("g")
        table = _build_native_table(
            marker,
            context,
            {
                "x": 10,
                "y": 10,
                "width": 600,
                "height": 180,
                "columns": [sample, "2026 AI"],
                "rows": [["A", "B"]],
                "style": {"font_family": "Arial"},
            },
        )
        assert f'lang="{language}"' in table.xml
        assert all(slot in table.xml for slot in ("<a:latin ", "<a:ea ", "<a:cs "))

        chart_context = ConvertContext(primary_language=language)
        _build_native_chart(
            marker,
            chart_context,
            {
                "x": 10,
                "y": 220,
                "width": 600,
                "height": 300,
                "type": "column",
                "title": sample,
                "categories": ["A", "B"],
                "series": [{"name": sample, "values": [1, 2]}],
                "style": {"font_family": "Arial"},
                "show_legend": True,
            },
        )
        chart_xml = next(
            value.decode("utf-8")
            for part, value in chart_context.package_files.items()
            if part.startswith("ppt/charts/chart") and not part.endswith(".rels")
        )
        assert f'<c:lang val="{language}"/>' in chart_xml
        assert f'lang="{language}"' in chart_xml

        notes_xml = create_notes_slide_xml(
            1,
            sample + "\n2026 AI",
            language,
        )
        assert f'lang="{language}"' in notes_xml
        assert ("rtl=\"1\"" in notes_xml) == (language in rtl_languages)

        output = root / f"{index}.pptx"
        assert create_pptx_with_native_svg(
            [svg],
            output,
            canvas_format="ppt169",
            verbose=False,
            transition=None,
            notes={stem: sample + "\n2026 AI"},
            pptx_structure="flat",
            structure_name="multilingual-smoke",
            primary_language=language,
        )
        with zipfile.ZipFile(output) as archive:
            assert archive.testzip() is None
            packaged_slide = archive.read(
                "ppt/slides/slide1.xml"
            ).decode("utf-8")
            packaged_notes = archive.read(
                "ppt/notesSlides/notesSlide1.xml"
            ).decode("utf-8")
            core = archive.read("docProps/core.xml").decode("utf-8")
            assert f'lang="{language}"' in packaged_slide
            assert f'lang="{language}"' in packaged_notes
            assert f"<dc:language>{language}</dc:language>" in core
        assert len(Presentation(str(output)).slides) == 1

    # A legacy lock with no language field keeps the previous per-run path.
    legacy_svg = root / "legacy.svg"
    legacy_svg.write_text(
        '<svg xmlns="http://www.w3.org/2000/svg" '
        'viewBox="0 0 1280 720">'
        '<text x="100" y="140" font-size="42">한국어 2026</text>'
        "</svg>",
        encoding="utf-8",
    )
    legacy_output = root / "legacy.pptx"
    assert create_pptx_with_native_svg(
        [legacy_svg],
        legacy_output,
        canvas_format="ppt169",
        verbose=False,
        transition=None,
        pptx_structure="flat",
        structure_name="legacy-smoke",
    )
    with zipfile.ZipFile(legacy_output) as archive:
        assert archive.testzip() is None
        legacy_slide = archive.read(
            "ppt/slides/slide1.xml"
        ).decode("utf-8")
        assert 'lang="ko-KR"' in legacy_slide

print("Multilingual text smoke: passed")
PY

Expected output:

Multilingual text smoke: passed

The RTL contract is paragraph a:pPr rtl="1" plus run-level a:rtl only when the run contains strong RTL characters. Do not use rtlCol; it controls column order, not paragraph direction.